Update API Keys UI to adjust based on users permissions (#47802)

* Update API Keys UI to adjust based on users permissions

Since API Keys support now RBAC we need to ensure that UI
is adjusted based on the user permissions.

* Applying PR suggestions
This commit is contained in:
Vardan Torosyan 2022-04-20 09:45:45 +02:00 committed by GitHub
parent 1588cd393a
commit cbd2d09d70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 49 additions and 21 deletions

View File

@ -174,19 +174,15 @@ func (hs *HTTPServer) declareFixedRoles() error {
DisplayName: "APIKeys writer", DisplayName: "APIKeys writer",
Description: "Gives access to add and delete api keys.", Description: "Gives access to add and delete api keys.",
Group: "API Keys", Group: "API Keys",
Permissions: []ac.Permission{ Permissions: ac.ConcatPermissions(apikeyReaderRole.Role.Permissions, []ac.Permission{
{ {
Action: ac.ActionAPIKeyCreate, Action: ac.ActionAPIKeyCreate,
}, },
{
Action: ac.ActionAPIKeyRead,
Scope: ac.ScopeAPIKeysAll,
},
{ {
Action: ac.ActionAPIKeyDelete, Action: ac.ActionAPIKeyDelete,
Scope: ac.ScopeAPIKeysAll, Scope: ac.ScopeAPIKeysAll,
}, },
}, }),
}, },
Grants: []string{string(models.ROLE_ADMIN)}, Grants: []string{string(models.ROLE_ADMIN)},
} }

View File

@ -31,11 +31,7 @@ func RegisterRoles(ac accesscontrol.AccessControl) error {
DisplayName: "Service accounts writer", DisplayName: "Service accounts writer",
Description: "Create, delete, read, or query service accounts.", Description: "Create, delete, read, or query service accounts.",
Group: "Service accounts", Group: "Service accounts",
Permissions: []accesscontrol.Permission{ Permissions: accesscontrol.ConcatPermissions(saReader.Role.Permissions, []accesscontrol.Permission{
{
Action: serviceaccounts.ActionRead,
Scope: serviceaccounts.ScopeAll,
},
{ {
Action: serviceaccounts.ActionWrite, Action: serviceaccounts.ActionWrite,
Scope: serviceaccounts.ScopeAll, Scope: serviceaccounts.ScopeAll,
@ -47,7 +43,7 @@ func RegisterRoles(ac accesscontrol.AccessControl) error {
Action: serviceaccounts.ActionDelete, Action: serviceaccounts.ActionDelete,
Scope: serviceaccounts.ScopeAll, Scope: serviceaccounts.ScopeAll,
}, },
}, }),
}, },
Grants: []string{string(models.ROLE_ADMIN)}, Grants: []string{string(models.ROLE_ADMIN)},
} }

View File

