Service accounts: RBAC the service account UI (#47788)

* WIP

* fix: bug for saving name did not remove edit

* refactor: better error msg

* Display the column Roles even when user can't see the role picker

* Remove spaces when building the search query request

* Disable Edit button and fix token addition and deletion

* Fix the error message text

Co-authored-by: Vardan Torosyan <vardants@gmail.com>
This commit is contained in:
Eric Leijonmarck 2022-04-14 23:06:08 +01:00 committed by GitHub
parent 2a178bd73c
commit b43e9b50b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 48 additions and 22 deletions

View File

@ -93,7 +93,7 @@ func (api *ServiceAccountsAPI) CreateServiceAccount(c *models.ReqContext) respon
serviceAccount, err := api.store.CreateServiceAccount(c.Req.Context(), c.OrgId, cmd.Name)
switch {
case errors.Is(err, &database.ErrSAInvalidName{}):
return response.Error(http.StatusBadRequest, "Invalid service account name", err)
return response.Error(http.StatusBadRequest, "Failed due to %s", err)
case err != nil:
return response.Error(http.StatusInternalServerError, "Failed to create service account", err)
}

View File

@ -17,14 +17,14 @@ func (e *ErrSAInvalidName) Unwrap() error {
return models.ErrUserAlreadyExists
}
type ErrMisingSAToken struct {
type ErrMissingSAToken struct {
}
func (e *ErrMisingSAToken) Error() string {
func (e *ErrMissingSAToken) Error() string {
return "service account token not found"
}
func (e *ErrMisingSAToken) Unwrap() error {
func (e *ErrMissingSAToken) Unwrap() error {
return models.ErrApiKeyNotFound
}
@ -44,7 +44,7 @@ type ErrDuplicateSAToken struct {
}
func (e *ErrDuplicateSAToken) Error() string {
return fmt.Sprintf("service account token %s already exists", e.name)
return fmt.Sprintf("service account token %s already exists in the organization", e.name)
}
func (e *ErrDuplicateSAToken) Unwrap() error {

View File

@ -57,7 +57,7 @@ func (s *ServiceAccountsStoreImpl) DeleteServiceAccountToken(ctx context.Context
if err != nil {
return err
} else if n == 0 {
return &ErrMisingSAToken{}
return &ErrMissingSAToken{}
}
return nil
})

View File

@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux';
import { getNavModel } from 'app/core/selectors/navModel';
import Page from 'app/core/components/Page/Page';
import { ServiceAccountProfile } from './ServiceAccountProfile';
import { StoreState, ServiceAccountDTO, ApiKey, Role } from 'app/types';
import { StoreState, ServiceAccountDTO, ApiKey, Role, AccessControlAction } from 'app/types';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import {
deleteServiceAccountToken,
@ -114,12 +114,24 @@ const ServiceAccountPageUnconnected = ({
<h3 className="page-heading" style={{ marginBottom: '0px' }}>
Tokens
</h3>
<Button onClick={() => setIsModalOpen(true)}>Add token</Button>
<Button
onClick={() => setIsModalOpen(true)}
disabled={!contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite)}
>
Add token
</Button>
</div>
{tokens && (
<ServiceAccountTokensTable tokens={tokens} timeZone={timezone} onDelete={onDeleteServiceAccountToken} />
)}
<CreateTokenModal isOpen={isModalOpen} token={newToken} onCreateToken={onCreateToken} onClose={onModalClose} />
{contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite) && (
<CreateTokenModal
isOpen={isModalOpen}
token={newToken}
onCreateToken={onCreateToken}
onClose={onModalClose}
/>
)}
</Page.Contents>
</Page>
);

View File

@ -1,9 +1,10 @@
import React, { PureComponent, useRef, useState } from 'react';
import { Role, ServiceAccountDTO } from 'app/types';
import { Role, ServiceAccountDTO, AccessControlAction } from 'app/types';
import { css, cx } from '@emotion/css';
import { dateTimeFormat, GrafanaTheme2, OrgRole, TimeZone } from '@grafana/data';
import { Button, ConfirmButton, ConfirmModal, Input, LegacyInputStatus, useStyles2 } from '@grafana/ui';
import { ServiceAccountRoleRow } from './ServiceAccountRoleRow';
import { contextSrv } from 'app/core/core';
interface Props {
serviceAccount: ServiceAccountDTO;
@ -26,6 +27,8 @@ export function ServiceAccountProfile({
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showDisableModal, setShowDisableModal] = useState(false);
const ableToWrite = contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite);
const deleteServiceAccountRef = useRef<HTMLButtonElement | null>(null);
const showDeleteServiceAccountModal = (show: boolean) => () => {
setShowDeleteModal(show);
@ -83,9 +86,10 @@ export function ServiceAccountProfile({
<table className="filter-table form-inline">
<tbody>
<ServiceAccountProfileRow
label="Display Name"
label="Name"
value={serviceAccount.name}
onChange={onServiceAccountNameChange}
disabled={!ableToWrite}
/>
<ServiceAccountProfileRow label="ID" value={serviceAccount.login} />
<ServiceAccountRoleRow
@ -110,6 +114,7 @@ export function ServiceAccountProfile({
variant="destructive"
onClick={showDeleteServiceAccountModal(true)}
ref={deleteServiceAccountRef}
disabled={!contextSrv.hasPermission(AccessControlAction.ServiceAccountsDelete)}
>
Delete service account
</Button>
@ -123,7 +128,7 @@ export function ServiceAccountProfile({
/>
</>
{serviceAccount.isDisabled ? (
<Button type={'button'} variant="secondary" onClick={handleServiceAccountEnable}>
<Button type={'button'} variant="secondary" onClick={handleServiceAccountEnable} disabled={!ableToWrite}>
Enable service account
</Button>
) : (
@ -133,6 +138,7 @@ export function ServiceAccountProfile({
variant="secondary"
onClick={showDisableServiceAccountModal(true)}
ref={disableServiceAccountRef}
disabled={!ableToWrite}
>
Disable service account
</Button>
@ -168,6 +174,7 @@ interface ServiceAccountProfileRowProps {
value?: string;
inputType?: string;
onChange?: (value: string) => void;
disabled?: boolean;
}
interface ServiceAccountProfileRowState {
@ -226,6 +233,7 @@ export class ServiceAccountProfileRow extends PureComponent<
};
onSave = () => {
this.setState({ editing: false });
if (this.props.onChange) {
this.props.onChange(this.state.value);
}
@ -248,7 +256,7 @@ export class ServiceAccountProfileRow extends PureComponent<
<label htmlFor={inputId}>{label}</label>
</td>
<td className="width-25" colSpan={2}>
{this.state.editing ? (
{!this.props.disabled && this.state.editing ? (
<Input
id={inputId}
type={inputType}
@ -265,10 +273,12 @@ export class ServiceAccountProfileRow extends PureComponent<
<td>
{this.props.onChange && (
<ConfirmButton
closeOnConfirm
confirmText="Save"
onClick={this.onEditClick}
onConfirm={this.onSave}
onClick={this.onEditClick}
onCancel={this.onCancelClick}
disabled={this.props.disabled}
>
Edit
</ConfirmButton>

View File

@ -2,8 +2,10 @@ import React, { FC } from 'react';
import { DeleteButton, Icon, Tooltip, useStyles2, useTheme2 } from '@grafana/ui';
import { dateTimeFormat, GrafanaTheme2, TimeZone } from '@grafana/data';
import { AccessControlAction } from 'app/types';
import { ApiKey } from '../../types';
import { css } from '@emotion/css';
import { contextSrv } from 'app/core/core';
interface Props {
tokens: ApiKey[];
@ -35,9 +37,11 @@ export const ServiceAccountTokensTable: FC<Props> = ({ tokens, timeZone, onDelet
<TokenExpiration timeZone={timeZone} token={key} />
</td>
<td>{formatDate(timeZone, key.created)}</td>
<td>
<DeleteButton aria-label="Delete API key" size="sm" onConfirm={() => onDelete(key)} />
</td>
{contextSrv.hasPermission(AccessControlAction.ServiceAccountsDelete) && (
<td>
<DeleteButton aria-label="Delete service account token" size="sm" onConfirm={() => onDelete(key)} />
</td>
)}
</tr>
);
})}
@ -82,7 +86,7 @@ const TokenExpiration = ({ timeZone, token }: TokenExpirationProps) => {
<span className={styles.hasExpired}>
Expired
<span className={styles.tooltipContainer}>
<Tooltip content="This API key has expired.">
<Tooltip content="This token has expired">
<Icon name="exclamation-triangle" className={styles.toolTipIcon} />
</Tooltip>
</span>

View File

@ -65,8 +65,8 @@ const ServiceAccountListItem = memo(
</a>
</td>
{contextSrv.licensedAccessControlEnabled() ? (
displayRolePicker && (
<td className={cx('link-td', styles.iconRow)}>
<td className={cx('link-td', styles.iconRow)}>
{displayRolePicker && (
<UserRolePicker
userId={serviceAccount.id}
orgId={serviceAccount.orgId}
@ -76,8 +76,8 @@ const ServiceAccountListItem = memo(
builtInRoles={builtInRoles}
disabled={!enableRolePicker}
/>
</td>
)
)}
</td>
) : (
<td className={cx('link-td', styles.iconRow)}>
<OrgRolePicker