From b43e9b50b497f315f31fb35a006d820871d88dda Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Thu, 14 Apr 2022 23:06:08 +0100 Subject: [PATCH] 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 --- pkg/services/serviceaccounts/api/api.go | 2 +- .../serviceaccounts/database/errors.go | 8 ++++---- .../serviceaccounts/database/token_store.go | 2 +- .../serviceaccounts/ServiceAccountPage.tsx | 18 ++++++++++++++--- .../serviceaccounts/ServiceAccountProfile.tsx | 20 ++++++++++++++----- .../ServiceAccountTokensTable.tsx | 12 +++++++---- .../ServiceAccountsListItem.tsx | 8 ++++---- 7 files changed, 48 insertions(+), 22 deletions(-) diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index d4da2814925..5acc84f090c 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -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) } diff --git a/pkg/services/serviceaccounts/database/errors.go b/pkg/services/serviceaccounts/database/errors.go index 3cd555480a6..8001b2bd889 100644 --- a/pkg/services/serviceaccounts/database/errors.go +++ b/pkg/services/serviceaccounts/database/errors.go @@ -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 { diff --git a/pkg/services/serviceaccounts/database/token_store.go b/pkg/services/serviceaccounts/database/token_store.go index 320bc2f1ed5..4a4b04f9585 100644 --- a/pkg/services/serviceaccounts/database/token_store.go +++ b/pkg/services/serviceaccounts/database/token_store.go @@ -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 }) diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.tsx index 66e59e0cc83..061e7a0d309 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.tsx @@ -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 = ({

Tokens

- + {tokens && ( )} - + {contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite) && ( + + )} ); diff --git a/public/app/features/serviceaccounts/ServiceAccountProfile.tsx b/public/app/features/serviceaccounts/ServiceAccountProfile.tsx index 408d4346d25..368134f9077 100644 --- a/public/app/features/serviceaccounts/ServiceAccountProfile.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountProfile.tsx @@ -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(null); const showDeleteServiceAccountModal = (show: boolean) => () => { setShowDeleteModal(show); @@ -83,9 +86,10 @@ export function ServiceAccountProfile({ Delete service account @@ -123,7 +128,7 @@ export function ServiceAccountProfile({ /> {serviceAccount.isDisabled ? ( - ) : ( @@ -133,6 +138,7 @@ export function ServiceAccountProfile({ variant="secondary" onClick={showDisableServiceAccountModal(true)} ref={disableServiceAccountRef} + disabled={!ableToWrite} > Disable service account @@ -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< - + {contextSrv.hasPermission(AccessControlAction.ServiceAccountsDelete) && ( + + )} ); })} @@ -82,7 +86,7 @@ const TokenExpiration = ({ timeZone, token }: TokenExpirationProps) => { Expired - + diff --git a/public/app/features/serviceaccounts/ServiceAccountsListItem.tsx b/public/app/features/serviceaccounts/ServiceAccountsListItem.tsx index d0de3b2e9e9..b2d2d3aabb8 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListItem.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListItem.tsx @@ -65,8 +65,8 @@ const ServiceAccountListItem = memo( {contextSrv.licensedAccessControlEnabled() ? ( - displayRolePicker && ( - - ) + )} + ) : (
- {this.state.editing ? ( + {!this.props.disabled && this.state.editing ? ( {this.props.onChange && ( Edit diff --git a/public/app/features/serviceaccounts/ServiceAccountTokensTable.tsx b/public/app/features/serviceaccounts/ServiceAccountTokensTable.tsx index be500c1f26e..e3c517bdef3 100644 --- a/public/app/features/serviceaccounts/ServiceAccountTokensTable.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountTokensTable.tsx @@ -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 = ({ tokens, timeZone, onDelet {formatDate(timeZone, key.created)} - onDelete(key)} /> - + onDelete(key)} /> +
+ + {displayRolePicker && ( -