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",
"app": "",
}).Inc()
return ldap.LDAPResultOperationsError, nil
return ldap.LDAPResultInsufficientAccessRights, nil
}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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