diff --git a/devenv/docker/blocks/openldap/ldap_dev.toml b/devenv/docker/blocks/openldap/ldap_dev.toml index 8767ff3c64a..6e5b368f523 100644 --- a/devenv/docker/blocks/openldap/ldap_dev.toml +++ b/devenv/docker/blocks/openldap/ldap_dev.toml @@ -28,38 +28,6 @@ search_filter = "(cn=%s)" # An array of base dns to search through 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 [servers.attributes] name = "givenName" diff --git a/devenv/docker/blocks/openldap/ldap_posix_dev.toml b/devenv/docker/blocks/openldap/ldap_posix_dev.toml new file mode 100644 index 00000000000..58d42e85b0a --- /dev/null +++ b/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" diff --git a/devenv/docker/blocks/openldap/notes.md b/devenv/docker/blocks/openldap/notes.md index a74c1427901..fad75de2237 100644 --- a/devenv/docker/blocks/openldap/notes.md +++ b/devenv/docker/blocks/openldap/notes.md @@ -12,7 +12,7 @@ After adding ldif files to `prepopulate`: ## 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 [auth.ldap] @@ -21,6 +21,8 @@ config_file = conf/ldap_dev.toml ; allow_sign_up = true ``` +Otherwise perform same actions for `ldap_dev_posix.toml` config. + ## Groups & Users admins @@ -38,3 +40,11 @@ editors ldap-editors no groups ldap-viewer + + +## Groups & Users (POSIX) + +admins + ldap-posix-admin +no groups + ldap-posix diff --git a/devenv/docker/blocks/openldap/prepopulate/2_users.ldif b/devenv/docker/blocks/openldap/prepopulate/2_users.ldif index 52e74b1e4b1..a13614428b7 100644 --- a/devenv/docker/blocks/openldap/prepopulate/2_users.ldif +++ b/devenv/docker/blocks/openldap/prepopulate/2_users.ldif @@ -78,3 +78,31 @@ objectClass: inetOrgPerson objectClass: organizationalPerson sn: 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 diff --git a/devenv/docker/blocks/openldap/prepopulate/3_groups.ldif b/devenv/docker/blocks/openldap/prepopulate/3_groups.ldif index 8638a089cc8..90fcca3f133 100644 --- a/devenv/docker/blocks/openldap/prepopulate/3_groups.ldif +++ b/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-daniel,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 diff --git a/docs/sources/auth/ldap.md b/docs/sources/auth/ldap.md index a3a01e6b6c6..ee5189eb7d0 100644 --- a/docs/sources/auth/ldap.md +++ b/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" ``` -Also set `member_of = "dn"` in the `[servers.attributes]` section. - ### 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 diff --git a/pkg/services/ldap/helpers.go b/pkg/services/ldap/helpers.go index 4cee20cf311..12438f23f6c 100644 --- a/pkg/services/ldap/helpers.go +++ b/pkg/services/ldap/helpers.go @@ -29,6 +29,10 @@ func appendIfNotEmpty(slice []string, values ...string) []string { } func getAttribute(name string, entry *ldap.Entry) string { + if strings.ToLower(name) == "dn" { + return entry.DN + } + for _, attr := range entry.Attributes { if attr.Name == name { 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 { + if strings.ToLower(name) == "dn" { + return []string{entry.DN} + } + for _, attr := range entry.Attributes { if attr.Name == name && len(attr.Values) > 0 { return attr.Values diff --git a/pkg/services/ldap/ldap.go b/pkg/services/ldap/ldap.go index d93fdbdabcb..15aff7be5c3 100644 --- a/pkg/services/ldap/ldap.go +++ b/pkg/services/ldap/ldap.go @@ -266,7 +266,9 @@ func (server *Server) Users(logins []string) ( 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 } @@ -327,6 +329,9 @@ func (server *Server) getSearchRequest( inputs.Email, inputs.Name, inputs.MemberOf, + + // In case for the POSIX LDAP schema server + server.Config.GroupSearchFilterUserAttribute, ) search := "" @@ -489,6 +494,7 @@ func (server *Server) requestMemberOf(entry *ldap.Entry) ([]string, error) { if len(groupSearchResult.Entries) > 0 { for _, group := range groupSearchResult.Entries { + memberOf = append( memberOf, getAttribute(groupIDAttribute, group), diff --git a/pkg/services/ldap/ldap_helpers_test.go b/pkg/services/ldap/ldap_helpers_test.go index b9f49c49644..65cd25fc041 100644 --- a/pkg/services/ldap/ldap_helpers_test.go +++ b/pkg/services/ldap/ldap_helpers_test.go @@ -105,6 +105,16 @@ func TestLDAPHelpers(t *testing.T) { }) 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() { value := []string{"roelgerrits"} entry := &ldap.Entry{ @@ -137,6 +147,16 @@ func TestLDAPHelpers(t *testing.T) { }) 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() { value := []string{"roelgerrits"} entry := &ldap.Entry{ diff --git a/pkg/services/ldap/ldap_private_test.go b/pkg/services/ldap/ldap_private_test.go index 1a43087399f..d089eab04b9 100644 --- a/pkg/services/ldap/ldap_private_test.go +++ b/pkg/services/ldap/ldap_private_test.go @@ -11,6 +11,44 @@ import ( ) 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("simple case", func() { server := &Server{