diff --git a/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes.md b/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes.md index 62bd4e19bf0..a207b91e10a 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes.md +++ b/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes.md @@ -77,7 +77,7 @@ The following list contains role-based access control actions. | `licensing:read` | n/a | Read licensing information. | | `licensing:write` | n/a | Update the license token. | | `org.users:write` | `users:*`
`users:id:*` | Update the organization role (`Viewer`, `Editor`, or `Admin`) of a user. | -| `org.users:add` | `users:*` | Add a user to an organization. | +| `org.users:add` | `users:*` | Add a user to an organization or invite a new user to an organization. | | `org.users:read` | `users:*`
`users:id:*` | Get user profiles within an organization. | | `org.users:remove` | `users:*`
`users:id:*` | Remove a user from an organization. | | `org:create` | n/a | Create an organization. | diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions.md index ccb0ffebc7c..b1cd0e2f972 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions.md +++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions.md @@ -61,7 +61,7 @@ The following tables list permissions associated with basic and fixed roles. | `fixed:licensing:reader` | `licensing:read`
`licensing.reports:read` | Read licensing information and licensing reports. | | `fixed:licensing:writer` | All permissions from `fixed:licensing:viewer` and
`licensing:write`
`licensing:delete` | Read licensing information and licensing reports, update and delete the license token. | | `fixed:org.users:reader` | `org.users:read` | Read users within a single organization. | -| `fixed:org.users:writer` | All permissions from `fixed:org.users:reader` and
`org.users:add`
`org.users:remove`
`org.users:write` | Within a single organization, add a user, invite a user, read information about a user and their role, remove a user from that organization, or change the role of a user. | +| `fixed:org.users:writer` | All permissions from `fixed:org.users:reader` and
`org.users:add`
`org.users:remove`
`org.users:write` | Within a single organization, add a user, invite a new user, read information about a user and their role, remove a user from that organization, or change the role of a user. | | `fixed:organization:maintainer` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs:create`
`orgs:delete`
`orgs.quotas:write` | Create, read, write, or delete an organization. Read or write its quotas. This role needs to be assigned globally. | | `fixed:organization:reader` | `orgs:read`
`orgs.quotas:read` | Read an organization and its quotas. | | `fixed:organization:writer` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs.preferences:read`
`orgs.preferences:write` | Read an organization, its quotas, or its preferences. Update organization properties, or its preferences. | diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index e7716d7e2b2..716edc93eb0 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -429,13 +429,6 @@ var orgsCreateAccessEvaluator = ac.EvalAll( ac.EvalPermission(ActionOrgsCreate), ) -// usersInviteEvaluator is used to protect the "Configuration > Users > Invite" page access -// accessible to org admins and server admins by default -var usersInviteEvaluator = ac.EvalAny( - ac.EvalPermission(ac.ActionUsersCreate), - ac.EvalPermission(ac.ActionOrgUsersAdd), -) - // teamsAccessEvaluator is used to protect the "Configuration > Teams" page access // grants access to a user when they can either create teams or can read and update a team var teamsAccessEvaluator = ac.EvalAny( diff --git a/pkg/api/api.go b/pkg/api/api.go index 35fa45bd7ef..06c55d8a7bc 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -90,7 +90,7 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/datasources/edit/*", authorize(reqOrgAdmin, datasources.EditPageAccess), hs.Index) r.Get("/org/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), hs.Index) r.Get("/org/users/new", reqOrgAdmin, hs.Index) - r.Get("/org/users/invite", authorize(reqOrgAdmin, usersInviteEvaluator), hs.Index) + r.Get("/org/users/invite", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd)), hs.Index) r.Get("/org/teams", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsRead)), hs.Index) r.Get("/org/teams/edit/*", authorize(reqCanAccessTeams, teamsEditAccessEvaluator), hs.Index) r.Get("/org/teams/new", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsCreate)), hs.Index) @@ -281,9 +281,9 @@ func (hs *HTTPServer) registerRoutes() { orgRoute.Delete("/users/:userId", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(hs.RemoveOrgUserForCurrentOrg)) // invites - orgRoute.Get("/invites", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), routing.Wrap(hs.GetPendingOrgInvites)) - orgRoute.Post("/invites", authorize(reqOrgAdmin, usersInviteEvaluator), quota("user"), routing.Wrap(hs.AddOrgInvite)) - orgRoute.Patch("/invites/:code/revoke", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), routing.Wrap(hs.RevokeInvite)) + orgRoute.Get("/invites", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd)), routing.Wrap(hs.GetPendingOrgInvites)) + orgRoute.Post("/invites", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd)), quota("user"), routing.Wrap(hs.AddOrgInvite)) + orgRoute.Patch("/invites/:code/revoke", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd)), routing.Wrap(hs.RevokeInvite)) // prefs orgRoute.Get("/preferences", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsPreferencesRead)), routing.Wrap(hs.GetOrgPreferences)) diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index 5044d0a4ba6..58fe29b2374 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -84,15 +84,6 @@ func (hs *HTTPServer) AddOrgInvite(c *models.ReqContext) response.Response { return hs.inviteExistingUserToOrg(c, userQuery.Result, &inviteDto) } - // Evaluate permissions for inviting a new user to Grafana - hasAccess, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, ac.EvalPermission(ac.ActionUsersCreate)) - if err != nil { - return response.Error(http.StatusInternalServerError, "Failed to evaluate permissions", err) - } - if !hasAccess { - return response.Error(http.StatusForbidden, "Permission denied: not permitted to create a new user", err) - } - if setting.DisableLoginForm { return response.Error(400, "Cannot invite when login is disabled.", nil) } @@ -103,6 +94,7 @@ func (hs *HTTPServer) AddOrgInvite(c *models.ReqContext) response.Response { cmd.Name = inviteDto.Name cmd.Status = models.TmpUserInvitePending cmd.InvitedByUserId = c.UserId + var err error cmd.Code, err = util.GetRandomString(30) if err != nil { return response.Error(500, "Could not generate random string", err) diff --git a/pkg/api/org_invite_test.go b/pkg/api/org_invite_test.go index 4f4cd04edf1..dedf03736ff 100644 --- a/pkg/api/org_invite_test.go +++ b/pkg/api/org_invite_test.go @@ -23,7 +23,7 @@ func TestOrgInvitesAPIEndpointAccess(t *testing.T) { tests := []accessControlTestCase2{ { expectedCode: http.StatusOK, - desc: "org viewer with the correct permissions can invite and existing user to his org", + desc: "org viewer with the correct permissions can invite an existing user to his org", url: "/api/org/invites", method: http.MethodPost, permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}}, @@ -31,7 +31,7 @@ func TestOrgInvitesAPIEndpointAccess(t *testing.T) { }, { expectedCode: http.StatusForbidden, - desc: "org viewer with missing permissions cannot invite and existing user to his org", + desc: "org viewer with missing permissions cannot invite an existing user to his org", url: "/api/org/invites", method: http.MethodPost, permissions: []accesscontrol.Permission{}, @@ -39,26 +39,18 @@ func TestOrgInvitesAPIEndpointAccess(t *testing.T) { }, { expectedCode: http.StatusForbidden, - desc: "org viewer with the wrong scope cannot invite and existing user to his org", + desc: "org viewer with the wrong scope cannot invite an existing user to his org", url: "/api/org/invites", method: http.MethodPost, permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: "users:id:100"}}, input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(models.ROLE_VIEWER) + `"}`, }, - { - expectedCode: http.StatusForbidden, - desc: "org viewer with user add permission cannot invite a new user to his org", - url: "/api/org/invites", - method: http.MethodPost, - permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}}, - input: `{"loginOrEmail": "new user", "role": "` + string(models.ROLE_VIEWER) + `"}`, - }, { expectedCode: http.StatusOK, desc: "org viewer with the correct permissions can invite a new user to his org", url: "/api/org/invites", method: http.MethodPost, - permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionUsersCreate}}, + permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}}, input: `{"loginOrEmail": "new user", "role": "` + string(models.ROLE_VIEWER) + `"}`, }, { diff --git a/pkg/api/org_users_test.go b/pkg/api/org_users_test.go index aa408a187ca..8073d04b1ef 100644 --- a/pkg/api/org_users_test.go +++ b/pkg/api/org_users_test.go @@ -650,7 +650,7 @@ func TestOrgUsersAPIEndpointWithSetPerms_AccessControl(t *testing.T) { desc: "org viewer with the correct permissions can invite a user as a viewer in his org", url: "/api/org/invites", method: http.MethodPost, - permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionUsersCreate}}, + permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}}, input: `{"loginOrEmail": "newUserEmail@test.com", "sendEmail": false, "role": "` + string(models.ROLE_VIEWER) + `"}`, }, { diff --git a/public/app/features/users/UsersActionBar.tsx b/public/app/features/users/UsersActionBar.tsx index 0d69e835bf3..b9dc2906313 100644 --- a/public/app/features/users/UsersActionBar.tsx +++ b/public/app/features/users/UsersActionBar.tsx @@ -37,9 +37,7 @@ export class UsersActionBar extends PureComponent { { label: 'Users', value: 'users' }, { label: `Pending Invites (${pendingInvitesCount})`, value: 'invites' }, ]; - const canAddToOrg: boolean = - contextSrv.hasAccess(AccessControlAction.UsersCreate, canInvite) || - contextSrv.hasAccess(AccessControlAction.OrgUsersAdd, canInvite); + const canAddToOrg: boolean = contextSrv.hasAccess(AccessControlAction.OrgUsersAdd, canInvite); return (