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:
Gabriel MABILLE
2022-10-12 13:33:33 +02:00
committed by GitHub
parent 72b9555487
commit 10c080dad1
10 changed files with 175 additions and 12 deletions

View File

@ -632,6 +632,7 @@ allow_assign_grafana_admin = false
enabled = false enabled = false
config_file = /etc/grafana/ldap.toml config_file = /etc/grafana/ldap.toml
allow_sign_up = true allow_sign_up = true
skip_org_role_sync = false
# LDAP background sync (Enterprise only) # LDAP background sync (Enterprise only)
# At 1 am every day # At 1 am every day

View File

@ -624,6 +624,8 @@
;enabled = false ;enabled = false
;config_file = /etc/grafana/ldap.toml ;config_file = /etc/grafana/ldap.toml
;allow_sign_up = true ;allow_sign_up = true
# prevent synchronizing ldap users organization roles
;skip_org_role_sync = false
# LDAP background sync (Enterprise only) # LDAP background sync (Enterprise only)
# At 1 am every day # At 1 am every day

View File

@ -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 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`). specific configuration file (default: `/etc/grafana/ldap.toml`).
```bash ```ini
[auth.ldap] [auth.ldap]
# Set to `true` to enable LDAP integration (default: `false`) # Set to `true` to enable LDAP integration (default: `false`)
enabled = true enabled = true
@ -36,11 +36,32 @@ enabled = true
# Path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`) # Path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`)
config_file = /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 # Allow sign-up should be `true` (default) to allow Grafana to create users on successful LDAP authentication.
# false only pre-existing Grafana users will be able to login (if LDAP authentication is ok). # If set to `false` only already existing Grafana users will be able to login.
allow_sign_up = true 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 ## Grafana LDAP Configuration
Depending on which LDAP server you're using and how that's configured your Grafana LDAP configuration may vary. Depending on which LDAP server you're using and how that's configured your Grafana LDAP configuration may vary.

View File

