mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
API: Optionally list expired keys (#20468)
* API: Optionally list expired keys * Update docs
This commit is contained in:
parent
1b38d94537
commit
d1c523838b
@ -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 (
|
|||||||
)
|
)
|
||||||
|
|
||||||
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 {
|
|||||||
|
|
||||||
type GetApiKeysQuery struct {
|
type GetApiKeysQuery struct {
|
||||||
OrgId int64
|
OrgId int64
|
||||||
IncludeInvalid bool
|
IncludeExpired bool
|
||||||
Result []*ApiKey
|
Result []*ApiKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
|||||||
// 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) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) => {
|
|||||||
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', () => {
|
|||||||
|
|
||||||
instance.componentDidMount();
|
instance.componentDidMount();
|
||||||
|
|
||||||
expect(instance.props.loadApiKeys).toHaveBeenCalled();
|
expect(instance.props.loadApiKeys).toHaveBeenCalledWith(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -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';
|
|||||||
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 {
|
|||||||
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 =
|
|||||||
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> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
|||||||
// 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> {
|
|||||||
|
|
||||||
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> {
|
|||||||
{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) {
|
|||||||
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 => ({
|
|||||||
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 = {
|
|||||||
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', () => {
|
|||||||
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', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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 {
|
|||||||
keys: ApiKey[];
|
keys: ApiKey[];
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
hasFetched: boolean;
|
hasFetched: boolean;
|
||||||
|
includeExpired: boolean;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user