mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Auth: Allow expiration of API keys (#17678)
* Modify backend to allow expiration of API Keys * Add middleware test for expired api keys * Modify frontend to enable expiration of API Keys * Fix frontend tests * Fix migration and add index for `expires` field * Add api key tests for database access * Substitude time.Now() by a mock for test usage * Front-end modifications * Change input label to `Time to live` * Change input behavior to comply with the other similar * Add tooltip * Modify AddApiKey api call response Expiration should be *time.Time instead of string * Present expiration date in the selected timezone * Use kbn for transforming intervals to seconds * Use `assert` library for tests * Frontend fixes Add checks for empty/undefined/null values * Change expires column from datetime to integer * Restrict api key duration input It should be interval not number * AddApiKey must complain if SecondsToLive is negative * Declare ErrInvalidApiKeyExpiration * Move configuration to auth section * Update docs * Eliminate alias for models in modified files * Omit expiration from api response if empty * Eliminate Goconvey from test file * Fix test Do not sleep, use mocked timeNow() instead * Remove index for expires from api_key table The index should be anyway on both org_id and expires fields. However this commit eliminates completely the index for now since not many rows are expected to be in this table. * Use getTimeZone function * Minor change in api key listing The frontend should display a message instead of empty string if the key does not expire.
This commit is contained in:
committed by
GitHub
parent
19185bd0af
commit
dc9ec7dc91
@@ -12,9 +12,34 @@ 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, Input } from '@grafana/ui';
|
||||
import { DeleteButton, EventsWithValidation, FormLabel, Input, ValidationEvents } from '@grafana/ui';
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||
import { store } from 'app/store/store';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
|
||||
// Utils
|
||||
import { dateTime, isDateTime } from '@grafana/ui/src/utils/moment_wrapper';
|
||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||
|
||||
const timeRangeValidationEvents: ValidationEvents = {
|
||||
[EventsWithValidation.onBlur]: [
|
||||
{
|
||||
rule: value => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
kbn.interval_to_seconds(value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
errorMessage: 'Not a valid duration',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
@@ -36,13 +61,18 @@ export interface State {
|
||||
enum ApiKeyStateProps {
|
||||
Name = 'name',
|
||||
Role = 'role',
|
||||
SecondsToLive = 'secondsToLive',
|
||||
}
|
||||
|
||||
const initialApiKeyState = {
|
||||
name: '',
|
||||
role: OrgRole.Viewer,
|
||||
secondsToLive: '',
|
||||
};
|
||||
|
||||
const tooltipText =
|
||||
'The api key life duration. For example 1d if your key is going to last for one day. All the supported units are: s,m,h,d,w,M,y';
|
||||
|
||||
export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -81,6 +111,9 @@ 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.setState((prevState: State) => {
|
||||
return {
|
||||
@@ -130,6 +163,17 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
);
|
||||
}
|
||||
|
||||
formatDate(date, format?) {
|
||||
if (!date) {
|
||||
return 'No expiration date';
|
||||
}
|
||||
date = isDateTime(date) ? date : dateTime(date);
|
||||
format = format || 'YYYY-MM-DD HH:mm:ss';
|
||||
const timezone = getTimeZone(store.getState().user);
|
||||
|
||||
return timezone.isUtc ? date.utc().format(format) : date.format(format);
|
||||
}
|
||||
|
||||
renderAddApiKeyForm() {
|
||||
const { newApiKey, isAdding } = this.state;
|
||||
|
||||
@@ -170,6 +214,17 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
<div className="gf-form max-width-21">
|
||||
<FormLabel tooltip={tooltipText}>Time to live</FormLabel>
|
||||
<Input
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
placeholder="1d"
|
||||
validationEvents={timeRangeValidationEvents}
|
||||
value={newApiKey.secondsToLive}
|
||||
onChange={evt => this.onApiKeyStateUpdate(evt, ApiKeyStateProps.SecondsToLive)}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form">
|
||||
<button className="btn gf-form-btn btn-primary">Add</button>
|
||||
</div>
|
||||
@@ -211,6 +266,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
<th>Expires</th>
|
||||
<th style={{ width: '34px' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -221,6 +277,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
<tr key={key.id}>
|
||||
<td>{key.name}</td>
|
||||
<td>{key.role}</td>
|
||||
<td>{this.formatDate(key.expiration)}</td>
|
||||
<td>
|
||||
<DeleteButton onConfirm={() => this.onDeleteApiKey(key)} />
|
||||
</td>
|
||||
|
||||
@@ -7,6 +7,8 @@ export const getMultipleMockKeys = (numberOfKeys: number): ApiKey[] => {
|
||||
id: i,
|
||||
name: `test-${i}`,
|
||||
role: OrgRole.Viewer,
|
||||
secondsToLive: null,
|
||||
expiration: '2019-06-04',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,5 +20,7 @@ export const getMockKey = (): ApiKey => {
|
||||
id: 1,
|
||||
name: 'test',
|
||||
role: OrgRole.Admin,
|
||||
secondsToLive: null,
|
||||
expiration: '2019-06-04',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -130,6 +130,32 @@ exports[`Render should render CTA if there are no API keys 1`] = `
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form max-width-21"
|
||||
>
|
||||
<Component
|
||||
tooltip="The api key life duration. For example 1d if your key is going to last for one day. All the supported units are: s,m,h,d,w,M,y"
|
||||
>
|
||||
Time to live
|
||||
</Component>
|
||||
<Input
|
||||
className="gf-form-input"
|
||||
onChange={[Function]}
|
||||
placeholder="1d"
|
||||
type="text"
|
||||
validationEvents={
|
||||
Object {
|
||||
"onBlur": Array [
|
||||
Object {
|
||||
"errorMessage": "Not a valid duration",
|
||||
"rule": [Function],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form"
|
||||
>
|
||||
|
||||
@@ -4,11 +4,14 @@ export interface ApiKey {
|
||||
id: number;
|
||||
name: string;
|
||||
role: OrgRole;
|
||||
secondsToLive: number;
|
||||
expiration: string;
|
||||
}
|
||||
|
||||
export interface NewApiKey {
|
||||
name: string;
|
||||
role: OrgRole;
|
||||
secondsToLive: number;
|
||||
}
|
||||
|
||||
export interface ApiKeysState {
|
||||
|
||||
Reference in New Issue
Block a user