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:
Sofia Papagiannaki
2019-06-26 09:47:03 +03:00
committed by GitHub
parent 19185bd0af
commit dc9ec7dc91
17 changed files with 432 additions and 154 deletions

View File

@@ -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>

View File

@@ -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',
};
};

View File

@@ -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"
>

View File

@@ -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 {