IAM: Protect external service accounts frontend list page (#77834)

* Add `isExternal` property to frontend model

* Remove enabled and token buttons for external SA

* Replace trash icon for lock icon for external SA

* Block the role picker for external SA

* Filter SA list using the external filter

* Add only external filter at backend

---------

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
This commit is contained in:
linoman
2023-11-09 17:45:46 +01:00
committed by GitHub
parent d4322f6e5a
commit 5bc4f56c79
11 changed files with 80 additions and 42 deletions

View File

@@ -6767,7 +6767,7 @@
"type": "boolean",
"example": false
},
"isManaged": {
"isExternal": {
"type": "boolean",
"example": false
},
@@ -6822,7 +6822,7 @@
"type": "boolean",
"example": false
},
"isManaged": {
"isExternal": {
"type": "boolean",
"example": false
},

View File

@@ -18652,7 +18652,7 @@
"type": "boolean",
"example": false
},
"isManaged": {
"isExternal": {
"type": "boolean",
"example": false
},
@@ -18707,7 +18707,7 @@
"type": "boolean",
"example": false
},
"isManaged": {
"isExternal": {
"type": "boolean",
"example": false
},

View File

@@ -17,6 +17,7 @@ import {
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { Page } from 'app/core/components/Page/Page';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
import config from 'app/core/config';
import { contextSrv } from 'app/core/core';
import { StoreState, ServiceAccountDTO, AccessControlAction, ServiceAccountStateFilter } from 'app/types';
@@ -56,6 +57,16 @@ const mapDispatchToProps = {
const connector = connect(mapStateToProps, mapDispatchToProps);
const availableFilters = [
{ label: 'All', value: ServiceAccountStateFilter.All },
{ label: 'With expired tokens', value: ServiceAccountStateFilter.WithExpiredTokens },
{ label: 'Disabled', value: ServiceAccountStateFilter.Disabled },
];
if (config.featureToggles.externalServiceAccounts || config.featureToggles.externalServiceAuth) {
availableFilters.push({ label: 'Managed', value: ServiceAccountStateFilter.External });
}
export const ServiceAccountsListPageUnconnected = ({
page,
changePage,
@@ -191,11 +202,7 @@ export const ServiceAccountsListPageUnconnected = ({
/>
</InlineField>
<RadioButtonGroup
options={[
{ label: 'All', value: ServiceAccountStateFilter.All },
{ label: 'With expired tokens', value: ServiceAccountStateFilter.WithExpiredTokens },
{ label: 'Disabled', value: ServiceAccountStateFilter.Disabled },
]}
options={availableFilters}
onChange={onStateFilterChange}
value={serviceAccountStateFilter}
className={styles.filter}

View File

@@ -91,7 +91,7 @@ const ServiceAccountListItem = memo(
<OrgRolePicker
aria-label="Role"
value={serviceAccount.role}
disabled={!canUpdateRole || serviceAccount.isDisabled}
disabled={serviceAccount.isExternal || !canUpdateRole || serviceAccount.isDisabled}
onChange={(newRole) => onRoleChange(newRole, serviceAccount)}
/>
</td>
@@ -112,32 +112,48 @@ const ServiceAccountListItem = memo(
</a>
</td>
<td>
<HorizontalGroup justify="flex-end">
{contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite) && !serviceAccount.tokens && (
<Button onClick={() => onAddTokenClick(serviceAccount)} disabled={serviceAccount.isDisabled}>
Add token
</Button>
)}
{contextSrv.hasPermissionInMetadata(AccessControlAction.ServiceAccountsWrite, serviceAccount) &&
(serviceAccount.isDisabled ? (
<Button variant="primary" onClick={() => onEnable(serviceAccount)}>
Enable
{!serviceAccount.isExternal && (
<HorizontalGroup justify="flex-end">
{contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite) && !serviceAccount.tokens && (
<Button
onClick={() => onAddTokenClick(serviceAccount)}
disabled={serviceAccount.isDisabled}
className={styles.actionButton}
>
Add token
</Button>
) : (
<Button variant="secondary" onClick={() => onDisable(serviceAccount)}>
Disable
</Button>
))}
{contextSrv.hasPermissionInMetadata(AccessControlAction.ServiceAccountsDelete, serviceAccount) && (
)}
{contextSrv.hasPermissionInMetadata(AccessControlAction.ServiceAccountsWrite, serviceAccount) &&
(serviceAccount.isDisabled ? (
<Button variant="primary" onClick={() => onEnable(serviceAccount)} className={styles.actionButton}>
Enable
</Button>
) : (
<Button variant="secondary" onClick={() => onDisable(serviceAccount)} className={styles.actionButton}>
Disable
</Button>
))}
{contextSrv.hasPermissionInMetadata(AccessControlAction.ServiceAccountsDelete, serviceAccount) && (
<IconButton
className={styles.deleteButton}
name="trash-alt"
size="md"
onClick={() => onRemoveButtonClick(serviceAccount)}
tooltip={`Delete service account ${serviceAccount.name}`}
/>
)}
</HorizontalGroup>
)}
{serviceAccount.isExternal && (
<HorizontalGroup justify="flex-end">
<IconButton
className={styles.deleteButton}
name="trash-alt"
disabled={true}
name="lock"
size="md"
onClick={() => onRemoveButtonClick(serviceAccount)}
tooltip={`Delete service account ${serviceAccount.name}`}
tooltip={`This is a managed service account and cannot be modified.`}
/>
)}
</HorizontalGroup>
</HorizontalGroup>
)}
</td>
</tr>
);
@@ -174,6 +190,9 @@ const getStyles = (theme: GrafanaTheme2) => {
color: ${theme.colors.text.secondary};
}
`,
actionButton: css({
minWidth: 85,
}),
};
};

View File

@@ -114,6 +114,8 @@ const getStateFilter = (value: ServiceAccountStateFilter) => {
return '&expiredTokens=true';
case ServiceAccountStateFilter.Disabled:
return '&disabled=true';
case ServiceAccountStateFilter.External:
return '&external=true';
default:
return '';
}

View File

@@ -34,6 +34,7 @@ export interface ServiceAccountDTO extends WithAccessControlMetadata {
avatarUrl?: string;
createdAt: string;
isDisabled: boolean;
isExternal?: boolean;
teams: string[];
role: OrgRole;
roles?: Role[];
@@ -60,6 +61,7 @@ export interface ServiceAccountProfileState {
export enum ServiceAccountStateFilter {
All = 'All',
WithExpiredTokens = 'WithExpiredTokens',
External = 'External',
Disabled = 'Disabled',
}

View File

@@ -9554,7 +9554,7 @@
"example": false,
"type": "boolean"
},
"isManaged": {
"isExternal": {
"example": false,
"type": "boolean"
},
@@ -9609,7 +9609,7 @@
"example": false,
"type": "boolean"
},
"isManaged": {
"isExternal": {
"example": false,
"type": "boolean"
},