mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
LDAP: Add skip_org_role_sync
configuration option (#56679)
* LDAP: Add skip_org_role_sync option * Document the new config option * Nit on docs * Update docs/sources/setup-grafana/configure-security/configure-authentication/ldap.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * Docs suggestions Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Co-authored-by: Jguer <joao.guerreiro@grafana.com> * Add test, Fix disabled user when no role Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Co-authored-by: Jguer <joao.guerreiro@grafana.com>
This commit is contained in:
parent
72b9555487
commit
10c080dad1
@ -632,6 +632,7 @@ allow_assign_grafana_admin = false
|
||||
enabled = false
|
||||
config_file = /etc/grafana/ldap.toml
|
||||
allow_sign_up = true
|
||||
skip_org_role_sync = false
|
||||
|
||||
# LDAP background sync (Enterprise only)
|
||||
# At 1 am every day
|
||||
|
@ -624,6 +624,8 @@
|
||||
;enabled = false
|
||||
;config_file = /etc/grafana/ldap.toml
|
||||
;allow_sign_up = true
|
||||
# prevent synchronizing ldap users organization roles
|
||||
;skip_org_role_sync = false
|
||||
|
||||
# LDAP background sync (Enterprise only)
|
||||
# At 1 am every day
|
||||
|
@ -28,7 +28,7 @@ This means that you should be able to configure LDAP integration using any compl
|
||||
In order to use LDAP integration you'll first need to enable LDAP in the [main config file]({{< relref "../../configure-grafana/" >}}) as well as specify the path to the LDAP
|
||||
specific configuration file (default: `/etc/grafana/ldap.toml`).
|
||||
|
||||
```bash
|
||||
```ini
|
||||
[auth.ldap]
|
||||
# Set to `true` to enable LDAP integration (default: `false`)
|
||||
enabled = true
|
||||
@ -36,11 +36,32 @@ enabled = true
|
||||
# Path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`)
|
||||
config_file = /etc/grafana/ldap.toml
|
||||
|
||||
# Allow sign up should almost always be true (default) to allow new Grafana users to be created (if LDAP authentication is ok). If set to
|
||||
# false only pre-existing Grafana users will be able to login (if LDAP authentication is ok).
|
||||
# Allow sign-up should be `true` (default) to allow Grafana to create users on successful LDAP authentication.
|
||||
# If set to `false` only already existing Grafana users will be able to login.
|
||||
allow_sign_up = true
|
||||
```
|
||||
|
||||
## Disable org role synchronization
|
||||
|
||||
If you use LDAP to authenticate users but don't use role mapping, and prefer to manually assign organizations
|
||||
and roles, you can use the `skip_org_role_sync` configuration option.
|
||||
|
||||
```ini
|
||||
[auth.ldap]
|
||||
# Set to `true` to enable LDAP integration (default: `false`)
|
||||
enabled = true
|
||||
|
||||
# Path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`)
|
||||
config_file = /etc/grafana/ldap.toml
|
||||
|
||||
# Allow sign-up should be `true` (default) to allow Grafana to create users on successful LDAP authentication.
|
||||
# If set to `false` only already existing Grafana users will be able to login.
|
||||
allow_sign_up = true
|
||||
|
||||
# Prevent synchronizing ldap users organization roles
|
||||
skip_org_role_sync = true
|
||||
```
|
||||
|
||||
## Grafana LDAP Configuration
|
||||
|
||||
Depending on which LDAP server you're using and how that's configured your Grafana LDAP configuration may vary.
|
||||
|
@ -220,5 +220,6 @@ export interface GrafanaConfig {
|
||||
export interface AuthSettings {
|
||||
OAuthSkipOrgRoleUpdateSync?: boolean;
|
||||
SAMLSkipOrgRoleSync?: boolean;
|
||||
LDAPSkipOrgRoleSync?: boolean;
|
||||
DisableSyncLock?: boolean;
|
||||
}
|
||||
|
@ -146,6 +146,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
||||
"auth": map[string]interface{}{
|
||||
"OAuthSkipOrgRoleUpdateSync": hs.Cfg.OAuthSkipOrgRoleUpdateSync,
|
||||
"SAMLSkipOrgRoleSync": hs.Cfg.SectionWithEnvOverrides("auth.saml").Key("skip_org_role_sync").MustBool(false),
|
||||
"LDAPSkipOrgRoleSync": hs.Cfg.LDAPSkipOrgRoleSync,
|
||||
"DisableSyncLock": hs.Cfg.DisableSyncLock,
|
||||
},
|
||||
"buildInfo": map[string]interface{}{
|
||||
|
@ -363,7 +363,8 @@ func (server *Server) users(logins []string) (
|
||||
// If there are no ldap group mappings access is true
|
||||
// otherwise a single group must match
|
||||
func (server *Server) validateGrafanaUser(user *models.ExternalUserInfo) error {
|
||||
if len(server.Config.Groups) > 0 && (len(user.OrgRoles) == 0 && (user.IsGrafanaAdmin == nil || !*user.IsGrafanaAdmin)) {
|
||||
if !SkipOrgRoleSync() && len(server.Config.Groups) > 0 &&
|
||||
(len(user.OrgRoles) == 0 && (user.IsGrafanaAdmin == nil || !*user.IsGrafanaAdmin)) {
|
||||
server.log.Error(
|
||||
"User does not belong in any of the specified LDAP groups",
|
||||
"username", user.Login,
|
||||
@ -446,6 +447,11 @@ func (server *Server) buildGrafanaUser(user *ldap.Entry) (*models.ExternalUserIn
|
||||
OrgRoles: map[int64]org.RoleType{},
|
||||
}
|
||||
|
||||
// Skipping org role sync
|
||||
if SkipOrgRoleSync() {
|
||||
return extUser, nil
|
||||
}
|
||||
|
||||
for _, group := range server.Config.Groups {
|
||||
// only use the first match for each org
|
||||
if extUser.OrgRoles[group.OrgId] != "" {
|
||||
|
@ -10,6 +10,8 @@ import (
|
||||
"gopkg.in/ldap.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models/roletype"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
@ -221,6 +223,122 @@ func TestServer_Users(t *testing.T) {
|
||||
require.Len(t, res, 1)
|
||||
assert.Equal(t, "Grot the First", res[0].Name)
|
||||
})
|
||||
|
||||
t.Run("org role mapping", func(t *testing.T) {
|
||||
conn := &MockConnection{}
|
||||
|
||||
usersOU := "ou=users,dc=example,dc=org"
|
||||
grootDN := "dn=groot," + usersOU
|
||||
grootSearch := ldap.SearchResult{Entries: []*ldap.Entry{{DN: grootDN,
|
||||
Attributes: []*ldap.EntryAttribute{
|
||||
{Name: "username", Values: []string{"groot"}},
|
||||
{Name: "name", Values: []string{"I am Groot"}},
|
||||
}}}}
|
||||
peterDN := "dn=peter," + usersOU
|
||||
peterSearch := ldap.SearchResult{Entries: []*ldap.Entry{{DN: peterDN,
|
||||
Attributes: []*ldap.EntryAttribute{
|
||||
{Name: "username", Values: []string{"peter"}},
|
||||
{Name: "name", Values: []string{"Peter"}},
|
||||
}}}}
|
||||
groupsOU := "ou=groups,dc=example,dc=org"
|
||||
creaturesDN := "dn=creatures," + groupsOU
|
||||
grootGroups := ldap.SearchResult{Entries: []*ldap.Entry{{DN: creaturesDN,
|
||||
Attributes: []*ldap.EntryAttribute{
|
||||
{Name: "member", Values: []string{grootDN}},
|
||||
}}},
|
||||
}
|
||||
humansDN := "dn=humans," + groupsOU
|
||||
peterGroups := ldap.SearchResult{Entries: []*ldap.Entry{{DN: humansDN,
|
||||
Attributes: []*ldap.EntryAttribute{
|
||||
{Name: "member", Values: []string{peterDN}},
|
||||
}}},
|
||||
}
|
||||
|
||||
conn.setSearchFunc(func(request *ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
switch request.BaseDN {
|
||||
case usersOU:
|
||||
switch request.Filter {
|
||||
case "(|(username=groot))":
|
||||
return &grootSearch, nil
|
||||
case "(|(username=peter))":
|
||||
return &peterSearch, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("test case not defined for user filter: '%s'", request.Filter)
|
||||
}
|
||||
case groupsOU:
|
||||
switch request.Filter {
|
||||
case "(member=groot)":
|
||||
return &grootGroups, nil
|
||||
case "(member=peter)":
|
||||
return &peterGroups, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("test case not defined for group filter: '%s'", request.Filter)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("test case not defined for baseDN: '%s'", request.BaseDN)
|
||||
}
|
||||
})
|
||||
|
||||
server := &Server{
|
||||
Config: &ServerConfig{
|
||||
Attr: AttributeMap{
|
||||
Username: "username",
|
||||
Name: "name",
|
||||
},
|
||||
SearchBaseDNs: []string{usersOU},
|
||||
SearchFilter: "(username=%s)",
|
||||
GroupSearchFilter: "(member=%s)",
|
||||
GroupSearchBaseDNs: []string{groupsOU},
|
||||
Groups: []*GroupToOrgRole{
|
||||
{
|
||||
GroupDN: creaturesDN,
|
||||
OrgId: 2,
|
||||
IsGrafanaAdmin: new(bool),
|
||||
OrgRole: "Admin",
|
||||
},
|
||||
},
|
||||
},
|
||||
Connection: conn,
|
||||
log: log.New("test-logger"),
|
||||
}
|
||||
|
||||
t.Run("disable user with no mapping", func(t *testing.T) {
|
||||
res, err := server.Users([]string{"peter"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, "Peter", res[0].Name)
|
||||
require.ElementsMatch(t, res[0].Groups, []string{humansDN})
|
||||
require.Empty(t, res[0].OrgRoles)
|
||||
require.True(t, res[0].IsDisabled)
|
||||
})
|
||||
t.Run("skip org role sync", func(t *testing.T) {
|
||||
backup := setting.LDAPSkipOrgRoleSync
|
||||
defer func() {
|
||||
setting.LDAPSkipOrgRoleSync = backup
|
||||
}()
|
||||
setting.LDAPSkipOrgRoleSync = true
|
||||
|
||||
res, err := server.Users([]string{"groot"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, "I am Groot", res[0].Name)
|
||||
require.ElementsMatch(t, res[0].Groups, []string{creaturesDN})
|
||||
require.Empty(t, res[0].OrgRoles)
|
||||
require.False(t, res[0].IsDisabled)
|
||||
})
|
||||
t.Run("sync org role", func(t *testing.T) {
|
||||
res, err := server.Users([]string{"groot"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, "I am Groot", res[0].Name)
|
||||
require.ElementsMatch(t, res[0].Groups, []string{creaturesDN})
|
||||
require.Len(t, res[0].OrgRoles, 1)
|
||||
role, mappingExist := res[0].OrgRoles[2]
|
||||
require.True(t, mappingExist)
|
||||
require.Equal(t, roletype.RoleAdmin, role)
|
||||
require.False(t, res[0].IsDisabled)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestServer_UserBind(t *testing.T) {
|
||||
|
@ -76,6 +76,10 @@ func IsEnabled() bool {
|
||||
return setting.LDAPEnabled
|
||||
}
|
||||
|
||||
func SkipOrgRoleSync() bool {
|
||||
return setting.LDAPSkipOrgRoleSync
|
||||
}
|
||||
|
||||
// ReloadConfig reads the config from the disk and caches it.
|
||||
func ReloadConfig() error {
|
||||
if !IsEnabled() {
|
||||
|
@ -147,6 +147,7 @@ var (
|
||||
|
||||
// LDAP
|
||||
LDAPEnabled bool
|
||||
LDAPSkipOrgRoleSync bool
|
||||
LDAPConfigFile string
|
||||
LDAPSyncCron string
|
||||
LDAPAllowSignup bool
|
||||
@ -413,8 +414,9 @@ type Cfg struct {
|
||||
FeedbackLinksEnabled bool
|
||||
|
||||
// LDAP
|
||||
LDAPEnabled bool
|
||||
LDAPAllowSignup bool
|
||||
LDAPEnabled bool
|
||||
LDAPSkipOrgRoleSync bool
|
||||
LDAPAllowSignup bool
|
||||
|
||||
Quota QuotaSettings
|
||||
|
||||
@ -1131,6 +1133,8 @@ func (cfg *Cfg) readLDAPConfig() {
|
||||
LDAPSyncCron = ldapSec.Key("sync_cron").String()
|
||||
LDAPEnabled = ldapSec.Key("enabled").MustBool(false)
|
||||
cfg.LDAPEnabled = LDAPEnabled
|
||||
LDAPSkipOrgRoleSync = ldapSec.Key("skip_org_role_sync").MustBool(false)
|
||||
cfg.LDAPSkipOrgRoleSync = LDAPSkipOrgRoleSync
|
||||
LDAPActiveSyncEnabled = ldapSec.Key("active_sync_enabled").MustBool(false)
|
||||
LDAPAllowSignup = ldapSec.Key("allow_sign_up").MustBool(true)
|
||||
cfg.LDAPAllowSignup = LDAPAllowSignup
|
||||
|
@ -105,7 +105,7 @@ export class UserAdminPage extends PureComponent<Props> {
|
||||
|
||||
render() {
|
||||
const { user, orgs, sessions, ldapSyncInfo, isLoading } = this.props;
|
||||
const isLDAPUser = user && user.isExternal && user.authLabels && user.authLabels.includes('LDAP');
|
||||
const isLDAPUser = user?.isExternal && user?.authLabels?.includes('LDAP');
|
||||
const canReadSessions = contextSrv.hasPermission(AccessControlAction.UsersAuthTokenList);
|
||||
const canReadLDAPStatus = contextSrv.hasPermission(AccessControlAction.LDAPStatusRead);
|
||||
const isOAuthUserWithSkippableSync =
|
||||
@ -113,9 +113,10 @@ export class UserAdminPage extends PureComponent<Props> {
|
||||
const isSAMLUser = user?.isExternal && user?.authLabels?.includes('SAML');
|
||||
const isUserSynced =
|
||||
!config.auth.DisableSyncLock &&
|
||||
((user?.isExternal && !(isOAuthUserWithSkippableSync || isSAMLUser)) ||
|
||||
((user?.isExternal && !(isOAuthUserWithSkippableSync || isSAMLUser || isLDAPUser)) ||
|
||||
(!config.auth.OAuthSkipOrgRoleUpdateSync && isOAuthUserWithSkippableSync) ||
|
||||
(!config.auth.SAMLSkipOrgRoleSync && isSAMLUser));
|
||||
(!config.auth.SAMLSkipOrgRoleSync && isSAMLUser) ||
|
||||
(!config.auth.LDAPSkipOrgRoleSync && isLDAPUser));
|
||||
|
||||
const pageNav: NavModelItem = {
|
||||
text: user?.login ?? '',
|
||||
@ -137,9 +138,13 @@ export class UserAdminPage extends PureComponent<Props> {
|
||||
onUserEnable={this.onUserEnable}
|
||||
onPasswordChange={this.onPasswordChange}
|
||||
/>
|
||||
{isLDAPUser && featureEnabled('ldapsync') && ldapSyncInfo && canReadLDAPStatus && (
|
||||
<UserLdapSyncInfo ldapSyncInfo={ldapSyncInfo} user={user} onUserSync={this.onUserSync} />
|
||||
)}
|
||||
{!config.auth.LDAPSkipOrgRoleSync &&
|
||||
isLDAPUser &&
|
||||
featureEnabled('ldapsync') &&
|
||||
ldapSyncInfo &&
|
||||
canReadLDAPStatus && (
|
||||
<UserLdapSyncInfo ldapSyncInfo={ldapSyncInfo} user={user} onUserSync={this.onUserSync} />
|
||||
)}
|
||||
<UserPermissions isGrafanaAdmin={user.isGrafanaAdmin} onGrafanaAdminChange={this.onGrafanaAdminChange} />
|
||||
</>
|
||||
)}
|
||||
|
Loading…
Reference in New Issue
Block a user