mirror of
https://github.com/grafana/grafana.git
synced 2026-09-05 04:40:13 -05:00
Service Accounts: Managed permissions for service accounts (#51818)
* backend changes * frontend changes * linting * nit * import order * allow SA creator to access the SA page * fix merge * tests * fix frontend tests Co-authored-by: alexanderzobnin alexanderzobnin@gmail.com
This commit is contained in:
co-authored by
alexanderzobnin alexanderzobnin@gmail.com
parent
2af5feb147
commit
d85df0a560
@@ -13,7 +13,11 @@ import { OrgRolePicker } from '../admin/OrgRolePicker';
|
||||
|
||||
export interface Props {}
|
||||
|
||||
const createServiceAccount = async (sa: ServiceAccountDTO) => getBackendSrv().post('/api/serviceaccounts/', sa);
|
||||
const createServiceAccount = async (sa: ServiceAccountDTO) => {
|
||||
const result = await getBackendSrv().post('/api/serviceaccounts/', sa);
|
||||
await contextSrv.fetchUserPermissions();
|
||||
return result;
|
||||
};
|
||||
|
||||
const updateServiceAccount = async (id: number, sa: ServiceAccountDTO) =>
|
||||
getBackendSrv().patch(`/api/serviceaccounts/${id}`, sa);
|
||||
@@ -44,7 +48,10 @@ export const ServiceAccountCreatePage = ({}: Props): JSX.Element => {
|
||||
setRoleOptions(options);
|
||||
}
|
||||
|
||||
if (contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList)) {
|
||||
if (
|
||||
contextSrv.accessControlBuiltInRoleAssignmentEnabled() &&
|
||||
contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList)
|
||||
) {
|
||||
const builtInRoles = await fetchBuiltinRoles(currentOrgId);
|
||||
setBuiltinRoles(builtInRoles);
|
||||
}
|
||||
@@ -75,7 +82,11 @@ export const ServiceAccountCreatePage = ({}: Props): JSX.Element => {
|
||||
tokens: response.tokens,
|
||||
};
|
||||
await updateServiceAccount(response.id, data);
|
||||
if (contextSrv.licensedAccessControlEnabled()) {
|
||||
if (
|
||||
contextSrv.licensedAccessControlEnabled() &&
|
||||
contextSrv.hasPermission(AccessControlAction.ActionUserRolesAdd) &&
|
||||
contextSrv.hasPermission(AccessControlAction.ActionUserRolesRemove)
|
||||
) {
|
||||
await updateUserRoles(pendingRoles, newAccount.id, newAccount.orgId);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -11,6 +11,7 @@ jest.mock('app/core/core', () => ({
|
||||
licensedAccessControlEnabled: () => false,
|
||||
hasPermission: () => true,
|
||||
hasPermissionInMetadata: () => true,
|
||||
hasAccessInMetadata: () => false,
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { contextSrv } from 'app/core/core';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
import { AccessControlAction, ApiKey, Role, ServiceAccountDTO, StoreState } from 'app/types';
|
||||
|
||||
import { ServiceAccountPermissions } from './ServiceAccountPermissions';
|
||||
import { CreateTokenModal, ServiceAccountToken } from './components/CreateTokenModal';
|
||||
import { ServiceAccountProfile } from './components/ServiceAccountProfile';
|
||||
import { ServiceAccountTokensTable } from './components/ServiceAccountTokensTable';
|
||||
@@ -79,6 +80,11 @@ export const ServiceAccountPageUnconnected = ({
|
||||
!contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite) || serviceAccount.isDisabled;
|
||||
|
||||
const ableToWrite = contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite);
|
||||
const canReadPermissions = contextSrv.hasAccessInMetadata(
|
||||
AccessControlAction.ServiceAccountsPermissionsRead,
|
||||
serviceAccount!,
|
||||
false
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadServiceAccount(serviceAccountId);
|
||||
@@ -186,7 +192,7 @@ export const ServiceAccountPageUnconnected = ({
|
||||
/>
|
||||
)}
|
||||
<div className={styles.tokensListHeader}>
|
||||
<h4>Tokens</h4>
|
||||
<h3>Tokens</h3>
|
||||
<Button onClick={() => setIsTokenModalOpen(true)} disabled={tokenActionsDisabled}>
|
||||
Add service account token
|
||||
</Button>
|
||||
@@ -199,6 +205,7 @@ export const ServiceAccountPageUnconnected = ({
|
||||
tokenActionsDisabled={tokenActionsDisabled}
|
||||
/>
|
||||
)}
|
||||
{canReadPermissions && <ServiceAccountPermissions serviceAccount={serviceAccount} />}
|
||||
</div>
|
||||
<ConfirmModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Permissions } from 'app/core/components/AccessControl';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
|
||||
import { AccessControlAction, ServiceAccountDTO } from '../../types';
|
||||
|
||||
type ServiceAccountPermissionsProps = {
|
||||
serviceAccount: ServiceAccountDTO;
|
||||
};
|
||||
|
||||
export const ServiceAccountPermissions = (props: ServiceAccountPermissionsProps) => {
|
||||
const canSetPermissions = contextSrv.hasPermissionInMetadata(
|
||||
AccessControlAction.ServiceAccountsPermissionsWrite,
|
||||
props.serviceAccount
|
||||
);
|
||||
|
||||
return (
|
||||
<Permissions
|
||||
title="Permissions"
|
||||
addPermissionTitle="Add permission"
|
||||
buttonLabel="Add permission"
|
||||
resource="serviceaccounts"
|
||||
resourceId={props.serviceAccount.id}
|
||||
canSetPermissions={canSetPermissions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -230,23 +230,22 @@ export const ServiceAccountsListPageUnconnected = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
<>
|
||||
<div className={cx(styles.table, 'admin-list-table')}>
|
||||
<table className="filter-table filter-table--hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Account</th>
|
||||
<th>ID</th>
|
||||
<th>Roles</th>
|
||||
<th>Tokens</th>
|
||||
<th style={{ width: '34px' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!isLoading &&
|
||||
serviceAccounts.length !== 0 &&
|
||||
serviceAccounts.map((serviceAccount: ServiceAccountDTO) => (
|
||||
{!isLoading && serviceAccounts.length !== 0 && (
|
||||
<>
|
||||
<div className={cx(styles.table, 'admin-list-table')}>
|
||||
<table className="filter-table filter-table--hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Account</th>
|
||||
<th>ID</th>
|
||||
<th>Roles</th>
|
||||
<th>Tokens</th>
|
||||
<th style={{ width: '34px' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{serviceAccounts.map((serviceAccount: ServiceAccountDTO) => (
|
||||
<ServiceAccountListItem
|
||||
serviceAccount={serviceAccount}
|
||||
key={serviceAccount.id}
|
||||
@@ -259,10 +258,11 @@ export const ServiceAccountsListPageUnconnected = ({
|
||||
onAddTokenClick={onTokenAdd}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{currentServiceAccount && (
|
||||
<>
|
||||
<ConfirmModal
|
||||
|
||||
@@ -37,7 +37,7 @@ export function ServiceAccountProfile({
|
||||
|
||||
return (
|
||||
<div className={styles.section}>
|
||||
<h4>Information</h4>
|
||||
<h3>Information</h3>
|
||||
<table className="filter-table">
|
||||
<tbody>
|
||||
<ServiceAccountProfileRow
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { dateTimeFormat, GrafanaTheme2, TimeZone } from '@grafana/data';
|
||||
@@ -17,7 +17,7 @@ export const ServiceAccountTokensTable = ({ tokens, timeZone, tokenActionsDisabl
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<table className="filter-table">
|
||||
<table className={cx(styles.section, 'filter-table')}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
@@ -124,4 +124,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
neverExpire: css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
`,
|
||||
section: css`
|
||||
margin-bottom: ${theme.spacing(4)};
|
||||
`,
|
||||
});
|
||||
|
||||
@@ -47,8 +47,10 @@ export function fetchACOptions(): ThunkResult<void> {
|
||||
|
||||
export function getApiKeysMigrationStatus(): ThunkResult<void> {
|
||||
return async (dispatch) => {
|
||||
const result = await getBackendSrv().get('/api/serviceaccounts/migrationstatus');
|
||||
dispatch(apiKeysMigrationStatusLoaded(!!result?.migrated));
|
||||
if (contextSrv.hasPermission(AccessControlAction.ServiceAccountsRead)) {
|
||||
const result = await getBackendSrv().get('/api/serviceaccounts/migrationstatus');
|
||||
dispatch(apiKeysMigrationStatusLoaded(!!result?.migrated));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,20 +63,22 @@ export function fetchServiceAccounts(
|
||||
): ThunkResult<void> {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
if (withLoadingIndicator) {
|
||||
dispatch(serviceAccountsFetchBegin());
|
||||
if (contextSrv.hasPermission(AccessControlAction.ServiceAccountsRead)) {
|
||||
if (withLoadingIndicator) {
|
||||
dispatch(serviceAccountsFetchBegin());
|
||||
}
|
||||
const { perPage, page, query, serviceAccountStateFilter } = getState().serviceAccounts;
|
||||
const result = await getBackendSrv().get(
|
||||
`/api/serviceaccounts/search?perpage=${perPage}&page=${page}&query=${query}${getStateFilter(
|
||||
serviceAccountStateFilter
|
||||
)}&accesscontrol=true`
|
||||
);
|
||||
dispatch(serviceAccountsFetched(result));
|
||||
}
|
||||
const { perPage, page, query, serviceAccountStateFilter } = getState().serviceAccounts;
|
||||
const result = await getBackendSrv().get(
|
||||
`/api/serviceaccounts/search?perpage=${perPage}&page=${page}&query=${query}${getStateFilter(
|
||||
serviceAccountStateFilter
|
||||
)}&accesscontrol=true`
|
||||
);
|
||||
dispatch(serviceAccountsFetched(result));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
serviceAccountsFetchEnd();
|
||||
dispatch(serviceAccountsFetchEnd());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user