providers/ldap: Improve compatibility with LDAP clients (#4750)
* Fixed invalid LDAP attributes by replacing '.'s and '/'s with '-' * Leave old fields for now for backward compatibility * Add forgotten depreceated field * Fix tests * Fix tests * use shorter attribute names Signed-off-by: Jens Langhammer <jens@goauthentik.io> * sanitize attributes Signed-off-by: Jens Langhammer <jens@goauthentik.io> * keep both sanitized and unsanitized user fields Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add sanitized fields to test 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
75866406dc
commit
51c6a14786
|
@ -11,7 +11,9 @@ 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.AKAttrsToLDAP(u.Attributes)
|
attrs := utils.AttributesToLDAP(u.Attributes, false)
|
||||||
|
sanitizedAttrs := utils.AttributesToLDAP(u.Attributes, true)
|
||||||
|
attrs = append(attrs, sanitizedAttrs...)
|
||||||
|
|
||||||
if u.IsActive == nil {
|
if u.IsActive == nil {
|
||||||
u.IsActive = api.PtrBool(false)
|
u.IsActive = api.PtrBool(false)
|
||||||
|
@ -20,9 +22,13 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
|
||||||
u.Email = api.PtrString("")
|
u.Email = api.PtrString("")
|
||||||
}
|
}
|
||||||
attrs = utils.EnsureAttributes(attrs, map[string][]string{
|
attrs = utils.EnsureAttributes(attrs, map[string][]string{
|
||||||
"memberOf": pi.GroupsForUser(u),
|
// Old fields for backwards compatibility
|
||||||
"goauthentik.io/ldap/active": {strconv.FormatBool(*u.IsActive)},
|
"goauthentik.io/ldap/active": {strconv.FormatBool(*u.IsActive)},
|
||||||
"goauthentik.io/ldap/superuser": {strconv.FormatBool(u.IsSuperuser)},
|
"goauthentik.io/ldap/superuser": {strconv.FormatBool(u.IsSuperuser)},
|
||||||
|
// End old fields
|
||||||
|
"ak-active": {strconv.FormatBool(*u.IsActive)},
|
||||||
|
"ak-superuser": {strconv.FormatBool(u.IsSuperuser)},
|
||||||
|
"memberOf": pi.GroupsForUser(u),
|
||||||
"cn": {u.Username},
|
"cn": {u.Username},
|
||||||
"sAMAccountName": {u.Username},
|
"sAMAccountName": {u.Username},
|
||||||
"uid": {u.Uid},
|
"uid": {u.Uid},
|
||||||
|
|
|
@ -22,7 +22,9 @@ type LDAPGroup struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lg *LDAPGroup) Entry() *ldap.Entry {
|
func (lg *LDAPGroup) Entry() *ldap.Entry {
|
||||||
attrs := utils.AKAttrsToLDAP(lg.AKAttributes)
|
attrs := utils.AttributesToLDAP(lg.AKAttributes, false)
|
||||||
|
sanitizedAttrs := utils.AttributesToLDAP(lg.AKAttributes, true)
|
||||||
|
attrs = append(attrs, sanitizedAttrs...)
|
||||||
|
|
||||||
objectClass := []string{constants.OCGroup, constants.OCGroupOfUniqueNames, constants.OCGroupOfNames, constants.OCAKGroup}
|
objectClass := []string{constants.OCGroup, constants.OCGroupOfUniqueNames, constants.OCGroupOfNames, constants.OCAKGroup}
|
||||||
if lg.IsVirtualGroup {
|
if lg.IsVirtualGroup {
|
||||||
|
@ -30,9 +32,12 @@ func (lg *LDAPGroup) Entry() *ldap.Entry {
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs = utils.EnsureAttributes(attrs, map[string][]string{
|
attrs = utils.EnsureAttributes(attrs, map[string][]string{
|
||||||
|
// Old fields for backwards compatibility
|
||||||
|
"goauthentik.io/ldap/superuser": {strconv.FormatBool(lg.IsSuperuser)},
|
||||||
|
// End old fields
|
||||||
|
"ak-superuser": {strconv.FormatBool(lg.IsSuperuser)},
|
||||||
"objectClass": objectClass,
|
"objectClass": objectClass,
|
||||||
"member": lg.Member,
|
"member": lg.Member,
|
||||||
"goauthentik.io/ldap/superuser": {strconv.FormatBool(lg.IsSuperuser)},
|
|
||||||
"cn": {lg.CN},
|
"cn": {lg.CN},
|
||||||
"uid": {lg.Uid},
|
"uid": {lg.Uid},
|
||||||
"sAMAccountName": {lg.CN},
|
"sAMAccountName": {lg.CN},
|
||||||
|
|
|
@ -9,6 +9,14 @@ import (
|
||||||
ldapConstants "goauthentik.io/internal/outpost/ldap/constants"
|
ldapConstants "goauthentik.io/internal/outpost/ldap/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func AttributeKeySanitize(key string) string {
|
||||||
|
return strings.ReplaceAll(
|
||||||
|
strings.ReplaceAll(key, "/", "-"),
|
||||||
|
".",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func stringify(in interface{}) *string {
|
func stringify(in interface{}) *string {
|
||||||
switch t := in.(type) {
|
switch t := in.(type) {
|
||||||
case string:
|
case string:
|
||||||
|
@ -36,13 +44,16 @@ func stringify(in interface{}) *string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AKAttrsToLDAP(attrs map[string]interface{}) []*ldap.EntryAttribute {
|
func AttributesToLDAP(attrs map[string]interface{}, sanitize bool) []*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: attrKey}
|
||||||
|
if sanitize {
|
||||||
|
entry.Name = AttributeKeySanitize(attrKey)
|
||||||
|
}
|
||||||
switch t := attrValue.(type) {
|
switch t := attrValue.(type) {
|
||||||
case []string:
|
case []string:
|
||||||
entry.Values = t
|
entry.Values = t
|
||||||
|
|
|
@ -19,16 +19,16 @@ 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(AKAttrsToLDAP(u.Attributes)))
|
assert.Equal(t, 1, len(AttributesToLDAP(u.Attributes, true)))
|
||||||
assert.Equal(t, "foo", AKAttrsToLDAP(u.Attributes)[0].Name)
|
assert.Equal(t, "foo", AttributesToLDAP(u.Attributes, true)[0].Name)
|
||||||
assert.Equal(t, []string{"bar"}, AKAttrsToLDAP(u.Attributes)[0].Values)
|
assert.Equal(t, []string{"bar"}, AttributesToLDAP(u.Attributes, true)[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(AKAttrsToLDAP(u.Attributes)))
|
assert.Equal(t, 1, len(AttributesToLDAP(u.Attributes, true)))
|
||||||
assert.Equal(t, "foo", AKAttrsToLDAP(u.Attributes)[0].Name)
|
assert.Equal(t, "foo", AttributesToLDAP(u.Attributes, true)[0].Name)
|
||||||
assert.Equal(t, []string{"bar"}, AKAttrsToLDAP(u.Attributes)[0].Values)
|
assert.Equal(t, []string{"bar"}, AttributesToLDAP(u.Attributes, true)[0].Values)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAKAttrsToLDAP_String_List(t *testing.T) {
|
func TestAKAttrsToLDAP_String_List(t *testing.T) {
|
||||||
|
@ -37,16 +37,16 @@ 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(AKAttrsToLDAP(u.Attributes)))
|
assert.Equal(t, 1, len(AttributesToLDAP(u.Attributes, true)))
|
||||||
assert.Equal(t, "foo", AKAttrsToLDAP(u.Attributes)[0].Name)
|
assert.Equal(t, "foo", AttributesToLDAP(u.Attributes, true)[0].Name)
|
||||||
assert.Equal(t, []string{"bar"}, AKAttrsToLDAP(u.Attributes)[0].Values)
|
assert.Equal(t, []string{"bar"}, AttributesToLDAP(u.Attributes, true)[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(AKAttrsToLDAP(u.Attributes)))
|
assert.Equal(t, 1, len(AttributesToLDAP(u.Attributes, true)))
|
||||||
assert.Equal(t, "foo", AKAttrsToLDAP(u.Attributes)[0].Name)
|
assert.Equal(t, "foo", AttributesToLDAP(u.Attributes, true)[0].Name)
|
||||||
assert.Equal(t, []string{"bar"}, AKAttrsToLDAP(u.Attributes)[0].Values)
|
assert.Equal(t, []string{"bar"}, AttributesToLDAP(u.Attributes, true)[0].Values)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAKAttrsToLDAP_Dict(t *testing.T) {
|
func TestAKAttrsToLDAP_Dict(t *testing.T) {
|
||||||
|
@ -56,9 +56,9 @@ func TestAKAttrsToLDAP_Dict(t *testing.T) {
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
assert.Equal(t, 1, len(AKAttrsToLDAP(d)))
|
assert.Equal(t, 1, len(AttributesToLDAP(d, true)))
|
||||||
assert.Equal(t, "foo", AKAttrsToLDAP(d)[0].Name)
|
assert.Equal(t, "foo", AttributesToLDAP(d, true)[0].Name)
|
||||||
assert.Equal(t, []string{"map[foo:bar]"}, AKAttrsToLDAP(d)[0].Values)
|
assert.Equal(t, []string{"map[foo:bar]"}, AttributesToLDAP(d, true)[0].Values)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAKAttrsToLDAP_Mixed(t *testing.T) {
|
func TestAKAttrsToLDAP_Mixed(t *testing.T) {
|
||||||
|
@ -69,7 +69,7 @@ func TestAKAttrsToLDAP_Mixed(t *testing.T) {
|
||||||
6,
|
6,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
assert.Equal(t, 1, len(AKAttrsToLDAP(d)))
|
assert.Equal(t, 1, len(AttributesToLDAP(d, true)))
|
||||||
assert.Equal(t, "foo", AKAttrsToLDAP(d)[0].Name)
|
assert.Equal(t, "foo", AttributesToLDAP(d, true)[0].Name)
|
||||||
assert.Equal(t, []string{"foo", "6"}, AKAttrsToLDAP(d)[0].Values)
|
assert.Equal(t, []string{"foo", "6"}, AttributesToLDAP(d, true)[0].Values)
|
||||||
}
|
}
|
||||||
|
|
|
@ -225,10 +225,16 @@ class TestProviderLDAP(SeleniumTestCase):
|
||||||
"uidNumber": [str(2000 + o_user.pk)],
|
"uidNumber": [str(2000 + o_user.pk)],
|
||||||
"gidNumber": [str(2000 + o_user.pk)],
|
"gidNumber": [str(2000 + o_user.pk)],
|
||||||
"memberOf": [],
|
"memberOf": [],
|
||||||
|
# Old fields for backwards compatibility
|
||||||
"goauthentik.io/ldap/active": ["true"],
|
"goauthentik.io/ldap/active": ["true"],
|
||||||
"goauthentik.io/ldap/superuser": ["false"],
|
"goauthentik.io/ldap/superuser": ["false"],
|
||||||
"goauthentik.io/user/override-ips": ["true"],
|
"goauthentik.io/user/override-ips": ["true"],
|
||||||
"goauthentik.io/user/service-account": ["true"],
|
"goauthentik.io/user/service-account": ["true"],
|
||||||
|
# End old fields
|
||||||
|
"ak-active": ["true"],
|
||||||
|
"ak-superuser": ["false"],
|
||||||
|
"goauthentikio-user-override-ips": ["true"],
|
||||||
|
"goauthentikio-user-service-account": ["true"],
|
||||||
},
|
},
|
||||||
"type": "searchResEntry",
|
"type": "searchResEntry",
|
||||||
},
|
},
|
||||||
|
@ -250,10 +256,16 @@ class TestProviderLDAP(SeleniumTestCase):
|
||||||
"uidNumber": [str(2000 + embedded_account.pk)],
|
"uidNumber": [str(2000 + embedded_account.pk)],
|
||||||
"gidNumber": [str(2000 + embedded_account.pk)],
|
"gidNumber": [str(2000 + embedded_account.pk)],
|
||||||
"memberOf": [],
|
"memberOf": [],
|
||||||
|
# Old fields for backwards compatibility
|
||||||
"goauthentik.io/ldap/active": ["true"],
|
"goauthentik.io/ldap/active": ["true"],
|
||||||
"goauthentik.io/ldap/superuser": ["false"],
|
"goauthentik.io/ldap/superuser": ["false"],
|
||||||
"goauthentik.io/user/override-ips": ["true"],
|
"goauthentik.io/user/override-ips": ["true"],
|
||||||
"goauthentik.io/user/service-account": ["true"],
|
"goauthentik.io/user/service-account": ["true"],
|
||||||
|
# End old fields
|
||||||
|
"ak-active": ["true"],
|
||||||
|
"ak-superuser": ["false"],
|
||||||
|
"goauthentikio-user-override-ips": ["true"],
|
||||||
|
"goauthentikio-user-service-account": ["true"],
|
||||||
},
|
},
|
||||||
"type": "searchResEntry",
|
"type": "searchResEntry",
|
||||||
},
|
},
|
||||||
|
@ -278,8 +290,12 @@ class TestProviderLDAP(SeleniumTestCase):
|
||||||
f"cn={group.name},ou=groups,dc=ldap,dc=goauthentik,dc=io"
|
f"cn={group.name},ou=groups,dc=ldap,dc=goauthentik,dc=io"
|
||||||
for group in self.user.ak_groups.all()
|
for group in self.user.ak_groups.all()
|
||||||
],
|
],
|
||||||
|
# Old fields for backwards compatibility
|
||||||
"goauthentik.io/ldap/active": ["true"],
|
"goauthentik.io/ldap/active": ["true"],
|
||||||
"goauthentik.io/ldap/superuser": ["true"],
|
"goauthentik.io/ldap/superuser": ["true"],
|
||||||
|
# End old fields
|
||||||
|
"ak-active": ["true"],
|
||||||
|
"ak-superuser": ["true"],
|
||||||
"extraAttribute": ["bar"],
|
"extraAttribute": ["bar"],
|
||||||
},
|
},
|
||||||
"type": "searchResEntry",
|
"type": "searchResEntry",
|
||||||
|
|
|
@ -29,8 +29,13 @@ 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
|
||||||
- `goauthentik.io/ldap/active`: "true" if the account is active, otherwise "false"
|
- `ak-active`: "true" if the account is active, otherwise "false"
|
||||||
- `goauthentik.io/ldap/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"
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
The use of the `goauthentik.io/ldap/active` and `goauthentik.io/ldap/superuser` attributes is deprecated as of authentik 2023.3. They will be removed completely in a future release.
|
||||||
|
Use the replacements fields above instead.
|
||||||
|
:::
|
||||||
|
|
||||||
The following fields are current set for groups:
|
The following fields are current set for groups:
|
||||||
|
|
||||||
|
@ -51,6 +56,10 @@ The virtual groups gidNumber is equal to the uidNumber of the user.
|
||||||
Starting with 2021.9.1, custom attributes will override the inbuilt attributes.
|
Starting with 2021.9.1, custom attributes will override the inbuilt attributes.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
:::info
|
||||||
|
Starting with 2023.3, periods and slashes in custom attributes will be sanitized.
|
||||||
|
:::
|
||||||
|
|
||||||
## SSL
|
## SSL
|
||||||
|
|
||||||
You can also configure SSL for your LDAP Providers by selecting a certificate and a server name in the provider settings.
|
You can also configure SSL for your LDAP Providers by selecting a certificate and a server name in the provider settings.
|
||||||
|
|
Reference in New Issue