providers/ldap: making ldap compatible with synology (#4694)

* internal/outpost/ldap: making ldap compatible with synology

* fix duplicate attributes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add docs about homedirectory

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix duplicate attributes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add substitution to values

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
roche-quentin 2023-02-22 15:26:41 +01:00 committed by GitHub
parent 51c6a14786
commit cd99b6e48f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 156 additions and 43 deletions

View File

@ -49,5 +49,6 @@ func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LD
"reason": "no_provider", "reason": "no_provider",
"app": "", "app": "",
}).Inc() }).Inc()
return ldap.LDAPResultOperationsError, nil
return ldap.LDAPResultInsufficientAccessRights, nil
} }

View File

@ -12,6 +12,7 @@ const (
OCGroupOfNames = "groupOfNames" OCGroupOfNames = "groupOfNames"
OCAKGroup = "goauthentik.io/ldap/group" OCAKGroup = "goauthentik.io/ldap/group"
OCAKVirtualGroup = "goauthentik.io/ldap/virtual-group" OCAKVirtualGroup = "goauthentik.io/ldap/virtual-group"
OCPosixGroup = "posixGroup"
) )
const ( const (
@ -19,6 +20,7 @@ const (
OCOrgPerson = "organizationalPerson" OCOrgPerson = "organizationalPerson"
OCInetOrgPerson = "inetOrgPerson" OCInetOrgPerson = "inetOrgPerson"
OCAKUser = "goauthentik.io/ldap/user" OCAKUser = "goauthentik.io/ldap/user"
OCPosixAccount = "posixAccount"
) )
const ( const (
@ -47,6 +49,7 @@ func GetUserOCs() map[string]bool {
OCOrgPerson: true, OCOrgPerson: true,
OCInetOrgPerson: true, OCInetOrgPerson: true,
OCAKUser: true, OCAKUser: true,
OCPosixAccount: true,
} }
} }
@ -56,6 +59,7 @@ func GetGroupOCs() map[string]bool {
OCGroupOfUniqueNames: true, OCGroupOfUniqueNames: true,
OCGroupOfNames: true, OCGroupOfNames: true,
OCAKGroup: true, OCAKGroup: true,
OCPosixGroup: true,
} }
} }

View File

@ -1,7 +1,9 @@
package ldap package ldap
import ( import (
"fmt"
"strconv" "strconv"
"strings"
"github.com/nmcclain/ldap" "github.com/nmcclain/ldap"
"goauthentik.io/api/v3" "goauthentik.io/api/v3"
@ -11,9 +13,29 @@ import (
func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry { func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
dn := pi.GetUserDN(u.Username) dn := pi.GetUserDN(u.Username)
attrs := utils.AttributesToLDAP(u.Attributes, false) userValueMap := func(value []string) []string {
sanitizedAttrs := utils.AttributesToLDAP(u.Attributes, true) for i, v := range value {
attrs = append(attrs, sanitizedAttrs...) 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 { if u.IsActive == nil {
u.IsActive = api.PtrBool(false) 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}, "objectClass": {constants.OCUser, constants.OCOrgPerson, constants.OCInetOrgPerson, constants.OCAKUser},
"uidNumber": {pi.GetUidNumber(u)}, "uidNumber": {pi.GetUidNumber(u)},
"gidNumber": {pi.GetUidNumber(u)}, "gidNumber": {pi.GetUidNumber(u)},
"homeDirectory": {fmt.Sprintf("/home/%s", u.Username)},
"sn": {u.Name},
}) })
return &ldap.Entry{DN: dn, Attributes: attrs} return &ldap.Entry{DN: dn, Attributes: attrs}
} }

View File

@ -2,6 +2,7 @@ package group
import ( import (
"strconv" "strconv"
"strings"
"github.com/nmcclain/ldap" "github.com/nmcclain/ldap"
"goauthentik.io/api/v3" "goauthentik.io/api/v3"
@ -18,15 +19,31 @@ type LDAPGroup struct {
Member []string Member []string
IsSuperuser bool IsSuperuser bool
IsVirtualGroup bool IsVirtualGroup bool
AKAttributes map[string]interface{} Attributes map[string]interface{}
} }
func (lg *LDAPGroup) Entry() *ldap.Entry { func (lg *LDAPGroup) Entry() *ldap.Entry {
attrs := utils.AttributesToLDAP(lg.AKAttributes, false) attrs := utils.AttributesToLDAP(lg.Attributes, func(key string) string {
sanitizedAttrs := utils.AttributesToLDAP(lg.AKAttributes, true) return utils.AttributeKeySanitize(key)
attrs = append(attrs, sanitizedAttrs...) }, 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 { if lg.IsVirtualGroup {
objectClass = append(objectClass, constants.OCAKVirtualGroup) objectClass = append(objectClass, constants.OCAKVirtualGroup)
} }
@ -55,7 +72,7 @@ func FromAPIGroup(g api.Group, si server.LDAPServerInstance) *LDAPGroup {
Member: si.UsersForGroup(g), Member: si.UsersForGroup(g),
IsVirtualGroup: false, IsVirtualGroup: false,
IsSuperuser: *g.IsSuperuser, 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)}, Member: []string{si.GetUserDN(u.Username)},
IsVirtualGroup: true, IsVirtualGroup: true,
IsSuperuser: false, IsSuperuser: false,
AKAttributes: nil, Attributes: nil,
} }
} }

View File

@ -37,7 +37,24 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n
}() }()
if searchReq.BaseDN == "" { 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)) bd, err := goldap.ParseDN(strings.ToLower(searchReq.BaseDN))
if err != nil { if err != nil {
@ -51,5 +68,22 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n
return provider.searcher.Search(req) 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
} }

View File

