浏览代码

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
Oleg Gaidarenko 6 年之前
父节点
当前提交
1e5fc76601

+ 0 - 32
devenv/docker/blocks/openldap/ldap_dev.toml

@@ -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 - 0
devenv/docker/blocks/openldap/ldap_posix_dev.toml

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

+ 11 - 1
devenv/docker/blocks/openldap/notes.md

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

+ 28 - 0
devenv/docker/blocks/openldap/prepopulate/2_users.ldif

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

+ 18 - 0
devenv/docker/blocks/openldap/prepopulate/3_groups.ldif

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

+ 0 - 2
docs/sources/auth/ldap.md

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

+ 8 - 0
pkg/services/ldap/helpers.go

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

+ 7 - 1
pkg/services/ldap/ldap.go

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

+ 20 - 0
pkg/services/ldap/ldap_helpers_test.go

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

+ 38 - 0
pkg/services/ldap/ldap_private_test.go

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