diff --git a/internal/outpost/ldap/bind.go b/internal/outpost/ldap/bind.go index 8dff77d64..adb3589ae 100644 --- a/internal/outpost/ldap/bind.go +++ b/internal/outpost/ldap/bind.go @@ -49,5 +49,6 @@ func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LD "reason": "no_provider", "app": "", }).Inc() - return ldap.LDAPResultOperationsError, nil + + return ldap.LDAPResultInsufficientAccessRights, nil } diff --git a/internal/outpost/ldap/constants/constants.go b/internal/outpost/ldap/constants/constants.go index d5647f780..8f5cdf415 100644 --- a/internal/outpost/ldap/constants/constants.go +++ b/internal/outpost/ldap/constants/constants.go @@ -12,6 +12,7 @@ const ( OCGroupOfNames = "groupOfNames" OCAKGroup = "goauthentik.io/ldap/group" OCAKVirtualGroup = "goauthentik.io/ldap/virtual-group" + OCPosixGroup = "posixGroup" ) const ( @@ -19,6 +20,7 @@ const ( OCOrgPerson = "organizationalPerson" OCInetOrgPerson = "inetOrgPerson" OCAKUser = "goauthentik.io/ldap/user" + OCPosixAccount = "posixAccount" ) const ( @@ -47,6 +49,7 @@ func GetUserOCs() map[string]bool { OCOrgPerson: true, OCInetOrgPerson: true, OCAKUser: true, + OCPosixAccount: true, } } @@ -56,6 +59,7 @@ func GetGroupOCs() map[string]bool { OCGroupOfUniqueNames: true, OCGroupOfNames: true, OCAKGroup: true, + OCPosixGroup: true, } } diff --git a/internal/outpost/ldap/entries.go b/internal/outpost/ldap/entries.go index 44b18cbd6..9912188f9 100644 --- a/internal/outpost/ldap/entries.go +++ b/internal/outpost/ldap/entries.go @@ -1,7 +1,9 @@ package ldap import ( + "fmt" "strconv" + "strings" "github.com/nmcclain/ldap" "goauthentik.io/api/v3" @@ -11,9 +13,29 @@ import ( func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry { dn := pi.GetUserDN(u.Username) - attrs := utils.AttributesToLDAP(u.Attributes, false) - sanitizedAttrs := utils.AttributesToLDAP(u.Attributes, true) - attrs = append(attrs, sanitizedAttrs...) + userValueMap := func(value []string) []string { + for i, v := range value { + if strings.Contains(v, "%s") { + value[i] = fmt.Sprintf(v, u.Username) + } + } + return value + } + attrs := utils.AttributesToLDAP(u.Attributes, func(key string) string { + return utils.AttributeKeySanitize(key) + }, userValueMap) + rawAttrs := utils.AttributesToLDAP(u.Attributes, func(key string) string { + return key + }, userValueMap) + // Only append attributes that don't already exist + // TODO: Remove in 2023.3 + for _, rawAttr := range rawAttrs { + for _, attr := range attrs { + if !strings.EqualFold(attr.Name, rawAttr.Name) { + attrs = append(attrs, rawAttr) + } + } + } if u.IsActive == nil { u.IsActive = api.PtrBool(false) @@ -38,6 +60,8 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry { "objectClass": {constants.OCUser, constants.OCOrgPerson, constants.OCInetOrgPerson, constants.OCAKUser}, "uidNumber": {pi.GetUidNumber(u)}, "gidNumber": {pi.GetUidNumber(u)}, + "homeDirectory": {fmt.Sprintf("/home/%s", u.Username)}, + "sn": {u.Name}, }) return &ldap.Entry{DN: dn, Attributes: attrs} } diff --git a/internal/outpost/ldap/group/group.go b/internal/outpost/ldap/group/group.go index cef60056f..089be6ebb 100644 --- a/internal/outpost/ldap/group/group.go +++ b/internal/outpost/ldap/group/group.go @@ -2,6 +2,7 @@ package group import ( "strconv" + "strings" "github.com/nmcclain/ldap" "goauthentik.io/api/v3" @@ -18,15 +19,31 @@ type LDAPGroup struct { Member []string IsSuperuser bool IsVirtualGroup bool - AKAttributes map[string]interface{} + Attributes map[string]interface{} } func (lg *LDAPGroup) Entry() *ldap.Entry { - attrs := utils.AttributesToLDAP(lg.AKAttributes, false) - sanitizedAttrs := utils.AttributesToLDAP(lg.AKAttributes, true) - attrs = append(attrs, sanitizedAttrs...) + attrs := utils.AttributesToLDAP(lg.Attributes, func(key string) string { + return utils.AttributeKeySanitize(key) + }, func(value []string) []string { + return value + }) + rawAttrs := utils.AttributesToLDAP(lg.Attributes, func(key string) string { + return key + }, func(value []string) []string { + return value + }) + // Only append attributes that don't already exist + // TODO: Remove in 2023.3 + for _, rawAttr := range rawAttrs { + for _, attr := range attrs { + if !strings.EqualFold(attr.Name, rawAttr.Name) { + attrs = append(attrs, rawAttr) + } + } + } - objectClass := []string{constants.OCGroup, constants.OCGroupOfUniqueNames, constants.OCGroupOfNames, constants.OCAKGroup} + objectClass := []string{constants.OCGroup, constants.OCGroupOfUniqueNames, constants.OCGroupOfNames, constants.OCAKGroup, constants.OCPosixGroup} if lg.IsVirtualGroup { objectClass = append(objectClass, constants.OCAKVirtualGroup) } @@ -55,7 +72,7 @@ func FromAPIGroup(g api.Group, si server.LDAPServerInstance) *LDAPGroup { Member: si.UsersForGroup(g), IsVirtualGroup: false, IsSuperuser: *g.IsSuperuser, - AKAttributes: g.Attributes, + Attributes: g.Attributes, } } @@ -68,6 +85,6 @@ func FromAPIUser(u api.User, si server.LDAPServerInstance) *LDAPGroup { Member: []string{si.GetUserDN(u.Username)}, IsVirtualGroup: true, IsSuperuser: false, - AKAttributes: nil, + Attributes: nil, } } diff --git a/internal/outpost/ldap/search.go b/internal/outpost/ldap/search.go index f1a73f41a..d9fe1d4c6 100644 --- a/internal/outpost/ldap/search.go +++ b/internal/outpost/ldap/search.go @@ -37,7 +37,24 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n }() if searchReq.BaseDN == "" { - return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultSuccess}, nil + return ldap.ServerSearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + { + Name: "objectClass", + Values: []string{"top", "OpenLDAProotDSE"}, + }, + { + Name: "subschemaSubentry", + Values: []string{"cn=subschema"}, + }, + }, + }, + }, + Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess, + }, nil } bd, err := goldap.ParseDN(strings.ToLower(searchReq.BaseDN)) if err != nil { @@ -51,5 +68,22 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n return provider.searcher.Search(req) } } - return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("no provider could handle request") + return ldap.ServerSearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + { + Name: "objectClass", + Values: []string{"top", "OpenLDAProotDSE"}, + }, + { + Name: "subschemaSubentry", + Values: []string{"cn=subschema"}, + }, + }, + }, + }, + Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess, + }, nil } diff --git a/internal/outpost/ldap/utils/utils.go b/internal/outpost/ldap/utils/utils.go index bb44a1381..1d203d1fb 100644 --- a/internal/outpost/ldap/utils/utils.go +++ b/internal/outpost/ldap/utils/utils.go @@ -44,33 +44,35 @@ func stringify(in interface{}) *string { } } -func AttributesToLDAP(attrs map[string]interface{}, sanitize bool) []*ldap.EntryAttribute { +func AttributesToLDAP( + attrs map[string]interface{}, + keyFormatter func(key string) string, + valueFormatter func(value []string) []string, +) []*ldap.EntryAttribute { attrList := []*ldap.EntryAttribute{} if attrs == nil { return attrList } for attrKey, attrValue := range attrs { - entry := &ldap.EntryAttribute{Name: attrKey} - if sanitize { - entry.Name = AttributeKeySanitize(attrKey) - } + entry := &ldap.EntryAttribute{Name: keyFormatter(attrKey)} switch t := attrValue.(type) { case []string: - entry.Values = t + entry.Values = valueFormatter(t) case *[]string: - entry.Values = *t + entry.Values = valueFormatter(*t) case []interface{}: - entry.Values = make([]string, len(t)) - for idx, v := range t { + vv := make([]string, 0) + for _, v := range t { v := stringify(v) if v != nil { - entry.Values[idx] = *v + vv = append(vv, *v) } } + entry.Values = valueFormatter(vv) default: v := stringify(t) if v != nil { - entry.Values = []string{*v} + entry.Values = valueFormatter([]string{*v}) } } attrList = append(attrList, entry) @@ -88,7 +90,7 @@ func EnsureAttributes(attrs []*ldap.EntryAttribute, shouldHave map[string][]stri func MustHaveAttribute(attrs []*ldap.EntryAttribute, name string, value []string) []*ldap.EntryAttribute { shouldSet := true for _, attr := range attrs { - if attr.Name == name { + if strings.EqualFold(attr.Name, name) { shouldSet = false } } diff --git a/internal/outpost/ldap/utils/utils_test.go b/internal/outpost/ldap/utils/utils_test.go index 13b4cdc86..a01d4dcf0 100644 --- a/internal/outpost/ldap/utils/utils_test.go +++ b/internal/outpost/ldap/utils/utils_test.go @@ -19,16 +19,26 @@ func TestAKAttrsToLDAP_String(t *testing.T) { u.Attributes = map[string]interface{}{ "foo": "bar", } - assert.Equal(t, 1, len(AttributesToLDAP(u.Attributes, true))) - assert.Equal(t, "foo", AttributesToLDAP(u.Attributes, true)[0].Name) - assert.Equal(t, []string{"bar"}, AttributesToLDAP(u.Attributes, true)[0].Values) + mapped := AttributesToLDAP(u.Attributes, func(key string) string { + return AttributeKeySanitize(key) + }, func(value []string) []string { + return value + }) + assert.Equal(t, 1, len(mapped)) + assert.Equal(t, "foo", mapped[0].Name) + assert.Equal(t, []string{"bar"}, mapped[0].Values) // pointer string u.Attributes = map[string]interface{}{ "foo": api.PtrString("bar"), } - assert.Equal(t, 1, len(AttributesToLDAP(u.Attributes, true))) - assert.Equal(t, "foo", AttributesToLDAP(u.Attributes, true)[0].Name) - assert.Equal(t, []string{"bar"}, AttributesToLDAP(u.Attributes, true)[0].Values) + mapped = AttributesToLDAP(u.Attributes, func(key string) string { + return AttributeKeySanitize(key) + }, func(value []string) []string { + return value + }) + assert.Equal(t, 1, len(mapped)) + assert.Equal(t, "foo", mapped[0].Name) + assert.Equal(t, []string{"bar"}, mapped[0].Values) } func TestAKAttrsToLDAP_String_List(t *testing.T) { @@ -37,16 +47,26 @@ func TestAKAttrsToLDAP_String_List(t *testing.T) { u.Attributes = map[string]interface{}{ "foo": []string{"bar"}, } - assert.Equal(t, 1, len(AttributesToLDAP(u.Attributes, true))) - assert.Equal(t, "foo", AttributesToLDAP(u.Attributes, true)[0].Name) - assert.Equal(t, []string{"bar"}, AttributesToLDAP(u.Attributes, true)[0].Values) + mapped := AttributesToLDAP(u.Attributes, func(key string) string { + return AttributeKeySanitize(key) + }, func(value []string) []string { + return value + }) + assert.Equal(t, 1, len(mapped)) + assert.Equal(t, "foo", mapped[0].Name) + assert.Equal(t, []string{"bar"}, mapped[0].Values) // pointer string list u.Attributes = map[string]interface{}{ "foo": &[]string{"bar"}, } - assert.Equal(t, 1, len(AttributesToLDAP(u.Attributes, true))) - assert.Equal(t, "foo", AttributesToLDAP(u.Attributes, true)[0].Name) - assert.Equal(t, []string{"bar"}, AttributesToLDAP(u.Attributes, true)[0].Values) + mapped = AttributesToLDAP(u.Attributes, func(key string) string { + return AttributeKeySanitize(key) + }, func(value []string) []string { + return value + }) + assert.Equal(t, 1, len(mapped)) + assert.Equal(t, "foo", mapped[0].Name) + assert.Equal(t, []string{"bar"}, mapped[0].Values) } func TestAKAttrsToLDAP_Dict(t *testing.T) { @@ -56,9 +76,14 @@ func TestAKAttrsToLDAP_Dict(t *testing.T) { "foo": "bar", }, } - assert.Equal(t, 1, len(AttributesToLDAP(d, true))) - assert.Equal(t, "foo", AttributesToLDAP(d, true)[0].Name) - assert.Equal(t, []string{"map[foo:bar]"}, AttributesToLDAP(d, true)[0].Values) + mapped := AttributesToLDAP(d, func(key string) string { + return AttributeKeySanitize(key) + }, func(value []string) []string { + return value + }) + assert.Equal(t, 1, len(mapped)) + assert.Equal(t, "foo", mapped[0].Name) + assert.Equal(t, []string{"map[foo:bar]"}, mapped[0].Values) } func TestAKAttrsToLDAP_Mixed(t *testing.T) { @@ -69,7 +94,12 @@ func TestAKAttrsToLDAP_Mixed(t *testing.T) { 6, }, } - assert.Equal(t, 1, len(AttributesToLDAP(d, true))) - assert.Equal(t, "foo", AttributesToLDAP(d, true)[0].Name) - assert.Equal(t, []string{"foo", "6"}, AttributesToLDAP(d, true)[0].Values) + mapped := AttributesToLDAP(d, func(key string) string { + return AttributeKeySanitize(key) + }, func(value []string) []string { + return value + }) + assert.Equal(t, 1, len(mapped)) + assert.Equal(t, "foo", mapped[0].Name) + assert.Equal(t, []string{"foo", "6"}, mapped[0].Values) } diff --git a/website/docs/providers/ldap/index.md b/website/docs/providers/ldap/index.md index 419352d3a..e857636be 100644 --- a/website/docs/providers/ldap/index.md +++ b/website/docs/providers/ldap/index.md @@ -29,6 +29,7 @@ The following fields are currently sent for users: - "organizationalPerson" - "goauthentik.io/ldap/user" - `memberOf`: A list of all DNs that the user is a member of +- `homeDirectory`: A default home directory path for the user, by default `/home/$username`. Can be overwritten by setting `homeDirectory` as an attribute on users or groups. - `ak-active`: "true" if the account is active, otherwise "false" - `ak-superuser`: "true" if the account is part of a group with superuser permissions, otherwise "false"