mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Access control: Make Admin/Users UI working with the permissions (#33176)
* API: authorize admin/users views * Render admin/users components based on user's permissions * Add LDAP permissions (required by admin/user page) * Extend default admin role by LDAP permissions * Show/hide LDAP debug views * Render LDAP debug page if user has access * Authorize LDAP debug view * fix permissions definitions * Add LDAP page permissions * remove ambiguous permissions check * Hide logout buttons in sessions table * Add org/users permissions * Use org permissions for managing user roles in orgs * Apply permissions to org/users * Apply suggestions from review * Fix tests * remove scopes from the frontend * Tweaks according to review * Handle /invites endpoints
This commit is contained in:
parent
66020b419c
commit
a7e721e987
@ -58,9 +58,9 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
r.Get("/datasources/", reqOrgAdmin, hs.Index)
|
r.Get("/datasources/", reqOrgAdmin, hs.Index)
|
||||||
r.Get("/datasources/new", reqOrgAdmin, hs.Index)
|
r.Get("/datasources/new", reqOrgAdmin, hs.Index)
|
||||||
r.Get("/datasources/edit/*", reqOrgAdmin, hs.Index)
|
r.Get("/datasources/edit/*", reqOrgAdmin, hs.Index)
|
||||||
r.Get("/org/users", reqOrgAdmin, hs.Index)
|
r.Get("/org/users", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersRead, accesscontrol.ScopeOrgCurrentUsersAll), hs.Index)
|
||||||
r.Get("/org/users/new", reqOrgAdmin, hs.Index)
|
r.Get("/org/users/new", reqOrgAdmin, hs.Index)
|
||||||
r.Get("/org/users/invite", reqOrgAdmin, hs.Index)
|
r.Get("/org/users/invite", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersAdd, accesscontrol.ScopeOrgCurrentUsersAll), hs.Index)
|
||||||
r.Get("/org/teams", reqCanAccessTeams, hs.Index)
|
r.Get("/org/teams", reqCanAccessTeams, hs.Index)
|
||||||
r.Get("/org/teams/*", reqCanAccessTeams, hs.Index)
|
r.Get("/org/teams/*", reqCanAccessTeams, hs.Index)
|
||||||
r.Get("/org/apikeys/", reqOrgAdmin, hs.Index)
|
r.Get("/org/apikeys/", reqOrgAdmin, hs.Index)
|
||||||
@ -68,13 +68,13 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
r.Get("/configuration", reqGrafanaAdmin, hs.Index)
|
r.Get("/configuration", reqGrafanaAdmin, hs.Index)
|
||||||
r.Get("/admin", reqGrafanaAdmin, hs.Index)
|
r.Get("/admin", reqGrafanaAdmin, hs.Index)
|
||||||
r.Get("/admin/settings", reqGrafanaAdmin, hs.Index)
|
r.Get("/admin/settings", reqGrafanaAdmin, hs.Index)
|
||||||
r.Get("/admin/users", reqGrafanaAdmin, hs.Index)
|
r.Get("/admin/users", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersRead, accesscontrol.ScopeUsersAll), hs.Index)
|
||||||
r.Get("/admin/users/create", reqGrafanaAdmin, hs.Index)
|
r.Get("/admin/users/create", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersCreate), hs.Index)
|
||||||
r.Get("/admin/users/edit/:id", reqGrafanaAdmin, hs.Index)
|
r.Get("/admin/users/edit/:id", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersRead), hs.Index)
|
||||||
r.Get("/admin/orgs", reqGrafanaAdmin, hs.Index)
|
r.Get("/admin/orgs", reqGrafanaAdmin, hs.Index)
|
||||||
r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, hs.Index)
|
r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, hs.Index)
|
||||||
r.Get("/admin/stats", reqGrafanaAdmin, hs.Index)
|
r.Get("/admin/stats", reqGrafanaAdmin, hs.Index)
|
||||||
r.Get("/admin/ldap", reqGrafanaAdmin, hs.Index)
|
r.Get("/admin/ldap", authorize(reqGrafanaAdmin, accesscontrol.ActionLDAPStatusRead), hs.Index)
|
||||||
|
|
||||||
r.Get("/styleguide", reqSignedIn, hs.Index)
|
r.Get("/styleguide", reqSignedIn, hs.Index)
|
||||||
|
|
||||||
@ -201,22 +201,23 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
|
|
||||||
// current org
|
// current org
|
||||||
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
|
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
|
||||||
orgRoute.Put("/", bind(dtos.UpdateOrgForm{}), routing.Wrap(UpdateOrgCurrent))
|
const orgScope = `org:current/users:{{ index . ":userId" }}`
|
||||||
orgRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), routing.Wrap(UpdateOrgAddressCurrent))
|
orgRoute.Put("/", reqOrgAdmin, bind(dtos.UpdateOrgForm{}), routing.Wrap(UpdateOrgCurrent))
|
||||||
orgRoute.Get("/users", routing.Wrap(hs.GetOrgUsersForCurrentOrg))
|
orgRoute.Put("/address", reqOrgAdmin, bind(dtos.UpdateOrgAddressForm{}), routing.Wrap(UpdateOrgAddressCurrent))
|
||||||
orgRoute.Post("/users", quota("user"), bind(models.AddOrgUserCommand{}), routing.Wrap(AddOrgUserToCurrentOrg))
|
orgRoute.Get("/users", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersRead, accesscontrol.ScopeOrgCurrentUsersAll), routing.Wrap(hs.GetOrgUsersForCurrentOrg))
|
||||||
orgRoute.Patch("/users/:userId", bind(models.UpdateOrgUserCommand{}), routing.Wrap(UpdateOrgUserForCurrentOrg))
|
orgRoute.Post("/users", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersAdd, accesscontrol.ScopeOrgCurrentUsersAll), quota("user"), bind(models.AddOrgUserCommand{}), routing.Wrap(AddOrgUserToCurrentOrg))
|
||||||
orgRoute.Delete("/users/:userId", routing.Wrap(RemoveOrgUserForCurrentOrg))
|
orgRoute.Patch("/users/:userId", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersRoleUpdate, orgScope), bind(models.UpdateOrgUserCommand{}), routing.Wrap(UpdateOrgUserForCurrentOrg))
|
||||||
|
orgRoute.Delete("/users/:userId", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersRemove, orgScope), routing.Wrap(RemoveOrgUserForCurrentOrg))
|
||||||
|
|
||||||
// invites
|
// invites
|
||||||
orgRoute.Get("/invites", routing.Wrap(GetPendingOrgInvites))
|
orgRoute.Get("/invites", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersAdd, accesscontrol.ScopeOrgCurrentUsersAll), routing.Wrap(GetPendingOrgInvites))
|
||||||
orgRoute.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), routing.Wrap(AddOrgInvite))
|
orgRoute.Post("/invites", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersAdd, accesscontrol.ScopeOrgCurrentUsersAll), quota("user"), bind(dtos.AddInviteForm{}), routing.Wrap(AddOrgInvite))
|
||||||
orgRoute.Patch("/invites/:code/revoke", routing.Wrap(RevokeInvite))
|
orgRoute.Patch("/invites/:code/revoke", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersAdd, accesscontrol.ScopeOrgCurrentUsersAll), routing.Wrap(RevokeInvite))
|
||||||
|
|
||||||
// prefs
|
// prefs
|
||||||
orgRoute.Get("/preferences", routing.Wrap(GetOrgPreferences))
|
orgRoute.Get("/preferences", reqOrgAdmin, routing.Wrap(GetOrgPreferences))
|
||||||
orgRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), routing.Wrap(UpdateOrgPreferences))
|
orgRoute.Put("/preferences", reqOrgAdmin, bind(dtos.UpdatePrefsCmd{}), routing.Wrap(UpdateOrgPreferences))
|
||||||
}, reqOrgAdmin)
|
})
|
||||||
|
|
||||||
// current org without requirement of user to be org admin
|
// current org without requirement of user to be org admin
|
||||||
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
|
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
|
||||||
@ -231,17 +232,19 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
|
|
||||||
// orgs (admin routes)
|
// orgs (admin routes)
|
||||||
apiRoute.Group("/orgs/:orgId", func(orgsRoute routing.RouteRegister) {
|
apiRoute.Group("/orgs/:orgId", func(orgsRoute routing.RouteRegister) {
|
||||||
orgsRoute.Get("/", routing.Wrap(GetOrgByID))
|
const orgScope = `org:{{ index . ":orgId" }}/users:*`
|
||||||
orgsRoute.Put("/", bind(dtos.UpdateOrgForm{}), routing.Wrap(UpdateOrg))
|
const orgUsersScope = `org:{{ index . ":orgId" }}/users:{{ index . ":userId" }}`
|
||||||
orgsRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), routing.Wrap(UpdateOrgAddress))
|
orgsRoute.Get("/", reqGrafanaAdmin, routing.Wrap(GetOrgByID))
|
||||||
orgsRoute.Delete("/", routing.Wrap(DeleteOrgByID))
|
orgsRoute.Put("/", reqGrafanaAdmin, bind(dtos.UpdateOrgForm{}), routing.Wrap(UpdateOrg))
|
||||||
orgsRoute.Get("/users", routing.Wrap(hs.GetOrgUsers))
|
orgsRoute.Put("/address", reqGrafanaAdmin, bind(dtos.UpdateOrgAddressForm{}), routing.Wrap(UpdateOrgAddress))
|
||||||
orgsRoute.Post("/users", bind(models.AddOrgUserCommand{}), routing.Wrap(AddOrgUser))
|
orgsRoute.Delete("/", reqGrafanaAdmin, routing.Wrap(DeleteOrgByID))
|
||||||
orgsRoute.Patch("/users/:userId", bind(models.UpdateOrgUserCommand{}), routing.Wrap(UpdateOrgUser))
|
orgsRoute.Get("/users", authorize(reqGrafanaAdmin, accesscontrol.ActionOrgUsersRead, orgScope), routing.Wrap(hs.GetOrgUsers))
|
||||||
orgsRoute.Delete("/users/:userId", routing.Wrap(RemoveOrgUser))
|
orgsRoute.Post("/users", authorize(reqGrafanaAdmin, accesscontrol.ActionOrgUsersAdd, orgScope), bind(models.AddOrgUserCommand{}), routing.Wrap(AddOrgUser))
|
||||||
orgsRoute.Get("/quotas", routing.Wrap(GetOrgQuotas))
|
orgsRoute.Patch("/users/:userId", authorize(reqGrafanaAdmin, accesscontrol.ActionOrgUsersRoleUpdate, orgUsersScope), bind(models.UpdateOrgUserCommand{}), routing.Wrap(UpdateOrgUser))
|
||||||
orgsRoute.Put("/quotas/:target", bind(models.UpdateOrgQuotaCmd{}), routing.Wrap(UpdateOrgQuota))
|
orgsRoute.Delete("/users/:userId", authorize(reqGrafanaAdmin, accesscontrol.ActionOrgUsersRemove, orgUsersScope), routing.Wrap(RemoveOrgUser))
|
||||||
}, reqGrafanaAdmin)
|
orgsRoute.Get("/quotas", reqGrafanaAdmin, routing.Wrap(GetOrgQuotas))
|
||||||
|
orgsRoute.Put("/quotas/:target", reqGrafanaAdmin, bind(models.UpdateOrgQuotaCmd{}), routing.Wrap(UpdateOrgQuota))
|
||||||
|
})
|
||||||
|
|
||||||
// orgs (admin routes)
|
// orgs (admin routes)
|
||||||
apiRoute.Group("/orgs/name/:name", func(orgsRoute routing.RouteRegister) {
|
apiRoute.Group("/orgs/name/:name", func(orgsRoute routing.RouteRegister) {
|
||||||
@ -425,19 +428,19 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
|
|
||||||
// admin api
|
// admin api
|
||||||
r.Group("/api/admin", func(adminRoute routing.RouteRegister) {
|
r.Group("/api/admin", func(adminRoute routing.RouteRegister) {
|
||||||
adminRoute.Get("/settings", routing.Wrap(AdminGetSettings))
|
adminRoute.Get("/settings", reqGrafanaAdmin, routing.Wrap(AdminGetSettings))
|
||||||
adminRoute.Get("/stats", routing.Wrap(AdminGetStats))
|
adminRoute.Get("/stats", reqGrafanaAdmin, routing.Wrap(AdminGetStats))
|
||||||
adminRoute.Post("/pause-all-alerts", bind(dtos.PauseAllAlertsCommand{}), routing.Wrap(PauseAllAlerts))
|
adminRoute.Post("/pause-all-alerts", reqGrafanaAdmin, bind(dtos.PauseAllAlertsCommand{}), routing.Wrap(PauseAllAlerts))
|
||||||
|
|
||||||
adminRoute.Post("/provisioning/dashboards/reload", routing.Wrap(hs.AdminProvisioningReloadDashboards))
|
adminRoute.Post("/provisioning/dashboards/reload", reqGrafanaAdmin, routing.Wrap(hs.AdminProvisioningReloadDashboards))
|
||||||
adminRoute.Post("/provisioning/plugins/reload", routing.Wrap(hs.AdminProvisioningReloadPlugins))
|
adminRoute.Post("/provisioning/plugins/reload", reqGrafanaAdmin, routing.Wrap(hs.AdminProvisioningReloadPlugins))
|
||||||
adminRoute.Post("/provisioning/datasources/reload", routing.Wrap(hs.AdminProvisioningReloadDatasources))
|
adminRoute.Post("/provisioning/datasources/reload", reqGrafanaAdmin, routing.Wrap(hs.AdminProvisioningReloadDatasources))
|
||||||
adminRoute.Post("/provisioning/notifications/reload", routing.Wrap(hs.AdminProvisioningReloadNotifications))
|
adminRoute.Post("/provisioning/notifications/reload", reqGrafanaAdmin, routing.Wrap(hs.AdminProvisioningReloadNotifications))
|
||||||
adminRoute.Post("/ldap/reload", routing.Wrap(hs.ReloadLDAPCfg))
|
adminRoute.Post("/ldap/reload", reqGrafanaAdmin, routing.Wrap(hs.ReloadLDAPCfg))
|
||||||
adminRoute.Post("/ldap/sync/:id", routing.Wrap(hs.PostSyncUserWithLDAP))
|
adminRoute.Post("/ldap/sync/:id", authorize(reqGrafanaAdmin, accesscontrol.ActionLDAPUsersSync), routing.Wrap(hs.PostSyncUserWithLDAP))
|
||||||
adminRoute.Get("/ldap/:username", routing.Wrap(hs.GetUserFromLDAP))
|
adminRoute.Get("/ldap/:username", authorize(reqGrafanaAdmin, accesscontrol.ActionLDAPUsersRead), routing.Wrap(hs.GetUserFromLDAP))
|
||||||
adminRoute.Get("/ldap/status", routing.Wrap(hs.GetLDAPStatus))
|
adminRoute.Get("/ldap/status", authorize(reqGrafanaAdmin, accesscontrol.ActionLDAPStatusRead), routing.Wrap(hs.GetLDAPStatus))
|
||||||
}, reqGrafanaAdmin)
|
})
|
||||||
|
|
||||||
// Administering users
|
// Administering users
|
||||||
r.Group("/api/admin/users", func(adminUserRoute routing.RouteRegister) {
|
r.Group("/api/admin/users", func(adminUserRoute routing.RouteRegister) {
|
||||||
|
@ -44,7 +44,7 @@ type CurrentUser struct {
|
|||||||
Permissions UserPermissionsMap `json:"permissions,omitempty"`
|
Permissions UserPermissionsMap `json:"permissions,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserPermissionsMap map[string]map[string]string
|
type UserPermissionsMap map[string]bool
|
||||||
|
|
||||||
type MetricRequest struct {
|
type MetricRequest struct {
|
||||||
From string `json:"from"`
|
From string `json:"from"`
|
||||||
|
@ -126,6 +126,7 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dtos.NavLink, error) {
|
func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dtos.NavLink, error) {
|
||||||
|
hasAccess := ac.HasAccess(hs.AccessControl, c)
|
||||||
navTree := []*dtos.NavLink{}
|
navTree := []*dtos.NavLink{}
|
||||||
|
|
||||||
if hasEditPerm {
|
if hasEditPerm {
|
||||||
@ -251,6 +252,9 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
Id: "datasources",
|
Id: "datasources",
|
||||||
Url: hs.Cfg.AppSubURL + "/datasources",
|
Url: hs.Cfg.AppSubURL + "/datasources",
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasAccess(ac.ReqOrgAdmin, ac.ActionOrgUsersRead, ac.ScopeOrgCurrentUsersAll) {
|
||||||
configNodes = append(configNodes, &dtos.NavLink{
|
configNodes = append(configNodes, &dtos.NavLink{
|
||||||
Text: "Users",
|
Text: "Users",
|
||||||
Id: "users",
|
Id: "users",
|
||||||
@ -361,11 +365,12 @@ func (hs *HTTPServer) buildAdminNavLinks(c *models.ReqContext) []*dtos.NavLink {
|
|||||||
adminNavLinks = append(adminNavLinks, &dtos.NavLink{
|
adminNavLinks = append(adminNavLinks, &dtos.NavLink{
|
||||||
Text: "Stats", Id: "server-stats", Url: hs.Cfg.AppSubURL + "/admin/stats", Icon: "graph-bar",
|
Text: "Stats", Id: "server-stats", Url: hs.Cfg.AppSubURL + "/admin/stats", Icon: "graph-bar",
|
||||||
})
|
})
|
||||||
if hs.Cfg.LDAPEnabled {
|
}
|
||||||
adminNavLinks = append(adminNavLinks, &dtos.NavLink{
|
|
||||||
Text: "LDAP", Id: "ldap", Url: hs.Cfg.AppSubURL + "/admin/ldap", Icon: "book",
|
if hs.Cfg.LDAPEnabled && hasAccess(ac.ReqGrafanaAdmin, ac.ActionLDAPStatusRead) {
|
||||||
})
|
adminNavLinks = append(adminNavLinks, &dtos.NavLink{
|
||||||
}
|
Text: "LDAP", Id: "ldap", Url: hs.Cfg.AppSubURL + "/admin/ldap", Icon: "book",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return adminNavLinks
|
return adminNavLinks
|
||||||
|
@ -37,20 +37,14 @@ var ReqGrafanaAdmin = func(c *models.ReqContext) bool {
|
|||||||
return c.IsGrafanaAdmin
|
return c.IsGrafanaAdmin
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildPermissionsMap(permissions []*Permission) map[string]map[string]string {
|
var ReqOrgAdmin = func(c *models.ReqContext) bool {
|
||||||
permissionsMap := make(map[string]map[string]string)
|
return c.OrgRole == models.ROLE_ADMIN
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildPermissionsMap(permissions []*Permission) map[string]bool {
|
||||||
|
permissionsMap := make(map[string]bool)
|
||||||
for _, p := range permissions {
|
for _, p := range permissions {
|
||||||
if item, ok := permissionsMap[p.Action]; ok {
|
permissionsMap[p.Action] = true
|
||||||
if _, ok := item[p.Scope]; !ok && p.Scope != "" {
|
|
||||||
permissionsMap[p.Action][p.Scope] = p.Scope
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newItem := make(map[string]string)
|
|
||||||
if p.Scope != "" {
|
|
||||||
newItem[p.Scope] = p.Scope
|
|
||||||
}
|
|
||||||
permissionsMap[p.Action] = newItem
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return permissionsMap
|
return permissionsMap
|
||||||
|
@ -42,6 +42,7 @@ func (p RoleDTO) Role() Role {
|
|||||||
const (
|
const (
|
||||||
// Permission actions
|
// Permission actions
|
||||||
|
|
||||||
|
// Users actions
|
||||||
ActionUsersRead = "users:read"
|
ActionUsersRead = "users:read"
|
||||||
ActionUsersWrite = "users:write"
|
ActionUsersWrite = "users:write"
|
||||||
ActionUsersTeamRead = "users.teams:read"
|
ActionUsersTeamRead = "users.teams:read"
|
||||||
@ -63,9 +64,23 @@ const (
|
|||||||
ActionUsersQuotasList = "users.quotas:list"
|
ActionUsersQuotasList = "users.quotas:list"
|
||||||
ActionUsersQuotasUpdate = "users.quotas:update"
|
ActionUsersQuotasUpdate = "users.quotas:update"
|
||||||
|
|
||||||
|
// Org actions
|
||||||
|
ActionOrgUsersRead = "org.users:read"
|
||||||
|
ActionOrgUsersAdd = "org.users:add"
|
||||||
|
ActionOrgUsersRemove = "org.users:remove"
|
||||||
|
ActionOrgUsersRoleUpdate = "org.users.role:update"
|
||||||
|
|
||||||
|
// LDAP actions
|
||||||
|
ActionLDAPUsersRead = "ldap.user:read"
|
||||||
|
ActionLDAPUsersSync = "ldap.user:sync"
|
||||||
|
ActionLDAPStatusRead = "ldap.status:read"
|
||||||
|
|
||||||
// Global Scopes
|
// Global Scopes
|
||||||
ScopeUsersAll = "users:*"
|
ScopeUsersAll = "users:*"
|
||||||
ScopeUsersSelf = "users:self"
|
ScopeUsersSelf = "users:self"
|
||||||
|
|
||||||
|
ScopeOrgAllUsersAll = "org:*/users:*"
|
||||||
|
ScopeOrgCurrentUsersAll = "org:current/users:*"
|
||||||
)
|
)
|
||||||
|
|
||||||
const RoleGrafanaAdmin = "Grafana Admin"
|
const RoleGrafanaAdmin = "Grafana Admin"
|
||||||
|
@ -29,6 +29,16 @@ var PredefinedRoles = map[string]RoleDTO{
|
|||||||
Action: ActionUsersQuotasList,
|
Action: ActionUsersQuotasList,
|
||||||
Scope: ScopeUsersAll,
|
Scope: ScopeUsersAll,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Action: ActionOrgUsersRead,
|
||||||
|
Scope: ScopeOrgAllUsersAll,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: ActionLDAPUsersRead,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: ActionLDAPStatusRead,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
usersAdminEdit: {
|
usersAdminEdit: {
|
||||||
@ -94,6 +104,26 @@ var PredefinedRoles = map[string]RoleDTO{
|
|||||||
Action: ActionUsersQuotasUpdate,
|
Action: ActionUsersQuotasUpdate,
|
||||||
Scope: ScopeUsersAll,
|
Scope: ScopeUsersAll,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Inherited from grafana:roles:users:admin:read
|
||||||
|
Action: ActionOrgUsersRead,
|
||||||
|
Scope: ScopeOrgAllUsersAll,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: ActionOrgUsersAdd,
|
||||||
|
Scope: ScopeOrgAllUsersAll,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: ActionOrgUsersRemove,
|
||||||
|
Scope: ScopeOrgAllUsersAll,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: ActionOrgUsersRoleUpdate,
|
||||||
|
Scope: ScopeOrgAllUsersAll,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: ActionLDAPUsersSync,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import config from '../../core/config';
|
|||||||
import { extend } from 'lodash';
|
import { extend } from 'lodash';
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import { rangeUtil } from '@grafana/data';
|
import { rangeUtil } from '@grafana/data';
|
||||||
import { AccessControlAction, AccessControlScope, UserPermission } from 'app/types';
|
import { AccessControlAction, UserPermission } from 'app/types';
|
||||||
|
|
||||||
export class User {
|
export class User {
|
||||||
id: number;
|
id: number;
|
||||||
@ -77,13 +77,13 @@ export class ContextSrv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Checks whether user has required permission
|
// Checks whether user has required permission
|
||||||
hasPermission(action: AccessControlAction, scope?: AccessControlScope): boolean {
|
hasPermission(action: AccessControlAction): boolean {
|
||||||
// Fallback if access control disabled
|
// Fallback if access control disabled
|
||||||
if (!config.featureToggles['accesscontrol']) {
|
if (!config.featureToggles['accesscontrol']) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !!(this.user.permissions?.[action] && (scope ? this.user.permissions[action][scope] : true));
|
return !!this.user.permissions?.[action];
|
||||||
}
|
}
|
||||||
|
|
||||||
isGrafanaVisible() {
|
isGrafanaVisible() {
|
||||||
|
@ -4,6 +4,7 @@ import { Select } from '@grafana/ui';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: OrgRole;
|
value: OrgRole;
|
||||||
|
disabled?: boolean;
|
||||||
onChange: (role: OrgRole) => void;
|
onChange: (role: OrgRole) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import { UserProfile } from './UserProfile';
|
|||||||
import { UserPermissions } from './UserPermissions';
|
import { UserPermissions } from './UserPermissions';
|
||||||
import { UserSessions } from './UserSessions';
|
import { UserSessions } from './UserSessions';
|
||||||
import { UserLdapSyncInfo } from './UserLdapSyncInfo';
|
import { UserLdapSyncInfo } from './UserLdapSyncInfo';
|
||||||
import { StoreState, UserDTO, UserOrg, UserSession, SyncInfo, UserAdminError } from 'app/types';
|
import { StoreState, UserDTO, UserOrg, UserSession, SyncInfo, UserAdminError, AccessControlAction } from 'app/types';
|
||||||
import {
|
import {
|
||||||
loadAdminUserPage,
|
loadAdminUserPage,
|
||||||
revokeSession,
|
revokeSession,
|
||||||
@ -27,6 +27,7 @@ import {
|
|||||||
} from './state/actions';
|
} from './state/actions';
|
||||||
import { UserOrgs } from './UserOrgs';
|
import { UserOrgs } from './UserOrgs';
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
|
import { contextSrv } from 'app/core/core';
|
||||||
|
|
||||||
interface Props extends GrafanaRouteComponentProps<{ id: string }> {
|
interface Props extends GrafanaRouteComponentProps<{ id: string }> {
|
||||||
navModel: NavModel;
|
navModel: NavModel;
|
||||||
@ -126,6 +127,8 @@ export class UserAdminPage extends PureComponent<Props, State> {
|
|||||||
const { navModel, user, orgs, sessions, ldapSyncInfo, isLoading } = this.props;
|
const { navModel, user, orgs, sessions, ldapSyncInfo, isLoading } = this.props;
|
||||||
// const { isLoading } = this.state;
|
// const { isLoading } = this.state;
|
||||||
const isLDAPUser = user && user.isExternal && user.authLabels && user.authLabels.includes('LDAP');
|
const isLDAPUser = user && user.isExternal && user.authLabels && user.authLabels.includes('LDAP');
|
||||||
|
const canReadSessions = contextSrv.hasPermission(AccessControlAction.UsersAuthTokenList);
|
||||||
|
const canReadLDAPStatus = contextSrv.hasPermission(AccessControlAction.LDAPStatusRead);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={navModel}>
|
<Page navModel={navModel}>
|
||||||
@ -140,7 +143,7 @@ export class UserAdminPage extends PureComponent<Props, State> {
|
|||||||
onUserEnable={this.onUserEnable}
|
onUserEnable={this.onUserEnable}
|
||||||
onPasswordChange={this.onPasswordChange}
|
onPasswordChange={this.onPasswordChange}
|
||||||
/>
|
/>
|
||||||
{isLDAPUser && config.licenseInfo.hasLicense && ldapSyncInfo && (
|
{isLDAPUser && config.licenseInfo.hasLicense && ldapSyncInfo && canReadLDAPStatus && (
|
||||||
<UserLdapSyncInfo ldapSyncInfo={ldapSyncInfo} user={user} onUserSync={this.onUserSync} />
|
<UserLdapSyncInfo ldapSyncInfo={ldapSyncInfo} user={user} onUserSync={this.onUserSync} />
|
||||||
)}
|
)}
|
||||||
<UserPermissions isGrafanaAdmin={user.isGrafanaAdmin} onGrafanaAdminChange={this.onGrafanaAdminChange} />
|
<UserPermissions isGrafanaAdmin={user.isGrafanaAdmin} onGrafanaAdminChange={this.onGrafanaAdminChange} />
|
||||||
@ -156,7 +159,7 @@ export class UserAdminPage extends PureComponent<Props, State> {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{sessions && (
|
{sessions && canReadSessions && (
|
||||||
<UserSessions
|
<UserSessions
|
||||||
sessions={sessions}
|
sessions={sessions}
|
||||||
onSessionRevoke={this.onSessionRevoke}
|
onSessionRevoke={this.onSessionRevoke}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { dateTimeFormat } from '@grafana/data';
|
import { dateTimeFormat } from '@grafana/data';
|
||||||
import { SyncInfo, UserDTO } from 'app/types';
|
import { AccessControlAction, SyncInfo, UserDTO } from 'app/types';
|
||||||
import { Button, LinkButton } from '@grafana/ui';
|
import { Button, LinkButton } from '@grafana/ui';
|
||||||
|
import { contextSrv } from 'app/core/core';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
ldapSyncInfo: SyncInfo;
|
ldapSyncInfo: SyncInfo;
|
||||||
@ -24,6 +25,8 @@ export class UserLdapSyncInfo extends PureComponent<Props, State> {
|
|||||||
const nextSyncSuccessful = ldapSyncInfo && ldapSyncInfo.nextSync;
|
const nextSyncSuccessful = ldapSyncInfo && ldapSyncInfo.nextSync;
|
||||||
const nextSyncTime = nextSyncSuccessful ? dateTimeFormat(ldapSyncInfo.nextSync, { format }) : '';
|
const nextSyncTime = nextSyncSuccessful ? dateTimeFormat(ldapSyncInfo.nextSync, { format }) : '';
|
||||||
const debugLDAPMappingURL = `${debugLDAPMappingBaseURL}?user=${user && user.login}`;
|
const debugLDAPMappingURL = `${debugLDAPMappingBaseURL}?user=${user && user.login}`;
|
||||||
|
const canReadLDAPUser = contextSrv.hasPermission(AccessControlAction.LDAPUsersRead);
|
||||||
|
const canSyncLDAPUser = contextSrv.hasPermission(AccessControlAction.LDAPUsersSync);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -56,12 +59,16 @@ export class UserLdapSyncInfo extends PureComponent<Props, State> {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div className="gf-form-button-row">
|
<div className="gf-form-button-row">
|
||||||
<Button variant="secondary" onClick={this.onUserSync}>
|
{canSyncLDAPUser && (
|
||||||
Sync user
|
<Button variant="secondary" onClick={this.onUserSync}>
|
||||||
</Button>
|
Sync user
|
||||||
<LinkButton variant="secondary" href={debugLDAPMappingURL}>
|
</Button>
|
||||||
Debug LDAP Mapping
|
)}
|
||||||
</LinkButton>
|
{canReadLDAPUser && (
|
||||||
|
<LinkButton variant="secondary" href={debugLDAPMappingURL}>
|
||||||
|
Debug LDAP Mapping
|
||||||
|
</LinkButton>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -4,11 +4,12 @@ import { hot } from 'react-hot-loader';
|
|||||||
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
|
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
|
||||||
import { NavModel } from '@grafana/data';
|
import { NavModel } from '@grafana/data';
|
||||||
import { Pagination, Tooltip, HorizontalGroup, stylesFactory, LinkButton, Input, Icon } from '@grafana/ui';
|
import { Pagination, Tooltip, HorizontalGroup, stylesFactory, LinkButton, Input, Icon } from '@grafana/ui';
|
||||||
import { StoreState, UserDTO } from '../../types';
|
import { AccessControlAction, StoreState, UserDTO } from '../../types';
|
||||||
import Page from 'app/core/components/Page/Page';
|
import Page from 'app/core/components/Page/Page';
|
||||||
import { getNavModel } from '../../core/selectors/navModel';
|
import { getNavModel } from '../../core/selectors/navModel';
|
||||||
import { fetchUsers, changeQuery, changePage } from './state/actions';
|
import { fetchUsers, changeQuery, changePage } from './state/actions';
|
||||||
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
|
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
|
||||||
|
import { contextSrv } from 'app/core/core';
|
||||||
|
|
||||||
interface OwnProps {}
|
interface OwnProps {}
|
||||||
|
|
||||||
@ -54,9 +55,11 @@ const UserListAdminPageUnConnected: React.FC<Props> = (props) => {
|
|||||||
onChange={(event) => changeQuery(event.currentTarget.value)}
|
onChange={(event) => changeQuery(event.currentTarget.value)}
|
||||||
prefix={<Icon name="search" />}
|
prefix={<Icon name="search" />}
|
||||||
/>
|
/>
|
||||||
<LinkButton href="admin/users/create" variant="primary">
|
{contextSrv.hasPermission(AccessControlAction.UsersCreate) && (
|
||||||
New user
|
<LinkButton href="admin/users/create" variant="primary">
|
||||||
</LinkButton>
|
New user
|
||||||
|
</LinkButton>
|
||||||
|
)}
|
||||||
</HorizontalGroup>
|
</HorizontalGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -12,9 +12,10 @@ import {
|
|||||||
withTheme,
|
withTheme,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import { Organization, OrgRole, UserOrg } from 'app/types';
|
import { AccessControlAction, Organization, OrgRole, UserOrg } from 'app/types';
|
||||||
import { OrgPicker, OrgSelectItem } from 'app/core/components/Select/OrgPicker';
|
import { OrgPicker, OrgSelectItem } from 'app/core/components/Select/OrgPicker';
|
||||||
import { OrgRolePicker } from './OrgRolePicker';
|
import { OrgRolePicker } from './OrgRolePicker';
|
||||||
|
import { contextSrv } from 'app/core/core';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
orgs: UserOrg[];
|
orgs: UserOrg[];
|
||||||
@ -43,6 +44,7 @@ export class UserOrgs extends PureComponent<Props, State> {
|
|||||||
const addToOrgContainerClass = css`
|
const addToOrgContainerClass = css`
|
||||||
margin-top: 0.8rem;
|
margin-top: 0.8rem;
|
||||||
`;
|
`;
|
||||||
|
const canAddToOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersAdd);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -63,9 +65,11 @@ export class UserOrgs extends PureComponent<Props, State> {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div className={addToOrgContainerClass}>
|
<div className={addToOrgContainerClass}>
|
||||||
<Button variant="secondary" onClick={this.showOrgAddModal(true)}>
|
{canAddToOrg && (
|
||||||
Add user to organization
|
<Button variant="secondary" onClick={this.showOrgAddModal(true)}>
|
||||||
</Button>
|
Add user to organization
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<AddToOrgModal isOpen={showAddOrgModal} onOrgAdd={onOrgAdd} onDismiss={this.showOrgAddModal(false)} />
|
<AddToOrgModal isOpen={showAddOrgModal} onOrgAdd={onOrgAdd} onDismiss={this.showOrgAddModal(false)} />
|
||||||
</div>
|
</div>
|
||||||
@ -131,6 +135,8 @@ class UnThemedOrgRow extends PureComponent<OrgRowProps, OrgRowState> {
|
|||||||
const { currentRole, isChangingRole } = this.state;
|
const { currentRole, isChangingRole } = this.state;
|
||||||
const styles = getOrgRowStyles(theme);
|
const styles = getOrgRowStyles(theme);
|
||||||
const labelClass = cx('width-16', styles.label);
|
const labelClass = cx('width-16', styles.label);
|
||||||
|
const canChangeRole = contextSrv.hasPermission(AccessControlAction.OrgUsersRoleUpdate);
|
||||||
|
const canRemoveFromOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersRemove);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
@ -144,26 +150,30 @@ class UnThemedOrgRow extends PureComponent<OrgRowProps, OrgRowState> {
|
|||||||
)}
|
)}
|
||||||
<td colSpan={1}>
|
<td colSpan={1}>
|
||||||
<div className="pull-right">
|
<div className="pull-right">
|
||||||
<ConfirmButton
|
{canChangeRole && (
|
||||||
confirmText="Save"
|
<ConfirmButton
|
||||||
onClick={this.onChangeRoleClick}
|
confirmText="Save"
|
||||||
onCancel={this.onCancelClick}
|
onClick={this.onChangeRoleClick}
|
||||||
onConfirm={this.onOrgRoleSave}
|
onCancel={this.onCancelClick}
|
||||||
>
|
onConfirm={this.onOrgRoleSave}
|
||||||
Change role
|
>
|
||||||
</ConfirmButton>
|
Change role
|
||||||
|
</ConfirmButton>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td colSpan={1}>
|
<td colSpan={1}>
|
||||||
<div className="pull-right">
|
<div className="pull-right">
|
||||||
<ConfirmButton
|
{canRemoveFromOrg && (
|
||||||
confirmText="Confirm removal"
|
<ConfirmButton
|
||||||
confirmVariant="destructive"
|
confirmText="Confirm removal"
|
||||||
onCancel={this.onCancelClick}
|
confirmVariant="destructive"
|
||||||
onConfirm={this.onOrgRemove}
|
onCancel={this.onCancelClick}
|
||||||
>
|
onConfirm={this.onOrgRemove}
|
||||||
Remove from organization
|
>
|
||||||
</ConfirmButton>
|
Remove from organization
|
||||||
|
</ConfirmButton>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { ConfirmButton, RadioButtonGroup, Icon } from '@grafana/ui';
|
import { ConfirmButton, RadioButtonGroup, Icon } from '@grafana/ui';
|
||||||
import { cx } from '@emotion/css';
|
import { cx } from '@emotion/css';
|
||||||
|
import { AccessControlAction } from 'app/types';
|
||||||
|
import { contextSrv } from 'app/core/core';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isGrafanaAdmin: boolean;
|
isGrafanaAdmin: boolean;
|
||||||
@ -49,6 +51,7 @@ export class UserPermissions extends PureComponent<Props, State> {
|
|||||||
const { isGrafanaAdmin } = this.props;
|
const { isGrafanaAdmin } = this.props;
|
||||||
const { isEditing, currentAdminOption } = this.state;
|
const { isEditing, currentAdminOption } = this.state;
|
||||||
const changeButtonContainerClass = cx('pull-right');
|
const changeButtonContainerClass = cx('pull-right');
|
||||||
|
const canChangePermissions = contextSrv.hasPermission(AccessControlAction.UsersPermissionsUpdate);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -80,15 +83,17 @@ export class UserPermissions extends PureComponent<Props, State> {
|
|||||||
)}
|
)}
|
||||||
<td>
|
<td>
|
||||||
<div className={changeButtonContainerClass}>
|
<div className={changeButtonContainerClass}>
|
||||||
<ConfirmButton
|
{canChangePermissions && (
|
||||||
className="pull-right"
|
<ConfirmButton
|
||||||
onClick={this.onChangeClick}
|
className="pull-right"
|
||||||
onConfirm={this.onGrafanaAdminChange}
|
onClick={this.onChangeClick}
|
||||||
onCancel={this.onCancelClick}
|
onConfirm={this.onGrafanaAdminChange}
|
||||||
confirmText="Change"
|
onCancel={this.onCancelClick}
|
||||||
>
|
confirmText="Change"
|
||||||
Change
|
>
|
||||||
</ConfirmButton>
|
Change
|
||||||
|
</ConfirmButton>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import React, { FC, PureComponent } from 'react';
|
import React, { FC, PureComponent } from 'react';
|
||||||
import { UserDTO } from 'app/types';
|
import { AccessControlAction, UserDTO } from 'app/types';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import { Button, ConfirmButton, ConfirmModal, Input, LegacyInputStatus, stylesFactory } from '@grafana/ui';
|
import { Button, ConfirmButton, ConfirmModal, Input, LegacyInputStatus, stylesFactory } from '@grafana/ui';
|
||||||
|
import { contextSrv } from 'app/core/core';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: UserDTO;
|
user: UserDTO;
|
||||||
@ -86,6 +87,12 @@ export class UserProfile extends PureComponent<Props, State> {
|
|||||||
const lockMessage = authSource ? `Synced via ${authSource}` : '';
|
const lockMessage = authSource ? `Synced via ${authSource}` : '';
|
||||||
const styles = getStyles(config.theme);
|
const styles = getStyles(config.theme);
|
||||||
|
|
||||||
|
const editLocked = user.isExternal || !contextSrv.hasPermission(AccessControlAction.UsersWrite);
|
||||||
|
const passwordChangeLocked = user.isExternal || !contextSrv.hasPermission(AccessControlAction.UsersPasswordUpdate);
|
||||||
|
const canDelete = contextSrv.hasPermission(AccessControlAction.UsersDelete);
|
||||||
|
const canDisable = contextSrv.hasPermission(AccessControlAction.UsersDisable);
|
||||||
|
const canEnable = contextSrv.hasPermission(AccessControlAction.UsersEnable);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3 className="page-heading">User information</h3>
|
<h3 className="page-heading">User information</h3>
|
||||||
@ -96,21 +103,21 @@ export class UserProfile extends PureComponent<Props, State> {
|
|||||||
<UserProfileRow
|
<UserProfileRow
|
||||||
label="Name"
|
label="Name"
|
||||||
value={user.name}
|
value={user.name}
|
||||||
locked={user.isExternal}
|
locked={editLocked}
|
||||||
lockMessage={lockMessage}
|
lockMessage={lockMessage}
|
||||||
onChange={this.onUserNameChange}
|
onChange={this.onUserNameChange}
|
||||||
/>
|
/>
|
||||||
<UserProfileRow
|
<UserProfileRow
|
||||||
label="Email"
|
label="Email"
|
||||||
value={user.email}
|
value={user.email}
|
||||||
locked={user.isExternal}
|
locked={editLocked}
|
||||||
lockMessage={lockMessage}
|
lockMessage={lockMessage}
|
||||||
onChange={this.onUserEmailChange}
|
onChange={this.onUserEmailChange}
|
||||||
/>
|
/>
|
||||||
<UserProfileRow
|
<UserProfileRow
|
||||||
label="Username"
|
label="Username"
|
||||||
value={user.login}
|
value={user.login}
|
||||||
locked={user.isExternal}
|
locked={editLocked}
|
||||||
lockMessage={lockMessage}
|
lockMessage={lockMessage}
|
||||||
onChange={this.onUserLoginChange}
|
onChange={this.onUserLoginChange}
|
||||||
/>
|
/>
|
||||||
@ -118,7 +125,7 @@ export class UserProfile extends PureComponent<Props, State> {
|
|||||||
label="Password"
|
label="Password"
|
||||||
value="********"
|
value="********"
|
||||||
inputType="password"
|
inputType="password"
|
||||||
locked={user.isExternal}
|
locked={passwordChangeLocked}
|
||||||
lockMessage={lockMessage}
|
lockMessage={lockMessage}
|
||||||
onChange={this.onPasswordChange}
|
onChange={this.onPasswordChange}
|
||||||
/>
|
/>
|
||||||
@ -126,34 +133,41 @@ export class UserProfile extends PureComponent<Props, State> {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.buttonRow}>
|
<div className={styles.buttonRow}>
|
||||||
<Button variant="destructive" onClick={this.showDeleteUserModal(true)}>
|
{canDelete && (
|
||||||
Delete user
|
<>
|
||||||
</Button>
|
<Button variant="destructive" onClick={this.showDeleteUserModal(true)}>
|
||||||
<ConfirmModal
|
Delete user
|
||||||
isOpen={showDeleteModal}
|
</Button>
|
||||||
title="Delete user"
|
<ConfirmModal
|
||||||
body="Are you sure you want to delete this user?"
|
isOpen={showDeleteModal}
|
||||||
confirmText="Delete user"
|
title="Delete user"
|
||||||
onConfirm={this.onUserDelete}
|
body="Are you sure you want to delete this user?"
|
||||||
onDismiss={this.showDeleteUserModal(false)}
|
confirmText="Delete user"
|
||||||
/>
|
onConfirm={this.onUserDelete}
|
||||||
{user.isDisabled ? (
|
onDismiss={this.showDeleteUserModal(false)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{user.isDisabled && canEnable && (
|
||||||
<Button variant="secondary" onClick={this.onUserEnable}>
|
<Button variant="secondary" onClick={this.onUserEnable}>
|
||||||
Enable user
|
Enable user
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
|
||||||
<Button variant="secondary" onClick={this.showDisableUserModal(true)}>
|
|
||||||
Disable user
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
<ConfirmModal
|
{!user.isDisabled && canDisable && (
|
||||||
isOpen={showDisableModal}
|
<>
|
||||||
title="Disable user"
|
<Button variant="secondary" onClick={this.showDisableUserModal(true)}>
|
||||||
body="Are you sure you want to disable this user?"
|
Disable user
|
||||||
confirmText="Disable user"
|
</Button>
|
||||||
onConfirm={this.onUserDisable}
|
<ConfirmModal
|
||||||
onDismiss={this.showDisableUserModal(false)}
|
isOpen={showDisableModal}
|
||||||
/>
|
title="Disable user"
|
||||||
|
body="Are you sure you want to disable this user?"
|
||||||
|
confirmText="Disable user"
|
||||||
|
onConfirm={this.onUserDisable}
|
||||||
|
onDismiss={this.showDisableUserModal(false)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { ConfirmButton, ConfirmModal, Button } from '@grafana/ui';
|
import { ConfirmButton, ConfirmModal, Button } from '@grafana/ui';
|
||||||
import { UserSession } from 'app/types';
|
import { AccessControlAction, UserSession } from 'app/types';
|
||||||
|
import { contextSrv } from 'app/core/core';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
sessions: UserSession[];
|
sessions: UserSession[];
|
||||||
@ -42,6 +43,8 @@ export class UserSessions extends PureComponent<Props, State> {
|
|||||||
margin-top: 0.8rem;
|
margin-top: 0.8rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const canLogout = contextSrv.hasPermission(AccessControlAction.UsersLogout);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3 className="page-heading">Sessions</h3>
|
<h3 className="page-heading">Sessions</h3>
|
||||||
@ -66,13 +69,15 @@ export class UserSessions extends PureComponent<Props, State> {
|
|||||||
<td>{`${session.browser} on ${session.os} ${session.osVersion}`}</td>
|
<td>{`${session.browser} on ${session.os} ${session.osVersion}`}</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="pull-right">
|
<div className="pull-right">
|
||||||
<ConfirmButton
|
{canLogout && (
|
||||||
confirmText="Confirm logout"
|
<ConfirmButton
|
||||||
confirmVariant="destructive"
|
confirmText="Confirm logout"
|
||||||
onConfirm={this.onSessionRevoke(session.id)}
|
confirmVariant="destructive"
|
||||||
>
|
onConfirm={this.onSessionRevoke(session.id)}
|
||||||
Force logout
|
>
|
||||||
</ConfirmButton>
|
Force logout
|
||||||
|
</ConfirmButton>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -81,7 +86,7 @@ export class UserSessions extends PureComponent<Props, State> {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div className={logoutFromAllDevicesClass}>
|
<div className={logoutFromAllDevicesClass}>
|
||||||
{sessions.length > 0 && (
|
{canLogout && sessions.length > 0 && (
|
||||||
<Button variant="secondary" onClick={this.showLogoutConfirmationModal(true)}>
|
<Button variant="secondary" onClick={this.showLogoutConfirmationModal(true)}>
|
||||||
Force logout from all devices
|
Force logout from all devices
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -10,7 +10,15 @@ import Page from 'app/core/components/Page/Page';
|
|||||||
import { LdapConnectionStatus } from './LdapConnectionStatus';
|
import { LdapConnectionStatus } from './LdapConnectionStatus';
|
||||||
import { LdapSyncInfo } from './LdapSyncInfo';
|
import { LdapSyncInfo } from './LdapSyncInfo';
|
||||||
import { LdapUserInfo } from './LdapUserInfo';
|
import { LdapUserInfo } from './LdapUserInfo';
|
||||||
import { AppNotificationSeverity, LdapError, LdapUser, StoreState, SyncInfo, LdapConnectionInfo } from 'app/types';
|
import {
|
||||||
|
AppNotificationSeverity,
|
||||||
|
LdapError,
|
||||||
|
LdapUser,
|
||||||
|
StoreState,
|
||||||
|
SyncInfo,
|
||||||
|
LdapConnectionInfo,
|
||||||
|
AccessControlAction,
|
||||||
|
} from 'app/types';
|
||||||
import {
|
import {
|
||||||
loadLdapState,
|
loadLdapState,
|
||||||
loadLdapSyncStatus,
|
loadLdapSyncStatus,
|
||||||
@ -19,6 +27,7 @@ import {
|
|||||||
clearUserMappingInfo,
|
clearUserMappingInfo,
|
||||||
} from '../state/actions';
|
} from '../state/actions';
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
|
import { contextSrv } from 'app/core/core';
|
||||||
|
|
||||||
interface Props extends GrafanaRouteComponentProps<{}, { username: string }> {
|
interface Props extends GrafanaRouteComponentProps<{}, { username: string }> {
|
||||||
navModel: NavModel;
|
navModel: NavModel;
|
||||||
@ -81,6 +90,7 @@ export class LdapPage extends PureComponent<Props, State> {
|
|||||||
render() {
|
render() {
|
||||||
const { ldapUser, userError, ldapError, ldapSyncInfo, ldapConnectionInfo, navModel, queryParams } = this.props;
|
const { ldapUser, userError, ldapError, ldapSyncInfo, ldapConnectionInfo, navModel, queryParams } = this.props;
|
||||||
const { isLoading } = this.state;
|
const { isLoading } = this.state;
|
||||||
|
const canReadLDAPUser = contextSrv.hasPermission(AccessControlAction.LDAPUsersRead);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={navModel}>
|
<Page navModel={navModel}>
|
||||||
@ -98,35 +108,39 @@ export class LdapPage extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
{config.licenseInfo.hasLicense && ldapSyncInfo && <LdapSyncInfo ldapSyncInfo={ldapSyncInfo} />}
|
{config.licenseInfo.hasLicense && ldapSyncInfo && <LdapSyncInfo ldapSyncInfo={ldapSyncInfo} />}
|
||||||
|
|
||||||
<h3 className="page-heading">Test user mapping</h3>
|
{canReadLDAPUser && (
|
||||||
<div className="gf-form-group">
|
<>
|
||||||
<form onSubmit={this.search} className="gf-form-inline">
|
<h3 className="page-heading">Test user mapping</h3>
|
||||||
<FormField
|
<div className="gf-form-group">
|
||||||
label="Username"
|
<form onSubmit={this.search} className="gf-form-inline">
|
||||||
labelWidth={8}
|
<FormField
|
||||||
inputWidth={30}
|
label="Username"
|
||||||
type="text"
|
labelWidth={8}
|
||||||
id="username"
|
inputWidth={30}
|
||||||
name="username"
|
type="text"
|
||||||
defaultValue={queryParams.username}
|
id="username"
|
||||||
/>
|
name="username"
|
||||||
<button type="submit" className="btn btn-primary">
|
defaultValue={queryParams.username}
|
||||||
Run
|
/>
|
||||||
</button>
|
<button type="submit" className="btn btn-primary">
|
||||||
</form>
|
Run
|
||||||
</div>
|
</button>
|
||||||
{userError && userError.title && (
|
</form>
|
||||||
<div className="gf-form-group">
|
</div>
|
||||||
<Alert
|
{userError && userError.title && (
|
||||||
title={userError.title}
|
<div className="gf-form-group">
|
||||||
severity={AppNotificationSeverity.Error}
|
<Alert
|
||||||
onRemove={this.onClearUserError}
|
title={userError.title}
|
||||||
>
|
severity={AppNotificationSeverity.Error}
|
||||||
{userError.body}
|
onRemove={this.onClearUserError}
|
||||||
</Alert>
|
>
|
||||||
</div>
|
{userError.body}
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{ldapUser && <LdapUserInfo ldapUser={ldapUser} showAttributeMapping={true} />}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{ldapUser && <LdapUserInfo ldapUser={ldapUser} showAttributeMapping={true} />}
|
|
||||||
</>
|
</>
|
||||||
</Page.Contents>
|
</Page.Contents>
|
||||||
</Page>
|
</Page>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
|
import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
|
||||||
import { getBackendSrv, locationService } from '@grafana/runtime';
|
import { getBackendSrv, locationService } from '@grafana/runtime';
|
||||||
import { ThunkResult, LdapUser, UserSession, UserDTO } from 'app/types';
|
import { ThunkResult, LdapUser, UserSession, UserDTO, AccessControlAction } from 'app/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
userAdminPageLoadedAction,
|
userAdminPageLoadedAction,
|
||||||
@ -21,6 +21,7 @@ import {
|
|||||||
pageChanged,
|
pageChanged,
|
||||||
} from './reducers';
|
} from './reducers';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
import { contextSrv } from 'app/core/core';
|
||||||
|
|
||||||
// UserAdminPage
|
// UserAdminPage
|
||||||
|
|
||||||
@ -134,6 +135,10 @@ export function deleteOrgUser(userId: number, orgId: number): ThunkResult<void>
|
|||||||
|
|
||||||
export function loadUserSessions(userId: number): ThunkResult<void> {
|
export function loadUserSessions(userId: number): ThunkResult<void> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
|
if (!contextSrv.hasPermission(AccessControlAction.UsersAuthTokenList)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const tokens = await getBackendSrv().get(`/api/admin/users/${userId}/auth-tokens`);
|
const tokens = await getBackendSrv().get(`/api/admin/users/${userId}/auth-tokens`);
|
||||||
tokens.reverse();
|
tokens.reverse();
|
||||||
const sessions = tokens.map((session: UserSession) => {
|
const sessions = tokens.map((session: UserSession) => {
|
||||||
@ -174,7 +179,8 @@ export function revokeAllSessions(userId: number): ThunkResult<void> {
|
|||||||
export function loadLdapSyncStatus(): ThunkResult<void> {
|
export function loadLdapSyncStatus(): ThunkResult<void> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
// Available only in enterprise
|
// Available only in enterprise
|
||||||
if (config.licenseInfo.hasLicense) {
|
const canReadLDAPStatus = contextSrv.hasPermission(AccessControlAction.LDAPStatusRead);
|
||||||
|
if (config.licenseInfo.hasLicense && canReadLDAPStatus) {
|
||||||
const syncStatus = await getBackendSrv().get(`/api/admin/ldap-sync-status`);
|
const syncStatus = await getBackendSrv().get(`/api/admin/ldap-sync-status`);
|
||||||
dispatch(ldapSyncStatusLoadedAction(syncStatus));
|
dispatch(ldapSyncStatusLoadedAction(syncStatus));
|
||||||
}
|
}
|
||||||
@ -192,6 +198,10 @@ export function syncLdapUser(userId: number): ThunkResult<void> {
|
|||||||
|
|
||||||
export function loadLdapState(): ThunkResult<void> {
|
export function loadLdapState(): ThunkResult<void> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
|
if (!contextSrv.hasPermission(AccessControlAction.LDAPStatusRead)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const connectionInfo = await getBackendSrv().get(`/api/admin/ldap/status`);
|
const connectionInfo = await getBackendSrv().get(`/api/admin/ldap/status`);
|
||||||
dispatch(ldapConnectionInfoLoadedAction(connectionInfo));
|
dispatch(ldapConnectionInfoLoadedAction(connectionInfo));
|
||||||
|
@ -4,6 +4,12 @@ import { Props, UsersActionBar } from './UsersActionBar';
|
|||||||
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
||||||
import { setUsersSearchQuery } from './state/reducers';
|
import { setUsersSearchQuery } from './state/reducers';
|
||||||
|
|
||||||
|
jest.mock('app/core/core', () => ({
|
||||||
|
contextSrv: {
|
||||||
|
hasPermission: () => true,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
const setup = (propOverrides?: object) => {
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
|
@ -4,6 +4,8 @@ import { setUsersSearchQuery } from './state/reducers';
|
|||||||
import { getInviteesCount, getUsersSearchQuery } from './state/selectors';
|
import { getInviteesCount, getUsersSearchQuery } from './state/selectors';
|
||||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||||
import { RadioButtonGroup, LinkButton } from '@grafana/ui';
|
import { RadioButtonGroup, LinkButton } from '@grafana/ui';
|
||||||
|
import { contextSrv } from 'app/core/core';
|
||||||
|
import { AccessControlAction } from 'app/types';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
@ -32,6 +34,7 @@ export class UsersActionBar extends PureComponent<Props> {
|
|||||||
{ label: 'Users', value: 'users' },
|
{ label: 'Users', value: 'users' },
|
||||||
{ label: `Pending Invites (${pendingInvitesCount})`, value: 'invites' },
|
{ label: `Pending Invites (${pendingInvitesCount})`, value: 'invites' },
|
||||||
];
|
];
|
||||||
|
const canAddToOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersAdd);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-action-bar">
|
<div className="page-action-bar">
|
||||||
@ -47,7 +50,7 @@ export class UsersActionBar extends PureComponent<Props> {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="page-action-bar__spacer" />
|
<div className="page-action-bar__spacer" />
|
||||||
{canInvite && <LinkButton href="org/users/invite">Invite</LinkButton>}
|
{canInvite && canAddToOrg && <LinkButton href="org/users/invite">Invite</LinkButton>}
|
||||||
{externalUserMngLinkUrl && (
|
{externalUserMngLinkUrl && (
|
||||||
<LinkButton href={externalUserMngLinkUrl} target="_blank" rel="noopener">
|
<LinkButton href={externalUserMngLinkUrl} target="_blank" rel="noopener">
|
||||||
{externalUserMngLinkName}
|
{externalUserMngLinkName}
|
||||||
|
@ -5,6 +5,12 @@ import { OrgUser } from 'app/types';
|
|||||||
import { getMockUsers } from './__mocks__/userMocks';
|
import { getMockUsers } from './__mocks__/userMocks';
|
||||||
import { ConfirmModal } from '@grafana/ui';
|
import { ConfirmModal } from '@grafana/ui';
|
||||||
|
|
||||||
|
jest.mock('app/core/core', () => ({
|
||||||
|
contextSrv: {
|
||||||
|
hasPermission: () => true,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
const setup = (propOverrides?: object) => {
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
users: [] as OrgUser[],
|
users: [] as OrgUser[],
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React, { FC, useState } from 'react';
|
import React, { FC, useState } from 'react';
|
||||||
import { OrgUser } from 'app/types';
|
import { AccessControlAction, OrgUser } from 'app/types';
|
||||||
import { OrgRolePicker } from '../admin/OrgRolePicker';
|
import { OrgRolePicker } from '../admin/OrgRolePicker';
|
||||||
import { Button, ConfirmModal } from '@grafana/ui';
|
import { Button, ConfirmModal } from '@grafana/ui';
|
||||||
import { OrgRole } from '@grafana/data';
|
import { OrgRole } from '@grafana/data';
|
||||||
|
import { contextSrv } from 'app/core/core';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
users: OrgUser[];
|
users: OrgUser[];
|
||||||
@ -12,6 +13,8 @@ export interface Props {
|
|||||||
|
|
||||||
const UsersTable: FC<Props> = (props) => {
|
const UsersTable: FC<Props> = (props) => {
|
||||||
const { users, onRoleChange, onRemoveUser } = props;
|
const { users, onRoleChange, onRemoveUser } = props;
|
||||||
|
const canUpdateRole = contextSrv.hasPermission(AccessControlAction.OrgUsersRoleUpdate);
|
||||||
|
const canRemoveFromOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersRemove);
|
||||||
|
|
||||||
const [showRemoveModal, setShowRemoveModal] = useState<string | boolean>(false);
|
const [showRemoveModal, setShowRemoveModal] = useState<string | boolean>(false);
|
||||||
return (
|
return (
|
||||||
@ -53,22 +56,28 @@ const UsersTable: FC<Props> = (props) => {
|
|||||||
<td className="width-1">{user.lastSeenAtAge}</td>
|
<td className="width-1">{user.lastSeenAtAge}</td>
|
||||||
|
|
||||||
<td className="width-8">
|
<td className="width-8">
|
||||||
<OrgRolePicker value={user.role} onChange={(newRole) => onRoleChange(newRole, user)} />
|
<OrgRolePicker
|
||||||
</td>
|
value={user.role}
|
||||||
|
disabled={!canUpdateRole}
|
||||||
<td>
|
onChange={(newRole) => onRoleChange(newRole, user)}
|
||||||
<Button size="sm" variant="destructive" onClick={() => setShowRemoveModal(user.login)} icon="times" />
|
|
||||||
<ConfirmModal
|
|
||||||
body={`Are you sure you want to delete user ${user.login}?`}
|
|
||||||
confirmText="Delete"
|
|
||||||
title="Delete"
|
|
||||||
onDismiss={() => setShowRemoveModal(false)}
|
|
||||||
isOpen={user.login === showRemoveModal}
|
|
||||||
onConfirm={() => {
|
|
||||||
onRemoveUser(user);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
{canRemoveFromOrg && (
|
||||||
|
<td>
|
||||||
|
<Button size="sm" variant="destructive" onClick={() => setShowRemoveModal(user.login)} icon="times" />
|
||||||
|
<ConfirmModal
|
||||||
|
body={`Are you sure you want to delete user ${user.login}?`}
|
||||||
|
confirmText="Delete"
|
||||||
|
title="Delete"
|
||||||
|
onDismiss={() => setShowRemoveModal(false)}
|
||||||
|
isOpen={user.login === showRemoveModal}
|
||||||
|
onConfirm={() => {
|
||||||
|
onRemoveUser(user);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -115,6 +115,7 @@ exports[`Render should render users table 1`] = `
|
|||||||
className="width-8"
|
className="width-8"
|
||||||
>
|
>
|
||||||
<OrgRolePicker
|
<OrgRolePicker
|
||||||
|
disabled={false}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
value="Admin"
|
value="Admin"
|
||||||
/>
|
/>
|
||||||
@ -184,6 +185,7 @@ exports[`Render should render users table 1`] = `
|
|||||||
className="width-8"
|
className="width-8"
|
||||||
>
|
>
|
||||||
<OrgRolePicker
|
<OrgRolePicker
|
||||||
|
disabled={false}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
value="Admin"
|
value="Admin"
|
||||||
/>
|
/>
|
||||||
@ -253,6 +255,7 @@ exports[`Render should render users table 1`] = `
|
|||||||
className="width-8"
|
className="width-8"
|
||||||
>
|
>
|
||||||
<OrgRolePicker
|
<OrgRolePicker
|
||||||
|
disabled={false}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
value="Admin"
|
value="Admin"
|
||||||
/>
|
/>
|
||||||
@ -322,6 +325,7 @@ exports[`Render should render users table 1`] = `
|
|||||||
className="width-8"
|
className="width-8"
|
||||||
>
|
>
|
||||||
<OrgRolePicker
|
<OrgRolePicker
|
||||||
|
disabled={false}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
value="Admin"
|
value="Admin"
|
||||||
/>
|
/>
|
||||||
@ -391,6 +395,7 @@ exports[`Render should render users table 1`] = `
|
|||||||
className="width-8"
|
className="width-8"
|
||||||
>
|
>
|
||||||
<OrgRolePicker
|
<OrgRolePicker
|
||||||
|
disabled={false}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
value="Admin"
|
value="Admin"
|
||||||
/>
|
/>
|
||||||
@ -460,6 +465,7 @@ exports[`Render should render users table 1`] = `
|
|||||||
className="width-8"
|
className="width-8"
|
||||||
>
|
>
|
||||||
<OrgRolePicker
|
<OrgRolePicker
|
||||||
|
disabled={false}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
value="Admin"
|
value="Admin"
|
||||||
/>
|
/>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { ThunkResult } from '../../../types';
|
import { AccessControlAction, ThunkResult } from '../../../types';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import { OrgUser } from 'app/types';
|
import { OrgUser } from 'app/types';
|
||||||
import { inviteesLoaded, usersLoaded } from './reducers';
|
import { inviteesLoaded, usersLoaded } from './reducers';
|
||||||
|
import { contextSrv } from 'app/core/core';
|
||||||
|
|
||||||
export function loadUsers(): ThunkResult<void> {
|
export function loadUsers(): ThunkResult<void> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
@ -12,6 +13,10 @@ export function loadUsers(): ThunkResult<void> {
|
|||||||
|
|
||||||
export function loadInvitees(): ThunkResult<void> {
|
export function loadInvitees(): ThunkResult<void> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
|
if (!contextSrv.hasPermission(AccessControlAction.OrgUsersAdd)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const invitees = await getBackendSrv().get('/api/org/invites');
|
const invitees = await getBackendSrv().get('/api/org/invites');
|
||||||
dispatch(inviteesLoaded(invitees));
|
dispatch(inviteesLoaded(invitees));
|
||||||
};
|
};
|
||||||
|
@ -8,11 +8,6 @@ export type UserPermission = {
|
|||||||
[key: string]: { [key: string]: string };
|
[key: string]: { [key: string]: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AccessControlPermission {
|
|
||||||
action: AccessControlAction;
|
|
||||||
scope?: AccessControlScope;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permission actions
|
// Permission actions
|
||||||
export enum AccessControlAction {
|
export enum AccessControlAction {
|
||||||
UsersRead = 'users:read',
|
UsersRead = 'users:read',
|
||||||
@ -20,19 +15,22 @@ export enum AccessControlAction {
|
|||||||
UsersTeamRead = 'users.teams:read',
|
UsersTeamRead = 'users.teams:read',
|
||||||
UsersAuthTokenList = 'users.authtoken:list',
|
UsersAuthTokenList = 'users.authtoken:list',
|
||||||
UsersAuthTokenUpdate = 'users.authtoken:update',
|
UsersAuthTokenUpdate = 'users.authtoken:update',
|
||||||
UsersPasswordUpdate = 'users.password.update',
|
UsersPasswordUpdate = 'users.password:update',
|
||||||
UsersDelete = 'users:delete',
|
UsersDelete = 'users:delete',
|
||||||
UsersCreate = 'users:create',
|
UsersCreate = 'users:create',
|
||||||
UsersEnable = 'users:enable',
|
UsersEnable = 'users:enable',
|
||||||
UsersDisable = 'users:disable',
|
UsersDisable = 'users:disable',
|
||||||
UsersPermissionsUpdate = 'users.permissions.update',
|
UsersPermissionsUpdate = 'users.permissions:update',
|
||||||
UsersLogout = 'users:logout',
|
UsersLogout = 'users:logout',
|
||||||
UsersQuotasList = 'users.quotas:list',
|
UsersQuotasList = 'users.quotas:list',
|
||||||
UsersQuotasUpdate = 'users.quotas:update',
|
UsersQuotasUpdate = 'users.quotas:update',
|
||||||
}
|
|
||||||
|
|
||||||
// Global Scopes
|
OrgUsersRead = 'org.users:read',
|
||||||
export enum AccessControlScope {
|
OrgUsersAdd = 'org.users:add',
|
||||||
UsersAll = 'users:*',
|
OrgUsersRemove = 'org.users:remove',
|
||||||
UsersSelf = 'users:self',
|
OrgUsersRoleUpdate = 'org.users.role:update',
|
||||||
|
|
||||||
|
LDAPUsersRead = 'ldap.user:read',
|
||||||
|
LDAPUsersSync = 'ldap.user:sync',
|
||||||
|
LDAPStatusRead = 'ldap.status:read',
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user