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",
|
"reason": "no_provider",
|
||||||
"app": "",
|
"app": "",
|
||||||
}).Inc()
|
}).Inc()
|
||||||
return ldap.LDAPResultOperationsError, nil
|
|
||||||
|
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
Reference in New Issue