mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 15:32:08 +08:00
LDAP: improve POSIX support (#18235)
* LDAP: improve POSIX support * Correctly abtain DN attributes result * Allow more flexibility with comparison mapping between POSIX group & user * Add devenv for POSIX LDAP server * Correct the docs Fixes #18140
This commit is contained in:
@ -28,38 +28,6 @@ search_filter = "(cn=%s)"
|
|||||||
# An array of base dns to search through
|
# An array of base dns to search through
|
||||||
search_base_dns = ["dc=grafana,dc=org"]
|
search_base_dns = ["dc=grafana,dc=org"]
|
||||||
|
|
||||||
# In POSIX LDAP schemas, without memberOf attribute a secondary query must be made for groups.
|
|
||||||
# This is done by enabling group_search_filter below. You must also set member_of= "cn"
|
|
||||||
# in [servers.attributes] below.
|
|
||||||
|
|
||||||
# Users with nested/recursive group membership and an LDAP server that supports LDAP_MATCHING_RULE_IN_CHAIN
|
|
||||||
# can set group_search_filter, group_search_filter_user_attribute, group_search_base_dns and member_of
|
|
||||||
# below in such a way that the user's recursive group membership is considered.
|
|
||||||
#
|
|
||||||
# Nested Groups + Active Directory (AD) Example:
|
|
||||||
#
|
|
||||||
# AD groups store the Distinguished Names (DNs) of members, so your filter must
|
|
||||||
# recursively search your groups for the authenticating user's DN. For example:
|
|
||||||
#
|
|
||||||
# group_search_filter = "(member:1.2.840.113556.1.4.1941:=%s)"
|
|
||||||
# group_search_filter_user_attribute = "distinguishedName"
|
|
||||||
# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
|
|
||||||
#
|
|
||||||
# [servers.attributes]
|
|
||||||
# ...
|
|
||||||
# member_of = "distinguishedName"
|
|
||||||
|
|
||||||
## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available)
|
|
||||||
# group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))"
|
|
||||||
## Group search filter user attribute defines what user attribute gets substituted for %s in group_search_filter.
|
|
||||||
## Defaults to the value of username in [server.attributes]
|
|
||||||
## Valid options are any of your values in [servers.attributes]
|
|
||||||
## If you are using nested groups you probably want to set this and member_of in
|
|
||||||
## [servers.attributes] to "distinguishedName"
|
|
||||||
# group_search_filter_user_attribute = "distinguishedName"
|
|
||||||
## An array of the base DNs to search through for groups. Typically uses ou=groups
|
|
||||||
# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
|
|
||||||
|
|
||||||
# Specify names of the ldap attributes your ldap uses
|
# Specify names of the ldap attributes your ldap uses
|
||||||
[servers.attributes]
|
[servers.attributes]
|
||||||
name = "givenName"
|
name = "givenName"
|
||||||
|
57
devenv/docker/blocks/openldap/ldap_posix_dev.toml
Normal file
57
devenv/docker/blocks/openldap/ldap_posix_dev.toml
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# To troubleshoot and get more log info enable ldap debug logging in grafana.ini
|
||||||
|
# [log]
|
||||||
|
# filters = ldap:debug
|
||||||
|
|
||||||
|
[[servers]]
|
||||||
|
# Ldap server host (specify multiple hosts space separated)
|
||||||
|
host = "127.0.0.1"
|
||||||
|
# Default port is 389 or 636 if use_ssl = true
|
||||||
|
port = 389
|
||||||
|
# Set to true if ldap server supports TLS
|
||||||
|
use_ssl = false
|
||||||
|
# Set to true if connect ldap server with STARTTLS pattern (create connection in insecure, then upgrade to secure connection with TLS)
|
||||||
|
start_tls = false
|
||||||
|
# set to true if you want to skip ssl cert validation
|
||||||
|
ssl_skip_verify = false
|
||||||
|
# set to the path to your root CA certificate or leave unset to use system defaults
|
||||||
|
# root_ca_cert = "/path/to/certificate.crt"
|
||||||
|
|
||||||
|
# Search user bind dn
|
||||||
|
bind_dn = "cn=admin,dc=grafana,dc=org"
|
||||||
|
# Search user bind password
|
||||||
|
# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
|
||||||
|
bind_password = 'grafana'
|
||||||
|
|
||||||
|
# An array of base dns to search through
|
||||||
|
search_base_dns = ["dc=grafana,dc=org"]
|
||||||
|
|
||||||
|
search_filter = "(uid=%s)"
|
||||||
|
|
||||||
|
group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))"
|
||||||
|
group_search_filter_user_attribute = "uid"
|
||||||
|
group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
|
||||||
|
|
||||||
|
[servers.attributes]
|
||||||
|
name = "givenName"
|
||||||
|
surname = "sn"
|
||||||
|
username = "cn"
|
||||||
|
member_of = "memberOf"
|
||||||
|
email = "email"
|
||||||
|
|
||||||
|
# Map ldap groups to grafana org roles
|
||||||
|
[[servers.group_mappings]]
|
||||||
|
group_dn = "cn=posix-admins,ou=groups,dc=grafana,dc=org"
|
||||||
|
org_role = "Admin"
|
||||||
|
grafana_admin = true
|
||||||
|
|
||||||
|
# The Grafana organization database id, optional, if left out the default org (id 1) will be used
|
||||||
|
# org_id = 1
|
||||||
|
|
||||||
|
[[servers.group_mappings]]
|
||||||
|
group_dn = "cn=editors,ou=groups,dc=grafana,dc=org"
|
||||||
|
org_role = "Editor"
|
||||||
|
|
||||||
|
[[servers.group_mappings]]
|
||||||
|
# If you want to match all (or no ldap groups) then you can use wildcard
|
||||||
|
group_dn = "*"
|
||||||
|
org_role = "Viewer"
|
@ -12,7 +12,7 @@ After adding ldif files to `prepopulate`:
|
|||||||
|
|
||||||
## Enabling LDAP in Grafana
|
## Enabling LDAP in Grafana
|
||||||
|
|
||||||
Copy the ldap_dev.toml file in this folder into your `conf` folder (it is gitignored already). To enable it in the .ini file to get Grafana to use this block:
|
If you want to use users/groups with `memberOf` support Copy the ldap_dev.toml file in this folder into your `conf` folder (it is gitignored already). To enable it in the .ini file to get Grafana to use this block:
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[auth.ldap]
|
[auth.ldap]
|
||||||
@ -21,6 +21,8 @@ config_file = conf/ldap_dev.toml
|
|||||||
; allow_sign_up = true
|
; allow_sign_up = true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Otherwise perform same actions for `ldap_dev_posix.toml` config.
|
||||||
|
|
||||||
## Groups & Users
|
## Groups & Users
|
||||||
|
|
||||||
admins
|
admins
|
||||||
@ -38,3 +40,11 @@ editors
|
|||||||
ldap-editors
|
ldap-editors
|
||||||
no groups
|
no groups
|
||||||
ldap-viewer
|
ldap-viewer
|
||||||
|
|
||||||
|
|
||||||
|
## Groups & Users (POSIX)
|
||||||
|
|
||||||
|
admins
|
||||||
|
ldap-posix-admin
|
||||||
|
no groups
|
||||||
|
ldap-posix
|
||||||
|
@ -78,3 +78,31 @@ objectClass: inetOrgPerson
|
|||||||
objectClass: organizationalPerson
|
objectClass: organizationalPerson
|
||||||
sn: ldap-torkel
|
sn: ldap-torkel
|
||||||
cn: ldap-torkel
|
cn: ldap-torkel
|
||||||
|
|
||||||
|
# admin for posix group (without support for memberOf attribute)
|
||||||
|
dn: uid=ldap-posix-admin,ou=users,dc=grafana,dc=org
|
||||||
|
mail: ldap-posix-admin@grafana.com
|
||||||
|
userPassword: grafana
|
||||||
|
objectclass: top
|
||||||
|
objectclass: posixAccount
|
||||||
|
objectclass: inetOrgPerson
|
||||||
|
homedirectory: /home/ldap-posix-admin
|
||||||
|
sn: ldap-posix-admin
|
||||||
|
cn: ldap-posix-admin
|
||||||
|
uid: ldap-posix-admin
|
||||||
|
uidnumber: 1
|
||||||
|
gidnumber: 1
|
||||||
|
|
||||||
|
# user for posix group (without support for memberOf attribute)
|
||||||
|
dn: uid=ldap-posix,ou=users,dc=grafana,dc=org
|
||||||
|
mail: ldap-posix@grafana.com
|
||||||
|
userPassword: grafana
|
||||||
|
objectclass: top
|
||||||
|
objectclass: posixAccount
|
||||||
|
objectclass: inetOrgPerson
|
||||||
|
homedirectory: /home/ldap-posix
|
||||||
|
sn: ldap-posix
|
||||||
|
cn: ldap-posix
|
||||||
|
uid: ldap-posix
|
||||||
|
uidnumber: 2
|
||||||
|
gidnumber: 2
|
||||||
|
@ -23,3 +23,21 @@ objectClass: groupOfNames
|
|||||||
member: cn=ldap-torkel,ou=users,dc=grafana,dc=org
|
member: cn=ldap-torkel,ou=users,dc=grafana,dc=org
|
||||||
member: cn=ldap-daniel,ou=users,dc=grafana,dc=org
|
member: cn=ldap-daniel,ou=users,dc=grafana,dc=org
|
||||||
member: cn=ldap-leo,ou=users,dc=grafana,dc=org
|
member: cn=ldap-leo,ou=users,dc=grafana,dc=org
|
||||||
|
|
||||||
|
# -- POSIX --
|
||||||
|
|
||||||
|
# posix admin group (without support for memberOf attribute)
|
||||||
|
dn: cn=posix-admins,ou=groups,dc=grafana,dc=org
|
||||||
|
cn: admins
|
||||||
|
objectClass: top
|
||||||
|
objectClass: posixGroup
|
||||||
|
gidNumber: 1
|
||||||
|
memberUid: ldap-posix-admin
|
||||||
|
|
||||||
|
# posix group (without support for memberOf attribute)
|
||||||
|
dn: cn=posix,ou=groups,dc=grafana,dc=org
|
||||||
|
cn: viewers
|
||||||
|
objectClass: top
|
||||||
|
objectClass: posixGroup
|
||||||
|
gidNumber: 2
|
||||||
|
memberUid: ldap-posix
|
||||||
|
@ -126,8 +126,6 @@ group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
|
|||||||
group_search_filter_user_attribute = "uid"
|
group_search_filter_user_attribute = "uid"
|
||||||
```
|
```
|
||||||
|
|
||||||
Also set `member_of = "dn"` in the `[servers.attributes]` section.
|
|
||||||
|
|
||||||
### Group Mappings
|
### Group Mappings
|
||||||
|
|
||||||
In `[[servers.group_mappings]]` you can map an LDAP group to a Grafana organization and role. These will be synced every time the user logs in, with LDAP being
|
In `[[servers.group_mappings]]` you can map an LDAP group to a Grafana organization and role. These will be synced every time the user logs in, with LDAP being
|
||||||
|
@ -29,6 +29,10 @@ func appendIfNotEmpty(slice []string, values ...string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getAttribute(name string, entry *ldap.Entry) string {
|
func getAttribute(name string, entry *ldap.Entry) string {
|
||||||
|
if strings.ToLower(name) == "dn" {
|
||||||
|
return entry.DN
|
||||||
|
}
|
||||||
|
|
||||||
for _, attr := range entry.Attributes {
|
for _, attr := range entry.Attributes {
|
||||||
if attr.Name == name {
|
if attr.Name == name {
|
||||||
if len(attr.Values) > 0 {
|
if len(attr.Values) > 0 {
|
||||||
@ -40,6 +44,10 @@ func getAttribute(name string, entry *ldap.Entry) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getArrayAttribute(name string, entry *ldap.Entry) []string {
|
func getArrayAttribute(name string, entry *ldap.Entry) []string {
|
||||||
|
if strings.ToLower(name) == "dn" {
|
||||||
|
return []string{entry.DN}
|
||||||
|
}
|
||||||
|
|
||||||
for _, attr := range entry.Attributes {
|
for _, attr := range entry.Attributes {
|
||||||
if attr.Name == name && len(attr.Values) > 0 {
|
if attr.Name == name && len(attr.Values) > 0 {
|
||||||
return attr.Values
|
return attr.Values
|
||||||
|
@ -266,7 +266,9 @@ func (server *Server) Users(logins []string) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
server.log.Debug("LDAP users found", "users", spew.Sdump(serializedUsers))
|
server.log.Debug(
|
||||||
|
"LDAP users found", "users", spew.Sdump(serializedUsers),
|
||||||
|
)
|
||||||
|
|
||||||
return serializedUsers, nil
|
return serializedUsers, nil
|
||||||
}
|
}
|
||||||
@ -327,6 +329,9 @@ func (server *Server) getSearchRequest(
|
|||||||
inputs.Email,
|
inputs.Email,
|
||||||
inputs.Name,
|
inputs.Name,
|
||||||
inputs.MemberOf,
|
inputs.MemberOf,
|
||||||
|
|
||||||
|
// In case for the POSIX LDAP schema server
|
||||||
|
server.Config.GroupSearchFilterUserAttribute,
|
||||||
)
|
)
|
||||||
|
|
||||||
search := ""
|
search := ""
|
||||||
@ -489,6 +494,7 @@ func (server *Server) requestMemberOf(entry *ldap.Entry) ([]string, error) {
|
|||||||
|
|
||||||
if len(groupSearchResult.Entries) > 0 {
|
if len(groupSearchResult.Entries) > 0 {
|
||||||
for _, group := range groupSearchResult.Entries {
|
for _, group := range groupSearchResult.Entries {
|
||||||
|
|
||||||
memberOf = append(
|
memberOf = append(
|
||||||
memberOf,
|
memberOf,
|
||||||
getAttribute(groupIDAttribute, group),
|
getAttribute(groupIDAttribute, group),
|
||||||
|
@ -105,6 +105,16 @@ func TestLDAPHelpers(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Convey("getAttribute()", t, func() {
|
Convey("getAttribute()", t, func() {
|
||||||
|
Convey("Should get DN", func() {
|
||||||
|
entry := &ldap.Entry{
|
||||||
|
DN: "test",
|
||||||
|
}
|
||||||
|
|
||||||
|
result := getAttribute("dn", entry)
|
||||||
|
|
||||||
|
So(result, ShouldEqual, "test")
|
||||||
|
})
|
||||||
|
|
||||||
Convey("Should get username", func() {
|
Convey("Should get username", func() {
|
||||||
value := []string{"roelgerrits"}
|
value := []string{"roelgerrits"}
|
||||||
entry := &ldap.Entry{
|
entry := &ldap.Entry{
|
||||||
@ -137,6 +147,16 @@ func TestLDAPHelpers(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Convey("getArrayAttribute()", t, func() {
|
Convey("getArrayAttribute()", t, func() {
|
||||||
|
Convey("Should get DN", func() {
|
||||||
|
entry := &ldap.Entry{
|
||||||
|
DN: "test",
|
||||||
|
}
|
||||||
|
|
||||||
|
result := getArrayAttribute("dn", entry)
|
||||||
|
|
||||||
|
So(result, ShouldResemble, []string{"test"})
|
||||||
|
})
|
||||||
|
|
||||||
Convey("Should get username", func() {
|
Convey("Should get username", func() {
|
||||||
value := []string{"roelgerrits"}
|
value := []string{"roelgerrits"}
|
||||||
entry := &ldap.Entry{
|
entry := &ldap.Entry{
|
||||||
|
@ -11,6 +11,44 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestLDAPPrivateMethods(t *testing.T) {
|
func TestLDAPPrivateMethods(t *testing.T) {
|
||||||
|
Convey("getSearchRequest()", t, func() {
|
||||||
|
Convey("with enabled GroupSearchFilterUserAttribute setting", func() {
|
||||||
|
server := &Server{
|
||||||
|
Config: &ServerConfig{
|
||||||
|
Attr: AttributeMap{
|
||||||
|
Username: "username",
|
||||||
|
Name: "name",
|
||||||
|
MemberOf: "memberof",
|
||||||
|
Email: "email",
|
||||||
|
},
|
||||||
|
GroupSearchFilterUserAttribute: "gansta",
|
||||||
|
SearchBaseDNs: []string{"BaseDNHere"},
|
||||||
|
},
|
||||||
|
log: log.New("test-logger"),
|
||||||
|
}
|
||||||
|
|
||||||
|
result := server.getSearchRequest("killa", []string{"gorilla"})
|
||||||
|
|
||||||
|
So(result, ShouldResemble, &ldap.SearchRequest{
|
||||||
|
BaseDN: "killa",
|
||||||
|
Scope: 2,
|
||||||
|
DerefAliases: 0,
|
||||||
|
SizeLimit: 0,
|
||||||
|
TimeLimit: 0,
|
||||||
|
TypesOnly: false,
|
||||||
|
Filter: "(|)",
|
||||||
|
Attributes: []string{
|
||||||
|
"username",
|
||||||
|
"email",
|
||||||
|
"name",
|
||||||
|
"memberof",
|
||||||
|
"gansta",
|
||||||
|
},
|
||||||
|
Controls: nil,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
Convey("serializeUsers()", t, func() {
|
Convey("serializeUsers()", t, func() {
|
||||||
Convey("simple case", func() {
|
Convey("simple case", func() {
|
||||||
server := &Server{
|
server := &Server{
|
||||||
|
Reference in New Issue
Block a user