@ -15,6 +15,7 @@ interface Props {
show: boolean; show: boolean;
onClose: () => void; onClose: () => void;
onKeyAdded: (apiKey: NewApiKey) => void; onKeyAdded: (apiKey: NewApiKey) => void;
disabled: boolean;
} }
function isValidInterval(value: string): boolean { function isValidInterval(value: string): boolean {
@ -40,7 +41,7 @@ const timeRangeValidationEvents: ValidationEvents = {
const tooltipText = const tooltipText =
'The API key life duration. For example, 1d if your key is going to last for one day. Supported units are: s,m,h,d,w,M,y'; 'The API key life duration. For example, 1d if your key is going to last for one day. Supported units are: s,m,h,d,w,M,y';
export const ApiKeysForm: FC<Props> = ({ show, onClose, onKeyAdded }) => { export const ApiKeysForm: FC<Props> = ({ show, onClose, onKeyAdded, disabled }) => {
const [name, setName] = useState<string>(''); const [name, setName] = useState<string>('');
const [role, setRole] = useState<OrgRole>(OrgRole.Viewer); const [role, setRole] = useState<OrgRole>(OrgRole.Viewer);
const [secondsToLive, setSecondsToLive] = useState<string>(''); const [secondsToLive, setSecondsToLive] = useState<string>('');
@ -102,7 +103,7 @@ export const ApiKeysForm: FC<Props> = ({ show, onClose, onKeyAdded }) => {
</InlineField> </InlineField>
</div> </div>
<div className="gf-form"> <div className="gf-form">
<Button>Add</Button> <Button disabled={disabled}>Add</Button>
</div> </div>
</div> </div>
</form> </form>

View File

@ -37,6 +37,9 @@ const setup = (propOverrides: Partial<Props>) => {
includeExpired: false, includeExpired: false,
includeExpiredDisabled: false, includeExpiredDisabled: false,
toggleIncludeExpired: toggleIncludeExpiredMock, toggleIncludeExpired: toggleIncludeExpiredMock,
canRead: true,
canCreate: true,
canDelete: true,
}; };
Object.assign(props, propOverrides); Object.assign(props, propOverrides);

View File

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux'; import { connect, ConnectedProps } from 'react-redux';
// Utils // Utils
import { ApiKey, NewApiKey, StoreState } from 'app/types'; import { AccessControlAction, ApiKey, NewApiKey, StoreState } from 'app/types';
import { getNavModel } from 'app/core/selectors/navModel'; import { getNavModel } from 'app/core/selectors/navModel';
import { getApiKeys, getApiKeysCount, getIncludeExpired, getIncludeExpiredDisabled } from './state/selectors'; import { getApiKeys, getApiKeysCount, getIncludeExpired, getIncludeExpiredDisabled } from './state/selectors';
import { addApiKey, deleteApiKey, loadApiKeys, toggleIncludeExpired } from './state/actions'; import { addApiKey, deleteApiKey, loadApiKeys, toggleIncludeExpired } from './state/actions';
@ -19,8 +19,13 @@ import { ApiKeysActionBar } from './ApiKeysActionBar';
import { ApiKeysTable } from './ApiKeysTable'; import { ApiKeysTable } from './ApiKeysTable';
import { ApiKeysController } from './ApiKeysController'; import { ApiKeysController } from './ApiKeysController';
import { ShowModalReactEvent } from 'app/types/events'; import { ShowModalReactEvent } from 'app/types/events';
import { contextSrv } from 'app/core/core';
function mapStateToProps(state: StoreState) { function mapStateToProps(state: StoreState) {
const canRead = contextSrv.hasAccess(AccessControlAction.ActionAPIKeysRead, true);
const canCreate = contextSrv.hasAccess(AccessControlAction.ActionAPIKeysCreate, true);
const canDelete = contextSrv.hasAccess(AccessControlAction.ActionAPIKeysDelete, true);
return { return {
navModel: getNavModel(state.navIndex, 'apikeys'), navModel: getNavModel(state.navIndex, 'apikeys'),
apiKeys: getApiKeys(state.apiKeys), apiKeys: getApiKeys(state.apiKeys),
@ -30,6 +35,9 @@ function mapStateToProps(state: StoreState) {
timeZone: getTimeZone(state.user), timeZone: getTimeZone(state.user),
includeExpired: getIncludeExpired(state.apiKeys), includeExpired: getIncludeExpired(state.apiKeys),
includeExpiredDisabled: getIncludeExpiredDisabled(state.apiKeys), includeExpiredDisabled: getIncludeExpiredDisabled(state.apiKeys),
canRead: canRead,
canCreate: canCreate,
canDelete: canDelete,
}; };
} }
@ -120,6 +128,9 @@ export class ApiKeysPageUnconnected extends PureComponent<Props, State> {
timeZone, timeZone,
includeExpired, includeExpired,
includeExpiredDisabled, includeExpiredDisabled,
canRead,
canCreate,
canDelete,
} = this.props; } = this.props;
if (!hasFetched) { if (!hasFetched) {
@ -146,23 +157,35 @@ export class ApiKeysPageUnconnected extends PureComponent<Props, State> {
onClick={toggleIsAdding} onClick={toggleIsAdding}
buttonTitle="New API key" buttonTitle="New API key"
proTip="Remember, you can provide view-only API access to other applications." proTip="Remember, you can provide view-only API access to other applications."
buttonDisabled={!canCreate}
/> />
) : null} ) : null}
{showTable ? ( {showTable ? (
<ApiKeysActionBar <ApiKeysActionBar
searchQuery={searchQuery} searchQuery={searchQuery}
disabled={isAdding} disabled={isAdding || !canCreate}
onAddClick={toggleIsAdding} onAddClick={toggleIsAdding}
onSearchChange={this.onSearchQueryChange} onSearchChange={this.onSearchQueryChange}
/> />
) : null} ) : null}
<ApiKeysForm show={isAdding} onClose={toggleIsAdding} onKeyAdded={this.onAddApiKey} /> <ApiKeysForm
show={isAdding}
onClose={toggleIsAdding}
onKeyAdded={this.onAddApiKey}
disabled={!canCreate}
/>
{showTable ? ( {showTable ? (
<VerticalGroup> <VerticalGroup>
<InlineField disabled={includeExpiredDisabled} label="Include expired keys"> <InlineField disabled={includeExpiredDisabled} label="Include expired keys">
<InlineSwitch id="showExpired" value={includeExpired} onChange={this.onIncludeExpiredChange} /> <InlineSwitch id="showExpired" value={includeExpired} onChange={this.onIncludeExpiredChange} />
</InlineField> </InlineField>
<ApiKeysTable apiKeys={apiKeys} timeZone={timeZone} onDelete={this.onDeleteApiKey} /> <ApiKeysTable
apiKeys={apiKeys}
timeZone={timeZone}
onDelete={this.onDeleteApiKey}
canRead={canRead}
canDelete={canDelete}
/>
</VerticalGroup> </VerticalGroup>
) : null} ) : null}
</> </>

View File

@ -9,9 +9,11 @@ interface Props {
apiKeys: ApiKey[]; apiKeys: ApiKey[];
timeZone: TimeZone; timeZone: TimeZone;
onDelete: (apiKey: ApiKey) => void; onDelete: (apiKey: ApiKey) => void;
canRead: boolean;
canDelete: boolean;
} }
export const ApiKeysTable: FC<Props> = ({ apiKeys, timeZone, onDelete }) => { export const ApiKeysTable: FC<Props> = ({ apiKeys, timeZone, onDelete, canRead, canDelete }) => {
const theme = useTheme2(); const theme = useTheme2();
const styles = getStyles(theme); const styles = getStyles(theme);
@ -25,7 +27,7 @@ export const ApiKeysTable: FC<Props> = ({ apiKeys, timeZone, onDelete }) => {
<th style={{ width: '34px' }} /> <th style={{ width: '34px' }} />
</tr> </tr>
</thead> </thead>
{apiKeys.length > 0 ? ( {canRead && apiKeys.length > 0 ? (
<tbody> <tbody>
{apiKeys.map((key) => { {apiKeys.map((key) => {
const isExpired = Boolean(key.expiration && Date.now() > new Date(key.expiration).getTime()); const isExpired = Boolean(key.expiration && Date.now() > new Date(key.expiration).getTime());
@ -44,7 +46,12 @@ export const ApiKeysTable: FC<Props> = ({ apiKeys, timeZone, onDelete }) => {
)} )}
</td> </td>
<td> <td>
<DeleteButton aria-label="Delete API key" size="sm" onConfirm={() => onDelete(key)} /> <DeleteButton
aria-label="Delete API key"
size="sm"
onConfirm={() => onDelete(key)}
disabled={!canDelete}
/>
</td> </td>
</tr> </tr>
); );

View File

@ -110,6 +110,8 @@ export enum AccessControlAction {
AlertingNotificationsExternalRead = 'alert.notifications.external:read', AlertingNotificationsExternalRead = 'alert.notifications.external:read',
ActionAPIKeysRead = 'apikeys:read', ActionAPIKeysRead = 'apikeys:read',
ActionAPIKeysCreate = 'apikeys:create',
ActionAPIKeysDelete = 'apikeys:delete',
} }
export interface Role { export interface Role {