Service Accounts: Enable adding folder, dashboard and data source permissions to service accounts (#76133)

* Add SAs to Datasource permissions

Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>

* add SAs to dashboards/folders managed permissions

* Update public/app/core/components/AccessControl/Permissions.tsx

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>

* regenerate i18n

* add doc

---------

Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>
This commit is contained in:
Jo 2023-10-06 17:48:13 +02:00 committed by GitHub
parent 8e576911b7
commit 4474f19836
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 226 additions and 84 deletions

View File

@ -18,12 +18,12 @@ For more information about dashboard permissions, refer to [Dashboard permission
## Grant folder permissions
When you grant user permissions for folders, that setting applies to all dashboards and subfolders contained in the folder. Consider using this approach to assigning dashboard and folder permissions when you have users or teams who require access to groups of related dashboards or folders.
When you grant user permissions for folders, that setting applies to all dashboards and subfolders contained in the folder. Consider using this approach to assigning dashboard and folder permissions when you have users, service accounts or teams who require access to groups of related dashboards or folders.
### Before you begin
- Ensure you have organization administrator privileges
- Identify the dashboard folder permissions you want to modify and the users or teams to which you want to grant access. For more information about dashboard permissions, refer to [Dashboard permissions]({{< relref "../../roles-and-permissions/#dashboard-permissions" >}}).
- Identify the dashboard folder permissions you want to modify and the users, service accounts or teams to which you want to grant access. For more information about dashboard permissions, refer to [Dashboard permissions]({{< relref "../../roles-and-permissions/#dashboard-permissions" >}}).
**To grant dashboard folder permissions**:
@ -31,7 +31,7 @@ When you grant user permissions for folders, that setting applies to all dashboa
1. In the left-side menu, click **Dashboards**.
1. Hover your mouse cursor over a folder and click **Go to folder**.
1. Click the **Permissions** tab, and then click **Add a permission**.
1. In the **Add Permission For** dropdown menu, select **User**, **Team**, or **Role**.
1. In the **Add Permission For** dropdown menu, select **User**, **Service Account**, **Team**, or **Role**.
1. Select the user, team, or role.
1. Select the permission and click **Save**.
@ -59,7 +59,7 @@ Grant dashboard permissions when you want to restrict or enhance dashboard acces
1. Open a dashboard.
1. In the top right corner of the dashboard, click **Dashboard settings** (the cog icon).
1. Click **Permissions** in left-side menu, and then **Add a permission**.
1. In the **Add Permission For** dropdown menu, select **User**, **Team**, or **Role**.
1. In the **Add Permission For** dropdown menu, select **User**, **Service Account**, **Team**, or **Role**.
1. Select the user, team, or role.
1. Select the permission and click **Save**.

View File

@ -225,21 +225,22 @@ type GetUserPermissionsQuery struct {
// ResourcePermission is structure that holds all actions that either a team / user / builtin-role
// can perform against specific resource.
type ResourcePermission struct {
ID int64
RoleName string
Actions []string
Scope string
UserId int64
UserLogin string
UserEmail string
TeamId int64
TeamEmail string
Team string
BuiltInRole string
IsManaged bool
IsInherited bool
Created time.Time
Updated time.Time
ID int64
RoleName string
Actions []string
Scope string
UserId int64
UserLogin string
UserEmail string
TeamId int64
TeamEmail string
Team string
BuiltInRole string
IsManaged bool
IsInherited bool
IsServiceAccount bool
Created time.Time
Updated time.Time
}
func (p *ResourcePermission) Contains(targetActions []string) bool {

View File

@ -165,9 +165,10 @@ func ProvideDashboardPermissions(
return []string{}, nil
},
Assignments: resourcepermissions.Assignments{
Users: true,
Teams: true,
BuiltInRoles: true,
Users: true,
Teams: true,
BuiltInRoles: true,
ServiceAccounts: true,
},
PermissionsToActions: map[string][]string{
"View": DashboardViewActions,
@ -226,9 +227,10 @@ func ProvideFolderPermissions(
return dashboards.GetInheritedScopes(ctx, orgID, resourceID, folderService)
},
Assignments: resourcepermissions.Assignments{
Users: true,
Teams: true,
BuiltInRoles: true,
Users: true,
Teams: true,
BuiltInRoles: true,
ServiceAccounts: true,
},
PermissionsToActions: map[string][]string{
"View": append(DashboardViewActions, FolderViewActions...),

View File

@ -57,9 +57,10 @@ func (a *api) registerEndpoints() {
}
type Assignments struct {
Users bool `json:"users"`
Teams bool `json:"teams"`
BuiltInRoles bool `json:"builtInRoles"`
Users bool `json:"users"`
ServiceAccounts bool `json:"serviceAccounts"`
Teams bool `json:"teams"`
BuiltInRoles bool `json:"builtInRoles"`
}
type Description struct {
@ -75,19 +76,20 @@ func (a *api) getDescription(c *contextmodel.ReqContext) response.Response {
}
type resourcePermissionDTO struct {
ID int64 `json:"id"`
RoleName string `json:"roleName"`
IsManaged bool `json:"isManaged"`
IsInherited bool `json:"isInherited"`
UserID int64 `json:"userId,omitempty"`
UserLogin string `json:"userLogin,omitempty"`
UserAvatarUrl string `json:"userAvatarUrl,omitempty"`
Team string `json:"team,omitempty"`
TeamID int64 `json:"teamId,omitempty"`
TeamAvatarUrl string `json:"teamAvatarUrl,omitempty"`
BuiltInRole string `json:"builtInRole,omitempty"`
Actions []string `json:"actions"`
Permission string `json:"permission"`
ID int64 `json:"id"`
RoleName string `json:"roleName"`
IsManaged bool `json:"isManaged"`
IsInherited bool `json:"isInherited"`
IsServiceAccount bool `json:"isServiceAccount"`
UserID int64 `json:"userId,omitempty"`
UserLogin string `json:"userLogin,omitempty"`
UserAvatarUrl string `json:"userAvatarUrl,omitempty"`
Team string `json:"team,omitempty"`
TeamID int64 `json:"teamId,omitempty"`
TeamAvatarUrl string `json:"teamAvatarUrl,omitempty"`
BuiltInRole string `json:"builtInRole,omitempty"`
Actions []string `json:"actions"`
Permission string `json:"permission"`
}
func (a *api) getPermissions(c *contextmodel.ReqContext) response.Response {
@ -115,19 +117,20 @@ func (a *api) getPermissions(c *contextmodel.ReqContext) response.Response {
}
dto = append(dto, resourcePermissionDTO{
ID: p.ID,
RoleName: p.RoleName,
UserID: p.UserId,
UserLogin: p.UserLogin,
UserAvatarUrl: dtos.GetGravatarUrl(p.UserEmail),
Team: p.Team,
TeamID: p.TeamId,
TeamAvatarUrl: teamAvatarUrl,
BuiltInRole: p.BuiltInRole,
Actions: p.Actions,
Permission: permission,
IsManaged: p.IsManaged,
IsInherited: p.IsInherited,
ID: p.ID,
RoleName: p.RoleName,
UserID: p.UserId,
UserLogin: p.UserLogin,
UserAvatarUrl: dtos.GetGravatarUrl(p.UserEmail),
Team: p.Team,
TeamID: p.TeamId,
TeamAvatarUrl: teamAvatarUrl,
BuiltInRole: p.BuiltInRole,
Actions: p.Actions,
Permission: permission,
IsManaged: p.IsManaged,
IsInherited: p.IsInherited,
IsServiceAccount: p.IsServiceAccount,
})
}
}

View File

@ -25,19 +25,20 @@ type store struct {
}
type flatResourcePermission struct {
ID int64 `xorm:"id"`
RoleName string
Action string
Scope string
UserId int64
UserLogin string
UserEmail string
TeamId int64
TeamEmail string
Team string
BuiltInRole string
Created time.Time
Updated time.Time
ID int64 `xorm:"id"`
RoleName string
Action string
Scope string
UserId int64
UserLogin string
UserEmail string
TeamId int64
TeamEmail string
Team string
BuiltInRole string
IsServiceAccount bool `xorm:"is_service_account"`
Created time.Time
Updated time.Time
}
func (p *flatResourcePermission) IsManaged(scope string) bool {
@ -307,6 +308,7 @@ func (s *store) getResourcePermissions(sess *db.Session, orgID int64, query GetR
userSelect := rawSelect + `
ur.user_id AS user_id,
u.login AS user_login,
u.is_service_account AS is_service_account,
u.email AS user_email,
0 AS team_id,
'' AS team,
@ -317,6 +319,7 @@ func (s *store) getResourcePermissions(sess *db.Session, orgID int64, query GetR
teamSelect := rawSelect + `
0 AS user_id,
'' AS user_login,
` + s.sql.GetDialect().BooleanStr(false) + ` AS is_service_account,
'' AS user_email,
tr.team_id AS team_id,
t.name AS team,
@ -327,6 +330,7 @@ func (s *store) getResourcePermissions(sess *db.Session, orgID int64, query GetR
builtinSelect := rawSelect + `
0 AS user_id,
'' AS user_login,
` + s.sql.GetDialect().BooleanStr(false) + ` AS is_service_account,
'' AS user_email,
0 as team_id,
'' AS team,
@ -480,21 +484,22 @@ func flatPermissionsToResourcePermission(scope string, permissions []flatResourc
first := permissions[0]
return &accesscontrol.ResourcePermission{
ID: first.ID,
RoleName: first.RoleName,
Actions: actions,
Scope: first.Scope,
UserId: first.UserId,
UserLogin: first.UserLogin,
UserEmail: first.UserEmail,
TeamId: first.TeamId,
TeamEmail: first.TeamEmail,
Team: first.Team,
BuiltInRole: first.BuiltInRole,
Created: first.Created,
Updated: first.Updated,
IsManaged: first.IsManaged(scope),
IsInherited: first.IsInherited(scope),
ID: first.ID,
RoleName: first.RoleName,
Actions: actions,
Scope: first.Scope,
UserId: first.UserId,
UserLogin: first.UserLogin,
UserEmail: first.UserEmail,
TeamId: first.TeamId,
TeamEmail: first.TeamEmail,
Team: first.Team,
BuiltInRole: first.BuiltInRole,
Created: first.Created,
Updated: first.Updated,
IsManaged: first.IsManaged(scope),
IsInherited: first.IsInherited(scope),
IsServiceAccount: first.IsServiceAccount,
}
}

View File

@ -3,6 +3,7 @@ import React, { useEffect, useMemo, useState } from 'react';
import { Stack } from '@grafana/experimental';
import { Button, Form, Select } from '@grafana/ui';
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
import { ServiceAccountPicker } from 'app/core/components/Select/ServiceAccountPicker';
import { TeamPicker } from 'app/core/components/Select/TeamPicker';
import { UserPicker } from 'app/core/components/Select/UserPicker';
import { Trans, t } from 'app/core/internationalization';
@ -36,6 +37,12 @@ export const AddPermission = ({
if (assignments.users) {
options.push({ value: PermissionTarget.User, label: t('access-control.add-permission.user-label', 'User') });
}
if (assignments.serviceAccounts) {
options.push({
value: PermissionTarget.ServiceAccount,
label: t('access-control.add-permission.serviceaccount-label', 'Service Account'),
});
}
if (assignments.teams) {
options.push({ value: PermissionTarget.Team, label: t('access-control.add-permission.team-label', 'Team') });
}
@ -57,6 +64,7 @@ export const AddPermission = ({
const isValid = () =>
(target === PermissionTarget.Team && teamId > 0) ||
(target === PermissionTarget.User && userId > 0) ||
(target === PermissionTarget.ServiceAccount && userId > 0) ||
(PermissionTarget.BuiltInRole && OrgRole.hasOwnProperty(builtInRole));
return (
@ -82,6 +90,10 @@ export const AddPermission = ({
{target === PermissionTarget.User && <UserPicker onSelected={(u) => setUserId(u?.value || 0)} />}
{target === PermissionTarget.ServiceAccount && (
<ServiceAccountPicker onSelected={(u) => setUserId(u?.value || 0)} />
)}
{target === PermissionTarget.Team && <TeamPicker onSelected={(t) => setTeamId(t.value?.id || 0)} />}
{target === PermissionTarget.BuiltInRole && (

View File

@ -22,12 +22,13 @@ const INITIAL_DESCRIPTION: Description = {
assignments: {
teams: false,
users: false,
serviceAccounts: false,
builtInRoles: false,
},
};
type ResourceId = string | number;
type Type = 'users' | 'teams' | 'builtInRoles';
type Type = 'users' | 'teams' | 'serviceAccounts' | 'builtInRoles';
export type Props = {
title?: string;
@ -68,6 +69,8 @@ export const Permissions = ({
let promise: Promise<void> | null = null;
if (state.target === PermissionTarget.User) {
promise = setUserPermission(resource, resourceId, state.userId!, state.permission);
} else if (state.target === PermissionTarget.ServiceAccount) {
promise = setUserPermission(resource, resourceId, state.userId!, state.permission);
} else if (state.target === PermissionTarget.Team) {
promise = setTeamPermission(resource, resourceId, state.teamId!, state.permission);
} else if (state.target === PermissionTarget.BuiltInRole) {
@ -85,6 +88,8 @@ export const Permissions = ({
promise = setUserPermission(resource, resourceId, item.userId, EMPTY_PERMISSION);
} else if (item.teamId) {
promise = setTeamPermission(resource, resourceId, item.teamId, EMPTY_PERMISSION);
} else if (item.isServiceAccount && item.userId) {
promise = setUserPermission(resource, resourceId, item.userId, EMPTY_PERMISSION);
} else if (item.builtInRole) {
promise = setBuiltInRolePermission(resource, resourceId, item.builtInRole, EMPTY_PERMISSION);
}
@ -100,6 +105,8 @@ export const Permissions = ({
}
if (item.userId) {
onAdd({ permission, userId: item.userId, target: PermissionTarget.User });
} else if (item.isServiceAccount) {
onAdd({ permission, userId: item.userId, target: PermissionTarget.User });
} else if (item.teamId) {
onAdd({ permission, teamId: item.teamId, target: PermissionTarget.Team });
} else if (item.builtInRole) {
@ -118,7 +125,15 @@ export const Permissions = ({
const users = useMemo(
() =>
sortBy(
items.filter((i) => i.userId),
items.filter((i) => i.userId && !i.isServiceAccount),
['userLogin', 'isManaged']
),
[items]
);
const serviceAccounts = useMemo(
() =>
sortBy(
items.filter((i) => i.userId && i.isServiceAccount),
['userLogin', 'isManaged']
),
[items]
@ -134,6 +149,7 @@ export const Permissions = ({
const titleRole = t('access-control.permissions.role', 'Role');
const titleUser = t('access-control.permissions.user', 'User');
const titleServiceAccount = t('access-control.permissions.serviceaccount', 'Service Account');
const titleTeam = t('access-control.permissions.team', 'Team');
return (
@ -202,6 +218,16 @@ export const Permissions = ({
onRemove={onRemove}
canSet={canSetPermissions}
/>
<PermissionList
title={titleServiceAccount}
items={serviceAccounts}
compareKey={'userLogin'}
permissionLevels={desc.permissions}
onChange={onChange}
onRemove={onRemove}
canSet={canSetPermissions}
/>
<PermissionList
title={titleTeam}
items={teams}

View File

@ -3,6 +3,7 @@ export type ResourcePermission = {
resourceId: string;
isManaged: boolean;
isInherited: boolean;
isServiceAccount: boolean;
userId?: number;
userLogin?: string;
userAvatarUrl?: string;
@ -26,6 +27,7 @@ export enum PermissionTarget {
None = 'None',
Team = 'Team',
User = 'User',
ServiceAccount = 'ServiceAccount',
BuiltInRole = 'builtInRole',
}
export type Description = {
@ -35,6 +37,7 @@ export type Description = {
export type Assignments = {
users: boolean;
serviceAccounts: boolean;
teams: boolean;
builtInRoles: boolean;
};

View File

@ -0,0 +1,77 @@
import { debounce, DebouncedFuncLeading, isNil } from 'lodash';
import React, { Component } from 'react';
import { SelectableValue } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { AsyncSelect } from '@grafana/ui';
import { ServiceAccountDTO, ServiceAccountsState } from 'app/types';
export interface Props {
onSelected: (user: SelectableValue<ServiceAccountDTO['id']>) => void;
className?: string;
inputId?: string;
}
export interface State {
isLoading: boolean;
}
export class ServiceAccountPicker extends Component<Props, State> {
debouncedSearch: DebouncedFuncLeading<typeof this.search>;
constructor(props: Props) {
super(props);
this.state = { isLoading: false };
this.search = this.search.bind(this);
this.debouncedSearch = debounce(this.search, 300, {
leading: true,
trailing: true,
});
}
search(query?: string) {
this.setState({ isLoading: true });
if (isNil(query)) {
query = '';
}
return getBackendSrv()
.get(`/api/serviceaccounts/search`)
.then((result: ServiceAccountsState) => {
return result.serviceAccounts.map((sa) => ({
id: sa.id,
value: sa.id,
label: sa.login,
imgUrl: sa.avatarUrl,
login: sa.login,
}));
})
.finally(() => {
this.setState({ isLoading: false });
});
}
render() {
const { className, onSelected, inputId } = this.props;
const { isLoading } = this.state;
return (
<div className="service-account-picker" data-testid="serviceAccountPicker">
<AsyncSelect
isClearable
className={className}
inputId={inputId}
isLoading={isLoading}
defaultOptions={true}
loadOptions={this.debouncedSearch}
onChange={onSelected}
placeholder="Start typing to search for service accounts"
noOptionsMessage="No service accounts found"
aria-label="Service Account picker"
/>
</div>
);
}
}

View File

@ -88,6 +88,7 @@ export enum DataSourcePermissionLevel {
export enum AclTarget {
Team = 'Team',
User = 'User',
ServiceAccount = 'ServiceAccount',
Viewer = 'Viewer',
Editor = 'Editor',
}

View File

@ -3,6 +3,7 @@
"access-control": {
"add-permission": {
"role-label": "",
"serviceaccount-label": "",
"team-label": "",
"title": "",
"user-label": ""
@ -18,6 +19,7 @@
"no-permissions": "",
"permissions-change-warning": "",
"role": "",
"serviceaccount": "",
"team": "",
"title": "",
"user": ""

View File

@ -3,6 +3,7 @@
"access-control": {
"add-permission": {
"role-label": "Role",
"serviceaccount-label": "Service Account",
"team-label": "Team",
"title": "Add permission for",
"user-label": "User"
@ -18,6 +19,7 @@
"no-permissions": "There are no permissions",
"permissions-change-warning": "This will change permissions for this folder and all its descendants. In total, this will affect:",
"role": "Role",
"serviceaccount": "Service Account",
"team": "Team",
"title": "Permissions",
"user": "User"

View File

@ -3,6 +3,7 @@
"access-control": {
"add-permission": {
"role-label": "",
"serviceaccount-label": "",
"team-label": "",
"title": "",
"user-label": ""
@ -18,6 +19,7 @@
"no-permissions": "",
"permissions-change-warning": "",
"role": "",
"serviceaccount": "",
"team": "",
"title": "",
"user": ""

View File

@ -3,6 +3,7 @@
"access-control": {
"add-permission": {
"role-label": "",
"serviceaccount-label": "",
"team-label": "",
"title": "",
"user-label": ""
@ -18,6 +19,7 @@
"no-permissions": "",
"permissions-change-warning": "",
"role": "",
"serviceaccount": "",
"team": "",
"title": "",
"user": ""

View File

@ -3,6 +3,7 @@
"access-control": {
"add-permission": {
"role-label": "Ŗőľę",
"serviceaccount-label": "Ŝęřvįčę Åččőūʼnŧ",
"team-label": "Ŧęäm",
"title": "Åđđ pęřmįşşįőʼn ƒőř",
"user-label": "Ůşęř"
@ -18,6 +19,7 @@
"no-permissions": "Ŧĥęřę äřę ʼnő pęřmįşşįőʼnş",
"permissions-change-warning": "Ŧĥįş ŵįľľ čĥäʼnģę pęřmįşşįőʼnş ƒőř ŧĥįş ƒőľđęř äʼnđ äľľ įŧş đęşčęʼnđäʼnŧş. Ĩʼn ŧőŧäľ, ŧĥįş ŵįľľ 䃃ęčŧ:",
"role": "Ŗőľę",
"serviceaccount": "Ŝęřvįčę Åččőūʼnŧ",
"team": "Ŧęäm",
"title": "Pęřmįşşįőʼnş",
"user": "Ůşęř"

View File

@ -3,6 +3,7 @@
"access-control": {
"add-permission": {
"role-label": "",
"serviceaccount-label": "",
"team-label": "",
"title": "",
"user-label": ""
@ -18,6 +19,7 @@
"no-permissions": "",
"permissions-change-warning": "",
"role": "",
"serviceaccount": "",
"team": "",
"title": "",
"user": ""