@ -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{} attrList := []*ldap.EntryAttribute{}
if attrs == nil { if attrs == nil {
return attrList return attrList
} }
for attrKey, attrValue := range attrs { for attrKey, attrValue := range attrs {
entry := &ldap.EntryAttribute{Name: attrKey} entry := &ldap.EntryAttribute{Name: keyFormatter(attrKey)}
if sanitize {
entry.Name = AttributeKeySanitize(attrKey)
}
switch t := attrValue.(type) { switch t := attrValue.(type) {
case []string: case []string:
entry.Values = t entry.Values = valueFormatter(t)
case *[]string: case *[]string:
entry.Values = *t entry.Values = valueFormatter(*t)
case []interface{}: case []interface{}:
entry.Values = make([]string, len(t)) vv := make([]string, 0)
for idx, v := range t { for _, v := range t {
v := stringify(v) v := stringify(v)
if v != nil { if v != nil {
entry.Values[idx] = *v vv = append(vv, *v)
} }
} }
entry.Values = valueFormatter(vv)
default: default:
v := stringify(t) v := stringify(t)
if v != nil { if v != nil {
entry.Values = []string{*v} entry.Values = valueFormatter([]string{*v})
} }
} }
attrList = append(attrList, entry) 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 { func MustHaveAttribute(attrs []*ldap.EntryAttribute, name string, value []string) []*ldap.EntryAttribute {
shouldSet := true shouldSet := true
for _, attr := range attrs { for _, attr := range attrs {
if attr.Name == name { if strings.EqualFold(attr.Name, name) {
shouldSet = false shouldSet = false
} }
} }

View File

@ -19,16 +19,26 @@ func TestAKAttrsToLDAP_String(t *testing.T) {
u.Attributes = map[string]interface{}{ u.Attributes = map[string]interface{}{
"foo": "bar", "foo": "bar",
} }
assert.Equal(t, 1, len(AttributesToLDAP(u.Attributes, true))) mapped := AttributesToLDAP(u.Attributes, func(key string) string {
assert.Equal(t, "foo", AttributesToLDAP(u.Attributes, true)[0].Name) return AttributeKeySanitize(key)
assert.Equal(t, []string{"bar"}, AttributesToLDAP(u.Attributes, true)[0].Values) }, 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 // pointer string
u.Attributes = map[string]interface{}{ u.Attributes = map[string]interface{}{
"foo": api.PtrString("bar"), "foo": api.PtrString("bar"),
} }
assert.Equal(t, 1, len(AttributesToLDAP(u.Attributes, true))) mapped = AttributesToLDAP(u.Attributes, func(key string) string {
assert.Equal(t, "foo", AttributesToLDAP(u.Attributes, true)[0].Name) return AttributeKeySanitize(key)
assert.Equal(t, []string{"bar"}, AttributesToLDAP(u.Attributes, true)[0].Values) }, 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) { func TestAKAttrsToLDAP_String_List(t *testing.T) {
@ -37,16 +47,26 @@ func TestAKAttrsToLDAP_String_List(t *testing.T) {
u.Attributes = map[string]interface{}{ u.Attributes = map[string]interface{}{
"foo": []string{"bar"}, "foo": []string{"bar"},
} }
assert.Equal(t, 1, len(AttributesToLDAP(u.Attributes, true))) mapped := AttributesToLDAP(u.Attributes, func(key string) string {
assert.Equal(t, "foo", AttributesToLDAP(u.Attributes, true)[0].Name) return AttributeKeySanitize(key)
assert.Equal(t, []string{"bar"}, AttributesToLDAP(u.Attributes, true)[0].Values) }, 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 // pointer string list
u.Attributes = map[string]interface{}{ u.Attributes = map[string]interface{}{
"foo": &[]string{"bar"}, "foo": &[]string{"bar"},
} }
assert.Equal(t, 1, len(AttributesToLDAP(u.Attributes, true))) mapped = AttributesToLDAP(u.Attributes, func(key string) string {
assert.Equal(t, "foo", AttributesToLDAP(u.Attributes, true)[0].Name) return AttributeKeySanitize(key)
assert.Equal(t, []string{"bar"}, AttributesToLDAP(u.Attributes, true)[0].Values) }, 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) { func TestAKAttrsToLDAP_Dict(t *testing.T) {
@ -56,9 +76,14 @@ func TestAKAttrsToLDAP_Dict(t *testing.T) {
"foo": "bar", "foo": "bar",
}, },
} }
assert.Equal(t, 1, len(AttributesToLDAP(d, true))) mapped := AttributesToLDAP(d, func(key string) string {
assert.Equal(t, "foo", AttributesToLDAP(d, true)[0].Name) return AttributeKeySanitize(key)
assert.Equal(t, []string{"map[foo:bar]"}, AttributesToLDAP(d, true)[0].Values) }, 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) { func TestAKAttrsToLDAP_Mixed(t *testing.T) {
@ -69,7 +94,12 @@ func TestAKAttrsToLDAP_Mixed(t *testing.T) {
6, 6,
}, },
} }
assert.Equal(t, 1, len(AttributesToLDAP(d, true))) mapped := AttributesToLDAP(d, func(key string) string {
assert.Equal(t, "foo", AttributesToLDAP(d, true)[0].Name) return AttributeKeySanitize(key)
assert.Equal(t, []string{"foo", "6"}, AttributesToLDAP(d, true)[0].Values) }, 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)
} }

View File

@ -29,6 +29,7 @@ The following fields are currently sent for users:
- "organizationalPerson" - "organizationalPerson"
- "goauthentik.io/ldap/user" - "goauthentik.io/ldap/user"
- `memberOf`: A list of all DNs that the user is a member of - `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-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" - `ak-superuser`: "true" if the account is part of a group with superuser permissions, otherwise "false"