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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 175 additions and 12 deletions

View File

@ -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

View File

@ -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

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
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.

View File

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

View File

@ -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{}{

View File

@ -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] != "" {

View File

@ -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) {

View File

@ -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() {

View File

@ -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

View File

@ -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} />
</>
)}