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:
parent
51c6a14786
commit
cd99b6e48f
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
Reference in New Issue