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 (