Commit d1c52383 by Sofia Papagiannaki Committed by GitHub

API: Optionally list expired keys (#20468)

* API: Optionally list expired keys

* Update docs
parent 1b38d945
...@@ -67,6 +67,10 @@ Content-Type: application/json ...@@ -67,6 +67,10 @@ Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
``` ```
Query Parameters:
- `includeExpired`: boolean. enable listing of expired keys. Optional.
**Example Response**: **Example Response**:
```http ```http
......
...@@ -10,7 +10,7 @@ import ( ...@@ -10,7 +10,7 @@ import (
) )
func GetAPIKeys(c *models.ReqContext) Response { func GetAPIKeys(c *models.ReqContext) Response {
query := models.GetApiKeysQuery{OrgId: c.OrgId} query := models.GetApiKeysQuery{OrgId: c.OrgId, IncludeExpired: c.QueryBool("includeExpired")}
if err := bus.Dispatch(&query); err != nil { if err := bus.Dispatch(&query); err != nil {
return Error(500, "Failed to list api keys", err) return Error(500, "Failed to list api keys", err)
......
...@@ -50,7 +50,7 @@ type DeleteApiKeyCommand struct { ...@@ -50,7 +50,7 @@ type DeleteApiKeyCommand struct {
type GetApiKeysQuery struct { type GetApiKeysQuery struct {
OrgId int64 OrgId int64
IncludeInvalid bool IncludeExpired bool
Result []*ApiKey Result []*ApiKey
} }
......
...@@ -19,7 +19,7 @@ func init() { ...@@ -19,7 +19,7 @@ func init() {
func GetApiKeys(query *models.GetApiKeysQuery) error { func GetApiKeys(query *models.GetApiKeysQuery) error {
sess := x.Limit(100, 0).Where("org_id=? and ( expires IS NULL or expires >= ?)", sess := x.Limit(100, 0).Where("org_id=? and ( expires IS NULL or expires >= ?)",
query.OrgId, timeNow().Unix()).Asc("name") query.OrgId, timeNow().Unix()).Asc("name")
if query.IncludeInvalid { if query.IncludeExpired {
sess = x.Limit(100, 0).Where("org_id=?", query.OrgId).Asc("name") sess = x.Limit(100, 0).Where("org_id=?", query.OrgId).Asc("name")
} }
......
...@@ -91,7 +91,7 @@ func TestApiKeyDataAccess(t *testing.T) { ...@@ -91,7 +91,7 @@ func TestApiKeyDataAccess(t *testing.T) {
// advance mocked getTime by 1s // advance mocked getTime by 1s
timeNow() timeNow()
query := models.GetApiKeysQuery{OrgId: 1, IncludeInvalid: false} query := models.GetApiKeysQuery{OrgId: 1, IncludeExpired: false}
err = GetApiKeys(&query) err = GetApiKeys(&query)
assert.Nil(t, err) assert.Nil(t, err)
...@@ -101,7 +101,7 @@ func TestApiKeyDataAccess(t *testing.T) { ...@@ -101,7 +101,7 @@ func TestApiKeyDataAccess(t *testing.T) {
} }
} }
query = models.GetApiKeysQuery{OrgId: 1, IncludeInvalid: true} query = models.GetApiKeysQuery{OrgId: 1, IncludeExpired: true}
err = GetApiKeys(&query) err = GetApiKeys(&query)
assert.Nil(t, err) assert.Nil(t, err)
......
...@@ -23,6 +23,7 @@ const setup = (propOverrides?: object) => { ...@@ -23,6 +23,7 @@ const setup = (propOverrides?: object) => {
setSearchQuery: jest.fn(), setSearchQuery: jest.fn(),
addApiKey: jest.fn(), addApiKey: jest.fn(),
apiKeysCount: 0, apiKeysCount: 0,
includeExpired: false,
}; };
Object.assign(props, propOverrides); Object.assign(props, propOverrides);
...@@ -63,7 +64,7 @@ describe('Life cycle', () => { ...@@ -63,7 +64,7 @@ describe('Life cycle', () => {
instance.componentDidMount(); instance.componentDidMount();
expect(instance.props.loadApiKeys).toHaveBeenCalled(); expect(instance.props.loadApiKeys).toHaveBeenCalledWith(false);
}); });
}); });
...@@ -72,7 +73,7 @@ describe('Functions', () => { ...@@ -72,7 +73,7 @@ describe('Functions', () => {
it('should call delete team', () => { it('should call delete team', () => {
const { instance } = setup(); const { instance } = setup();
instance.onDeleteApiKey(getMockKey()); instance.onDeleteApiKey(getMockKey());
expect(instance.props.deleteApiKey).toHaveBeenCalledWith(1); expect(instance.props.deleteApiKey).toHaveBeenCalledWith(1, false);
}); });
}); });
......
...@@ -12,7 +12,7 @@ import ApiKeysAddedModal from './ApiKeysAddedModal'; ...@@ -12,7 +12,7 @@ import ApiKeysAddedModal from './ApiKeysAddedModal';
import config from 'app/core/config'; import config from 'app/core/config';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { DeleteButton, EventsWithValidation, FormLabel, Input, ValidationEvents } from '@grafana/ui'; import { DeleteButton, EventsWithValidation, FormLabel, Input, Switch, ValidationEvents } from '@grafana/ui';
import { NavModel, dateTime, isDateTime } from '@grafana/data'; import { NavModel, dateTime, isDateTime } from '@grafana/data';
import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
import { store } from 'app/store/store'; import { store } from 'app/store/store';
...@@ -51,6 +51,7 @@ export interface Props { ...@@ -51,6 +51,7 @@ export interface Props {
setSearchQuery: typeof setSearchQuery; setSearchQuery: typeof setSearchQuery;
addApiKey: typeof addApiKey; addApiKey: typeof addApiKey;
apiKeysCount: number; apiKeysCount: number;
includeExpired: boolean;
} }
export interface State { export interface State {
...@@ -76,7 +77,7 @@ const tooltipText = ...@@ -76,7 +77,7 @@ const tooltipText =
export class ApiKeysPage extends PureComponent<Props, any> { export class ApiKeysPage extends PureComponent<Props, any> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = { isAdding: false, newApiKey: initialApiKeyState }; this.state = { isAdding: false, newApiKey: initialApiKeyState, includeExpired: false };
} }
componentDidMount() { componentDidMount() {
...@@ -84,17 +85,21 @@ export class ApiKeysPage extends PureComponent<Props, any> { ...@@ -84,17 +85,21 @@ export class ApiKeysPage extends PureComponent<Props, any> {
} }
async fetchApiKeys() { async fetchApiKeys() {
await this.props.loadApiKeys(); await this.props.loadApiKeys(this.state.includeExpired);
} }
onDeleteApiKey(key: ApiKey) { onDeleteApiKey(key: ApiKey) {
this.props.deleteApiKey(key.id); this.props.deleteApiKey(key.id, this.props.includeExpired);
} }
onSearchQueryChange = (value: string) => { onSearchQueryChange = (value: string) => {
this.props.setSearchQuery(value); this.props.setSearchQuery(value);
}; };
onIncludeExpiredChange = (value: boolean) => {
this.setState({ hasFetched: false, includeExpired: value }, this.fetchApiKeys);
};
onToggleAdding = () => { onToggleAdding = () => {
this.setState({ isAdding: !this.state.isAdding }); this.setState({ isAdding: !this.state.isAdding });
}; };
...@@ -114,7 +119,7 @@ export class ApiKeysPage extends PureComponent<Props, any> { ...@@ -114,7 +119,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
// make sure that secondsToLive is number or null // make sure that secondsToLive is number or null
const secondsToLive = this.state.newApiKey['secondsToLive']; const secondsToLive = this.state.newApiKey['secondsToLive'];
this.state.newApiKey['secondsToLive'] = secondsToLive ? kbn.interval_to_seconds(secondsToLive) : null; this.state.newApiKey['secondsToLive'] = secondsToLive ? kbn.interval_to_seconds(secondsToLive) : null;
this.props.addApiKey(this.state.newApiKey, openModal); this.props.addApiKey(this.state.newApiKey, openModal, this.props.includeExpired);
this.setState((prevState: State) => { this.setState((prevState: State) => {
return { return {
...prevState, ...prevState,
...@@ -232,7 +237,7 @@ export class ApiKeysPage extends PureComponent<Props, any> { ...@@ -232,7 +237,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
renderApiKeyList() { renderApiKeyList() {
const { isAdding } = this.state; const { isAdding } = this.state;
const { apiKeys, searchQuery } = this.props; const { apiKeys, searchQuery, includeExpired } = this.props;
return ( return (
<> <>
...@@ -256,6 +261,14 @@ export class ApiKeysPage extends PureComponent<Props, any> { ...@@ -256,6 +261,14 @@ export class ApiKeysPage extends PureComponent<Props, any> {
{this.renderAddApiKeyForm()} {this.renderAddApiKeyForm()}
<h3 className="page-heading">Existing Keys</h3> <h3 className="page-heading">Existing Keys</h3>
<Switch
label="Show expired"
checked={includeExpired}
onChange={event => {
// @ts-ignore
this.onIncludeExpiredChange(event.target.checked);
}}
/>
<table className="filter-table"> <table className="filter-table">
<thead> <thead>
<tr> <tr>
...@@ -304,6 +317,7 @@ function mapStateToProps(state: any) { ...@@ -304,6 +317,7 @@ function mapStateToProps(state: any) {
navModel: getNavModel(state.navIndex, 'apikeys'), navModel: getNavModel(state.navIndex, 'apikeys'),
apiKeys: getApiKeys(state.apiKeys), apiKeys: getApiKeys(state.apiKeys),
searchQuery: state.apiKeys.searchQuery, searchQuery: state.apiKeys.searchQuery,
includeExpired: state.includeExpired,
apiKeysCount: getApiKeysCount(state.apiKeys), apiKeysCount: getApiKeysCount(state.apiKeys),
hasFetched: state.apiKeys.hasFetched, hasFetched: state.apiKeys.hasFetched,
}; };
......
...@@ -26,27 +26,31 @@ const apiKeysLoaded = (apiKeys: ApiKey[]): LoadApiKeysAction => ({ ...@@ -26,27 +26,31 @@ const apiKeysLoaded = (apiKeys: ApiKey[]): LoadApiKeysAction => ({
payload: apiKeys, payload: apiKeys,
}); });
export function addApiKey(apiKey: ApiKey, openModal: (key: string) => void): ThunkResult<void> { export function addApiKey(
apiKey: ApiKey,
openModal: (key: string) => void,
includeExpired: boolean
): ThunkResult<void> {
return async dispatch => { return async dispatch => {
const result = await getBackendSrv().post('/api/auth/keys', apiKey); const result = await getBackendSrv().post('/api/auth/keys', apiKey);
dispatch(setSearchQuery('')); dispatch(setSearchQuery(''));
dispatch(loadApiKeys()); dispatch(loadApiKeys(includeExpired));
openModal(result.key); openModal(result.key);
}; };
} }
export function loadApiKeys(): ThunkResult<void> { export function loadApiKeys(includeExpired: boolean): ThunkResult<void> {
return async dispatch => { return async dispatch => {
const response = await getBackendSrv().get('/api/auth/keys'); const response = await getBackendSrv().get('/api/auth/keys?includeExpired=' + includeExpired);
dispatch(apiKeysLoaded(response)); dispatch(apiKeysLoaded(response));
}; };
} }
export function deleteApiKey(id: number): ThunkResult<void> { export function deleteApiKey(id: number, includeExpired: boolean): ThunkResult<void> {
return async dispatch => { return async dispatch => {
getBackendSrv() getBackendSrv()
.delete('/api/auth/keys/' + id) .delete('/api/auth/keys/' + id)
.then(dispatch(loadApiKeys())); .then(dispatch(loadApiKeys(includeExpired)));
}; };
} }
......
...@@ -5,6 +5,7 @@ export const initialApiKeysState: ApiKeysState = { ...@@ -5,6 +5,7 @@ export const initialApiKeysState: ApiKeysState = {
keys: [], keys: [],
searchQuery: '', searchQuery: '',
hasFetched: false, hasFetched: false,
includeExpired: false,
}; };
export const apiKeysReducer = (state = initialApiKeysState, action: Action): ApiKeysState => { export const apiKeysReducer = (state = initialApiKeysState, action: Action): ApiKeysState => {
......
...@@ -7,7 +7,7 @@ describe('API Keys selectors', () => { ...@@ -7,7 +7,7 @@ describe('API Keys selectors', () => {
const mockKeys = getMultipleMockKeys(5); const mockKeys = getMultipleMockKeys(5);
it('should return all keys if no search query', () => { it('should return all keys if no search query', () => {
const mockState: ApiKeysState = { keys: mockKeys, searchQuery: '', hasFetched: false }; const mockState: ApiKeysState = { keys: mockKeys, searchQuery: '', hasFetched: false, includeExpired: false };
const keys = getApiKeys(mockState); const keys = getApiKeys(mockState);
...@@ -15,7 +15,7 @@ describe('API Keys selectors', () => { ...@@ -15,7 +15,7 @@ describe('API Keys selectors', () => {
}); });
it('should filter keys if search query exists', () => { it('should filter keys if search query exists', () => {
const mockState: ApiKeysState = { keys: mockKeys, searchQuery: '5', hasFetched: false }; const mockState: ApiKeysState = { keys: mockKeys, searchQuery: '5', hasFetched: false, includeExpired: false };
const keys = getApiKeys(mockState); const keys = getApiKeys(mockState);
......
...@@ -18,4 +18,5 @@ export interface ApiKeysState { ...@@ -18,4 +18,5 @@ export interface ApiKeysState {
keys: ApiKey[]; keys: ApiKey[];
searchQuery: string; searchQuery: string;
hasFetched: boolean; hasFetched: boolean;
includeExpired: boolean;
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment