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",
Description: "Gives access to add and delete api keys.",
Group: "API Keys",
Permissions: []ac.Permission{
Permissions: ac.ConcatPermissions(apikeyReaderRole.Role.Permissions, []ac.Permission{
{
Action: ac.ActionAPIKeyCreate,
},
{
Action: ac.ActionAPIKeyRead,
Scope: ac.ScopeAPIKeysAll,
},
{
Action: ac.ActionAPIKeyDelete,
Scope: ac.ScopeAPIKeysAll,
},
},
}),
},
Grants: []string{string(models.ROLE_ADMIN)},
}

View File

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

View File

@ -15,6 +15,7 @@ interface Props {
show: boolean;
onClose: () => void;
onKeyAdded: (apiKey: NewApiKey) => void;
disabled: boolean;
}
function isValidInterval(value: string): boolean {
@ -40,7 +41,7 @@ const timeRangeValidationEvents: ValidationEvents = {
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';
export const ApiKeysForm: FC<Props> = ({ show, onClose, onKeyAdded }) => {
export const ApiKeysForm: FC<Props> = ({ show, onClose, onKeyAdded, disabled }) => {
const [name, setName] = useState<string>('');
const [role, setRole] = useState<OrgRole>(OrgRole.Viewer);
const [secondsToLive, setSecondsToLive] = useState<string>('');
@ -102,7 +103,7 @@ export const ApiKeysForm: FC<Props> = ({ show, onClose, onKeyAdded }) => {
</InlineField>
</div>
<div className="gf-form">
<Button>Add</Button>
<Button disabled={disabled}>Add</Button>
</div>
</div>
</form>

View File

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

View File

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

View File

@ -9,9 +9,11 @@ interface Props {
apiKeys: ApiKey[];
timeZone: TimeZone;
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 styles = getStyles(theme);
@ -25,7 +27,7 @@ export const ApiKeysTable: FC<Props> = ({ apiKeys, timeZone, onDelete }) => {
<th style={{ width: '34px' }} />
</tr>
</thead>
{apiKeys.length > 0 ? (
{canRead && apiKeys.length > 0 ? (
<tbody>
{apiKeys.map((key) => {
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>
<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>
</tr>
);

View File

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