diff --git a/docs/sources/administration/user-management/manage-dashboard-permissions/index.md b/docs/sources/administration/user-management/manage-dashboard-permissions/index.md index 5e181405478..8a656049776 100644 --- a/docs/sources/administration/user-management/manage-dashboard-permissions/index.md +++ b/docs/sources/administration/user-management/manage-dashboard-permissions/index.md @@ -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**. diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index 8d3d37a160d..87a95f89a89 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -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 { diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index 4c13bbae6df..3b469539b71 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -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...), diff --git a/pkg/services/accesscontrol/resourcepermissions/api.go b/pkg/services/accesscontrol/resourcepermissions/api.go index a92e1f1475a..9ab6cf9f1c4 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api.go +++ b/pkg/services/accesscontrol/resourcepermissions/api.go @@ -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, }) } } diff --git a/pkg/services/accesscontrol/resourcepermissions/store.go b/pkg/services/accesscontrol/resourcepermissions/store.go index 8db2ca16a18..00fb2635649 100644 --- a/pkg/services/accesscontrol/resourcepermissions/store.go +++ b/pkg/services/accesscontrol/resourcepermissions/store.go @@ -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, } } diff --git a/public/app/core/components/AccessControl/AddPermission.tsx b/public/app/core/components/AccessControl/AddPermission.tsx index b6e8527f22c..740e0acc041 100644 --- a/public/app/core/components/AccessControl/AddPermission.tsx +++ b/public/app/core/components/AccessControl/AddPermission.tsx @@ -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 && setUserId(u?.value || 0)} />} + {target === PermissionTarget.ServiceAccount && ( + setUserId(u?.value || 0)} /> + )} + {target === PermissionTarget.Team && setTeamId(t.value?.id || 0)} />} {target === PermissionTarget.BuiltInRole && ( diff --git a/public/app/core/components/AccessControl/Permissions.tsx b/public/app/core/components/AccessControl/Permissions.tsx index 8e41b7540db..92dfb78e7bd 100644 --- a/public/app/core/components/AccessControl/Permissions.tsx +++ b/public/app/core/components/AccessControl/Permissions.tsx @@ -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 | 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} /> + + ) => void; + className?: string; + inputId?: string; +} + +export interface State { + isLoading: boolean; +} + +export class ServiceAccountPicker extends Component { + debouncedSearch: DebouncedFuncLeading; + + 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 ( +
+ +
+ ); + } +} diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts index cb14abce28f..5c9e80d71e4 100644 --- a/public/app/types/acl.ts +++ b/public/app/types/acl.ts @@ -88,6 +88,7 @@ export enum DataSourcePermissionLevel { export enum AclTarget { Team = 'Team', User = 'User', + ServiceAccount = 'ServiceAccount', Viewer = 'Viewer', Editor = 'Editor', } diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index a3a351ff484..6b13a287858 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -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": "" diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 2a273dbcb54..9d4ae262e9d 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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" diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 3e400ed2f3a..55fbec548ea 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -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": "" diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 3f536aec4df..fa76398f0ba 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -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": "" diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 8188fe6aedd..e54ce1ec676 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -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": "Ůşęř" diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index ab3e400cdcb..f7bcbfbe56f 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -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": ""