@ -220,5 +220,6 @@ export interface GrafanaConfig {
export interface AuthSettings { export interface AuthSettings {
OAuthSkipOrgRoleUpdateSync?: boolean; OAuthSkipOrgRoleUpdateSync?: boolean;
SAMLSkipOrgRoleSync?: boolean; SAMLSkipOrgRoleSync?: boolean;
LDAPSkipOrgRoleSync?: boolean;
DisableSyncLock?: boolean; DisableSyncLock?: boolean;
} }

View File

@ -146,6 +146,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
"auth": map[string]interface{}{ "auth": map[string]interface{}{
"OAuthSkipOrgRoleUpdateSync": hs.Cfg.OAuthSkipOrgRoleUpdateSync, "OAuthSkipOrgRoleUpdateSync": hs.Cfg.OAuthSkipOrgRoleUpdateSync,
"SAMLSkipOrgRoleSync": hs.Cfg.SectionWithEnvOverrides("auth.saml").Key("skip_org_role_sync").MustBool(false), "SAMLSkipOrgRoleSync": hs.Cfg.SectionWithEnvOverrides("auth.saml").Key("skip_org_role_sync").MustBool(false),
"LDAPSkipOrgRoleSync": hs.Cfg.LDAPSkipOrgRoleSync,
"DisableSyncLock": hs.Cfg.DisableSyncLock, "DisableSyncLock": hs.Cfg.DisableSyncLock,
}, },
"buildInfo": map[string]interface{}{ "buildInfo": map[string]interface{}{

View File

@ -363,7 +363,8 @@ func (server *Server) users(logins []string) (
// If there are no ldap group mappings access is true // If there are no ldap group mappings access is true
// otherwise a single group must match // otherwise a single group must match
func (server *Server) validateGrafanaUser(user *models.ExternalUserInfo) error { 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( server.log.Error(
"User does not belong in any of the specified LDAP groups", "User does not belong in any of the specified LDAP groups",
"username", user.Login, "username", user.Login,
@ -446,6 +447,11 @@ func (server *Server) buildGrafanaUser(user *ldap.Entry) (*models.ExternalUserIn
OrgRoles: map[int64]org.RoleType{}, OrgRoles: map[int64]org.RoleType{},
} }
// Skipping org role sync
if SkipOrgRoleSync() {
return extUser, nil
}
for _, group := range server.Config.Groups { for _, group := range server.Config.Groups {
// only use the first match for each org // only use the first match for each org
if extUser.OrgRoles[group.OrgId] != "" { if extUser.OrgRoles[group.OrgId] != "" {

View File

@ -10,6 +10,8 @@ import (
"gopkg.in/ldap.v3" "gopkg.in/ldap.v3"
"github.com/grafana/grafana/pkg/infra/log" "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) { func TestNew(t *testing.T) {
@ -221,6 +223,122 @@ func TestServer_Users(t *testing.T) {
require.Len(t, res, 1) require.Len(t, res, 1)
assert.Equal(t, "Grot the First", res[0].Name) 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) { func TestServer_UserBind(t *testing.T) {

View File

@ -76,6 +76,10 @@ func IsEnabled() bool {
return setting.LDAPEnabled return setting.LDAPEnabled
} }
func SkipOrgRoleSync() bool {
return setting.LDAPSkipOrgRoleSync
}
// ReloadConfig reads the config from the disk and caches it. // ReloadConfig reads the config from the disk and caches it.
func ReloadConfig() error { func ReloadConfig() error {
if !IsEnabled() { if !IsEnabled() {

View File

@ -147,6 +147,7 @@ var (
// LDAP // LDAP
LDAPEnabled bool LDAPEnabled bool
LDAPSkipOrgRoleSync bool
LDAPConfigFile string LDAPConfigFile string
LDAPSyncCron string LDAPSyncCron string
LDAPAllowSignup bool LDAPAllowSignup bool
@ -413,8 +414,9 @@ type Cfg struct {
FeedbackLinksEnabled bool FeedbackLinksEnabled bool
// LDAP // LDAP
LDAPEnabled bool LDAPEnabled bool
LDAPAllowSignup bool LDAPSkipOrgRoleSync bool
LDAPAllowSignup bool
Quota QuotaSettings Quota QuotaSettings
@ -1131,6 +1133,8 @@ func (cfg *Cfg) readLDAPConfig() {
LDAPSyncCron = ldapSec.Key("sync_cron").String() LDAPSyncCron = ldapSec.Key("sync_cron").String()
LDAPEnabled = ldapSec.Key("enabled").MustBool(false) LDAPEnabled = ldapSec.Key("enabled").MustBool(false)
cfg.LDAPEnabled = LDAPEnabled cfg.LDAPEnabled = LDAPEnabled
LDAPSkipOrgRoleSync = ldapSec.Key("skip_org_role_sync").MustBool(false)
cfg.LDAPSkipOrgRoleSync = LDAPSkipOrgRoleSync
LDAPActiveSyncEnabled = ldapSec.Key("active_sync_enabled").MustBool(false) LDAPActiveSyncEnabled = ldapSec.Key("active_sync_enabled").MustBool(false)
LDAPAllowSignup = ldapSec.Key("allow_sign_up").MustBool(true) LDAPAllowSignup = ldapSec.Key("allow_sign_up").MustBool(true)
cfg.LDAPAllowSignup = LDAPAllowSignup cfg.LDAPAllowSignup = LDAPAllowSignup

View File

@ -105,7 +105,7 @@ export class UserAdminPage extends PureComponent<Props> {
render() { render() {
const { user, orgs, sessions, ldapSyncInfo, isLoading } = this.props; 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 canReadSessions = contextSrv.hasPermission(AccessControlAction.UsersAuthTokenList);
const canReadLDAPStatus = contextSrv.hasPermission(AccessControlAction.LDAPStatusRead); const canReadLDAPStatus = contextSrv.hasPermission(AccessControlAction.LDAPStatusRead);
const isOAuthUserWithSkippableSync = const isOAuthUserWithSkippableSync =
@ -113,9 +113,10 @@ export class UserAdminPage extends PureComponent<Props> {
const isSAMLUser = user?.isExternal && user?.authLabels?.includes('SAML'); const isSAMLUser = user?.isExternal && user?.authLabels?.includes('SAML');
const isUserSynced = const isUserSynced =
!config.auth.DisableSyncLock && !config.auth.DisableSyncLock &&
((user?.isExternal && !(isOAuthUserWithSkippableSync || isSAMLUser)) || ((user?.isExternal && !(isOAuthUserWithSkippableSync || isSAMLUser || isLDAPUser)) ||
(!config.auth.OAuthSkipOrgRoleUpdateSync && isOAuthUserWithSkippableSync) || (!config.auth.OAuthSkipOrgRoleUpdateSync && isOAuthUserWithSkippableSync) ||
(!config.auth.SAMLSkipOrgRoleSync && isSAMLUser)); (!config.auth.SAMLSkipOrgRoleSync && isSAMLUser) ||
(!config.auth.LDAPSkipOrgRoleSync && isLDAPUser));
const pageNav: NavModelItem = { const pageNav: NavModelItem = {
text: user?.login ?? '', text: user?.login ?? '',
@ -137,9 +138,13 @@ export class UserAdminPage extends PureComponent<Props> {
onUserEnable={this.onUserEnable} onUserEnable={this.onUserEnable}
onPasswordChange={this.onPasswordChange} onPasswordChange={this.onPasswordChange}
/> />
{isLDAPUser && featureEnabled('ldapsync') && ldapSyncInfo && canReadLDAPStatus && ( {!config.auth.LDAPSkipOrgRoleSync &&
<UserLdapSyncInfo ldapSyncInfo={ldapSyncInfo} user={user} onUserSync={this.onUserSync} /> isLDAPUser &&
)} featureEnabled('ldapsync') &&
ldapSyncInfo &&
canReadLDAPStatus && (
<UserLdapSyncInfo ldapSyncInfo={ldapSyncInfo} user={user} onUserSync={this.onUserSync} />
)}
<UserPermissions isGrafanaAdmin={user.isGrafanaAdmin} onGrafanaAdminChange={this.onGrafanaAdminChange} /> <UserPermissions isGrafanaAdmin={user.isGrafanaAdmin} onGrafanaAdminChange={this.onGrafanaAdminChange} />
</> </>
)} )}