mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
1588cd393a
commit
cbd2d09d70
@ -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)},
|
||||||
}
|
}
|
||||||
|
@ -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)},
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
@ -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}
|
||||||
</>
|
</>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user