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