mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
IAM: Protect managed service account frontend details page (#77839)
* Add `isManaged` property to frontend model * Remove enabled and token buttons for managed SA * Replace trash icon for lock icon for managed SA * Block the role picker for managed SA * Filter SA list usiong the managed filter * Rename external for managed * Add only managed filter * Toggle the enable buttons for managed sa * Disable add token and delete token buttons * Remove the edit name button * Disable the Role picker for managed sa * Hide the permissions section * Add managed by row --------- Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com> Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>
This commit is contained in:
parent
e94f8234a1
commit
408dab8c57
@ -158,6 +158,8 @@ type ServiceAccountProfileDTO struct {
|
|||||||
Teams []string `json:"teams" xorm:"-"`
|
Teams []string `json:"teams" xorm:"-"`
|
||||||
// example: false
|
// example: false
|
||||||
IsExternal bool `json:"isExternal,omitempty" xorm:"-"`
|
IsExternal bool `json:"isExternal,omitempty" xorm:"-"`
|
||||||
|
// example: grafana-app
|
||||||
|
RequiredBy string `json:"requiredBy,omitempty" xorm:"-"`
|
||||||
|
|
||||||
Tokens int64 `json:"tokens,omitempty"`
|
Tokens int64 `json:"tokens,omitempty"`
|
||||||
AccessControl map[string]bool `json:"accessControl,omitempty" xorm:"-"`
|
AccessControl map[string]bool `json:"accessControl,omitempty" xorm:"-"`
|
||||||
|
@ -139,6 +139,7 @@ func (s *ServiceAccountsProxy) RetrieveServiceAccount(ctx context.Context, orgID
|
|||||||
|
|
||||||
if s.isProxyEnabled {
|
if s.isProxyEnabled {
|
||||||
sa.IsExternal = isExternalServiceAccount(sa.Login)
|
sa.IsExternal = isExternalServiceAccount(sa.Login)
|
||||||
|
sa.RequiredBy = strings.ReplaceAll(sa.Name, serviceaccounts.ExtSvcPrefix, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
return sa, nil
|
return sa, nil
|
||||||
|
@ -6771,6 +6771,10 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"example": false
|
"example": false
|
||||||
},
|
},
|
||||||
|
"requiredBy": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "grafana-app"
|
||||||
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "sa-grafana"
|
"example": "sa-grafana"
|
||||||
@ -6826,6 +6830,10 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"example": false
|
"example": false
|
||||||
},
|
},
|
||||||
|
"requiredBy": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "grafana-app"
|
||||||
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "sa-grafana"
|
"example": "sa-grafana"
|
||||||
|
@ -18724,6 +18724,10 @@
|
|||||||
"format": "int64",
|
"format": "int64",
|
||||||
"example": 1
|
"example": 1
|
||||||
},
|
},
|
||||||
|
"requiredBy": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "grafana-app"
|
||||||
|
},
|
||||||
"role": {
|
"role": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Editor"
|
"example": "Editor"
|
||||||
|
@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { connect, ConnectedProps } from 'react-redux';
|
import { connect, ConnectedProps } from 'react-redux';
|
||||||
|
|
||||||
import { getTimeZone, NavModelItem } from '@grafana/data';
|
import { getTimeZone, NavModelItem } from '@grafana/data';
|
||||||
import { Button, ConfirmModal, HorizontalGroup } from '@grafana/ui';
|
import { Button, ConfirmModal, HorizontalGroup, IconButton } from '@grafana/ui';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
@ -70,7 +70,9 @@ export const ServiceAccountPageUnconnected = ({
|
|||||||
|
|
||||||
const serviceAccountId = parseInt(match.params.id, 10);
|
const serviceAccountId = parseInt(match.params.id, 10);
|
||||||
const tokenActionsDisabled =
|
const tokenActionsDisabled =
|
||||||
!contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite) || serviceAccount.isDisabled;
|
serviceAccount.isDisabled ||
|
||||||
|
serviceAccount.isExternal ||
|
||||||
|
!contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite);
|
||||||
|
|
||||||
const ableToWrite = contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite);
|
const ableToWrite = contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite);
|
||||||
const canReadPermissions = contextSrv.hasPermissionInMetadata(
|
const canReadPermissions = contextSrv.hasPermissionInMetadata(
|
||||||
@ -134,7 +136,7 @@ export const ServiceAccountPageUnconnected = ({
|
|||||||
<Page navId="serviceaccounts" pageNav={pageNav}>
|
<Page navId="serviceaccounts" pageNav={pageNav}>
|
||||||
<Page.Contents isLoading={isLoading}>
|
<Page.Contents isLoading={isLoading}>
|
||||||
<div>
|
<div>
|
||||||
{serviceAccount && (
|
{serviceAccount && !serviceAccount.isExternal && (
|
||||||
<HorizontalGroup spacing="md" height="auto" justify="flex-end">
|
<HorizontalGroup spacing="md" height="auto" justify="flex-end">
|
||||||
<Button
|
<Button
|
||||||
type={'button'}
|
type={'button'}
|
||||||
@ -165,14 +167,26 @@ export const ServiceAccountPageUnconnected = ({
|
|||||||
)}
|
)}
|
||||||
</HorizontalGroup>
|
</HorizontalGroup>
|
||||||
)}
|
)}
|
||||||
|
{serviceAccount && serviceAccount.isExternal && (
|
||||||
|
<HorizontalGroup spacing="md" height="auto" justify="flex-end">
|
||||||
|
<IconButton
|
||||||
|
disabled={true}
|
||||||
|
name="lock"
|
||||||
|
size="md"
|
||||||
|
tooltip={`This is a managed service account and cannot be modified.`}
|
||||||
|
/>
|
||||||
|
</HorizontalGroup>
|
||||||
|
)}
|
||||||
{serviceAccount && (
|
{serviceAccount && (
|
||||||
<ServiceAccountProfile serviceAccount={serviceAccount} timeZone={timezone} onChange={onProfileChange} />
|
<ServiceAccountProfile serviceAccount={serviceAccount} timeZone={timezone} onChange={onProfileChange} />
|
||||||
)}
|
)}
|
||||||
<HorizontalGroup justify="space-between" height="auto">
|
<HorizontalGroup justify="space-between" height="auto">
|
||||||
<h3>Tokens</h3>
|
<h3>Tokens</h3>
|
||||||
|
{!serviceAccount.isExternal && (
|
||||||
<Button onClick={() => setIsTokenModalOpen(true)} disabled={tokenActionsDisabled}>
|
<Button onClick={() => setIsTokenModalOpen(true)} disabled={tokenActionsDisabled}>
|
||||||
Add service account token
|
Add service account token
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</HorizontalGroup>
|
</HorizontalGroup>
|
||||||
{tokens && (
|
{tokens && (
|
||||||
<ServiceAccountTokensTable
|
<ServiceAccountTokensTable
|
||||||
@ -182,7 +196,9 @@ export const ServiceAccountPageUnconnected = ({
|
|||||||
tokenActionsDisabled={tokenActionsDisabled}
|
tokenActionsDisabled={tokenActionsDisabled}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{canReadPermissions && <ServiceAccountPermissions serviceAccount={serviceAccount} />}
|
{!serviceAccount.isExternal && canReadPermissions && (
|
||||||
|
<ServiceAccountPermissions serviceAccount={serviceAccount} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
|
@ -2,7 +2,7 @@ import { css } from '@emotion/css';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { dateTimeFormat, GrafanaTheme2, OrgRole, TimeZone } from '@grafana/data';
|
import { dateTimeFormat, GrafanaTheme2, OrgRole, TimeZone } from '@grafana/data';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { Label, TextLink, useStyles2 } from '@grafana/ui';
|
||||||
import { fetchRoleOptions } from 'app/core/components/RolePicker/api';
|
import { fetchRoleOptions } from 'app/core/components/RolePicker/api';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
import { AccessControlAction, Role, ServiceAccountDTO } from 'app/types';
|
import { AccessControlAction, Role, ServiceAccountDTO } from 'app/types';
|
||||||
@ -53,7 +53,7 @@ export function ServiceAccountProfile({ serviceAccount, timeZone, onChange }: Pr
|
|||||||
<ServiceAccountProfileRow
|
<ServiceAccountProfileRow
|
||||||
label="Name"
|
label="Name"
|
||||||
value={serviceAccount.name}
|
value={serviceAccount.name}
|
||||||
onChange={onNameChange}
|
onChange={!serviceAccount.isExternal ? onNameChange : undefined}
|
||||||
disabled={!ableToWrite || serviceAccount.isDisabled}
|
disabled={!ableToWrite || serviceAccount.isDisabled}
|
||||||
/>
|
/>
|
||||||
<ServiceAccountProfileRow label="ID" value={serviceAccount.login} disabled={serviceAccount.isDisabled} />
|
<ServiceAccountProfileRow label="ID" value={serviceAccount.login} disabled={serviceAccount.isDisabled} />
|
||||||
@ -68,6 +68,16 @@ export function ServiceAccountProfile({ serviceAccount, timeZone, onChange }: Pr
|
|||||||
value={dateTimeFormat(serviceAccount.createdAt, { timeZone })}
|
value={dateTimeFormat(serviceAccount.createdAt, { timeZone })}
|
||||||
disabled={serviceAccount.isDisabled}
|
disabled={serviceAccount.isDisabled}
|
||||||
/>
|
/>
|
||||||
|
{serviceAccount.isExternal && serviceAccount.requiredBy && (
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Label>Used by</Label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<TextLink href={`/plugins/${serviceAccount.requiredBy}`}>{serviceAccount.requiredBy}</TextLink>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -42,7 +42,7 @@ export const ServiceAccountRoleRow = ({ label, serviceAccount, roleOptions, onRo
|
|||||||
inputId={inputId}
|
inputId={inputId}
|
||||||
aria-label="Role"
|
aria-label="Role"
|
||||||
value={serviceAccount.role}
|
value={serviceAccount.role}
|
||||||
disabled={serviceAccount.isDisabled}
|
disabled={serviceAccount.isExternal || serviceAccount.isDisabled}
|
||||||
onChange={onRoleChange}
|
onChange={onRoleChange}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
@ -35,6 +35,7 @@ export interface ServiceAccountDTO extends WithAccessControlMetadata {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
isExternal?: boolean;
|
isExternal?: boolean;
|
||||||
|
requiredBy?: string;
|
||||||
teams: string[];
|
teams: string[];
|
||||||
role: OrgRole;
|
role: OrgRole;
|
||||||
roles?: Role[];
|
roles?: Role[];
|
||||||
|
@ -9626,6 +9626,10 @@
|
|||||||
"format": "int64",
|
"format": "int64",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"requiredBy": {
|
||||||
|
"example": "grafana-app",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"role": {
|
"role": {
|
||||||
"example": "Editor",
|
"example": "Editor",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
Loading…
Reference in New Issue
Block a user