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
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
Query Parameters:
- `includeExpired`: boolean. enable listing of expired keys. Optional.
**Example Response**:
```http
......
......@@ -10,7 +10,7 @@ import (
)
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 {
return Error(500, "Failed to list api keys", err)
......
......@@ -50,7 +50,7 @@ type DeleteApiKeyCommand struct {
type GetApiKeysQuery struct {
OrgId int64
IncludeInvalid bool
IncludeExpired bool
Result []*ApiKey
}
......
......@@ -19,7 +19,7 @@ func init() {
func GetApiKeys(query *models.GetApiKeysQuery) error {
sess := x.Limit(100, 0).Where("org_id=? and ( expires IS NULL or expires >= ?)",
query.OrgId, timeNow().Unix()).Asc("name")
if query.IncludeInvalid {
if query.IncludeExpired {
sess = x.Limit(100, 0).Where("org_id=?", query.OrgId).Asc("name")
}
......
......@@ -91,7 +91,7 @@ func TestApiKeyDataAccess(t *testing.T) {
// advance mocked getTime by 1s
timeNow()
query := models.GetApiKeysQuery{OrgId: 1, IncludeInvalid: false}
query := models.GetApiKeysQuery{OrgId: 1, IncludeExpired: false}
err = GetApiKeys(&query)
assert.Nil(t, err)
......@@ -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)
assert.Nil(t, err)
......
......@@ -23,6 +23,7 @@ const setup = (propOverrides?: object) => {
setSearchQuery: jest.fn(),
addApiKey: jest.fn(),
apiKeysCount: 0,
includeExpired: false,
};
Object.assign(props, propOverrides);
......@@ -63,7 +64,7 @@ describe('Life cycle', () => {
instance.componentDidMount();
expect(instance.props.loadApiKeys).toHaveBeenCalled();
expect(instance.props.loadApiKeys).toHaveBeenCalledWith(false);
});
});
......@@ -72,7 +73,7 @@ describe('Functions', () => {
it('should call delete team', () => {
const { instance } = setup();
instance.onDeleteApiKey(getMockKey());
expect(instance.props.deleteApiKey).toHaveBeenCalledWith(1);
expect(instance.props.deleteApiKey).toHaveBeenCalledWith(1, false);
});
});
......
......@@ -12,7 +12,7 @@ import ApiKeysAddedModal from './ApiKeysAddedModal';
import config from 'app/core/config';
import appEvents from 'app/core/app_events';
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 { FilterInput } from 'app/core/components/FilterInput/FilterInput';
import { store } from 'app/store/store';
......@@ -51,6 +51,7 @@ export interface Props {
setSearchQuery: typeof setSearchQuery;
addApiKey: typeof addApiKey;
apiKeysCount: number;
includeExpired: boolean;
}
export interface State {
......@@ -76,7 +77,7 @@ const tooltipText =
export class ApiKeysPage extends PureComponent<Props, any> {
constructor(props: Props) {
super(props);
this.state = { isAdding: false, newApiKey: initialApiKeyState };
this.state = { isAdding: false, newApiKey: initialApiKeyState, includeExpired: false };
}
componentDidMount() {
......@@ -84,17 +85,21 @@ export class ApiKeysPage extends PureComponent<Props, any> {
}
async fetchApiKeys() {
await this.props.loadApiKeys();
await this.props.loadApiKeys(this.state.includeExpired);
}
onDeleteApiKey(key: ApiKey) {
this.props.deleteApiKey(key.id);
this.props.deleteApiKey(key.id, this.props.includeExpired);
}
onSearchQueryChange = (value: string) => {
this.props.setSearchQuery(value);
};
onIncludeExpiredChange = (value: boolean) => {
this.setState({ hasFetched: false, includeExpired: value }, this.fetchApiKeys);
};
onToggleAdding = () => {
this.setState({ isAdding: !this.state.isAdding });
};
......@@ -114,7 +119,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
// make sure that secondsToLive is number or null
const secondsToLive = this.state.newApiKey['secondsToLive'];
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) => {
return {
...prevState,
......@@ -232,7 +237,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
renderApiKeyList() {
const { isAdding } = this.state;
const { apiKeys, searchQuery } = this.props;
const { apiKeys, searchQuery, includeExpired } = this.props;
return (
<>
......@@ -256,6 +261,14 @@ export class ApiKeysPage extends PureComponent<Props, any> {
{this.renderAddApiKeyForm()}
<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">
<thead>
<tr>
......@@ -304,6 +317,7 @@ function mapStateToProps(state: any) {
navModel: getNavModel(state.navIndex, 'apikeys'),
apiKeys: getApiKeys(state.apiKeys),
searchQuery: state.apiKeys.searchQuery,
includeExpired: state.includeExpired,
apiKeysCount: getApiKeysCount(state.apiKeys),
hasFetched: state.apiKeys.hasFetched,
};
......
......@@ -26,27 +26,31 @@ const apiKeysLoaded = (apiKeys: ApiKey[]): LoadApiKeysAction => ({
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 => {
const result = await getBackendSrv().post('/api/auth/keys', apiKey);
dispatch(setSearchQuery(''));
dispatch(loadApiKeys());
dispatch(loadApiKeys(includeExpired));
openModal(result.key);
};
}
export function loadApiKeys(): ThunkResult<void> {
export function loadApiKeys(includeExpired: boolean): ThunkResult<void> {
return async dispatch => {
const response = await getBackendSrv().get('/api/auth/keys');
const response = await getBackendSrv().get('/api/auth/keys?includeExpired=' + includeExpired);
dispatch(apiKeysLoaded(response));
};
}
export function deleteApiKey(id: number): ThunkResult<void> {
export function deleteApiKey(id: number, includeExpired: boolean): ThunkResult<void> {
return async dispatch => {
getBackendSrv()
.delete('/api/auth/keys/' + id)
.then(dispatch(loadApiKeys()));
.then(dispatch(loadApiKeys(includeExpired)));
};
}
......
......@@ -5,6 +5,7 @@ export const initialApiKeysState: ApiKeysState = {
keys: [],
searchQuery: '',
hasFetched: false,
includeExpired: false,
};
export const apiKeysReducer = (state = initialApiKeysState, action: Action): ApiKeysState => {
......
......@@ -7,7 +7,7 @@ describe('API Keys selectors', () => {
const mockKeys = getMultipleMockKeys(5);
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);
......@@ -15,7 +15,7 @@ describe('API Keys selectors', () => {
});
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);
......
......@@ -18,4 +18,5 @@ export interface ApiKeysState {
keys: ApiKey[];
searchQuery: string;
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