diff --git a/Pipfile b/Pipfile index 0cf076c36f..a61d34a7c7 100644 --- a/Pipfile +++ b/Pipfile @@ -47,6 +47,7 @@ xmlsec = "*" duo-client = "*" ua-parser = "*" deepmerge = "*" +colorama = "*" [requires] python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock index b2579ee3d7..d88d050a4e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f90d9fb4713eaf9c5ffe6a3858e64843670f79ab5007e7debf914c1f094c8d63" + "sha256": "e4f2e57bd5c709809515ab2b95eb3f5fa337d4a9334f4110a24bf28c3f9d5f8f" }, "pipfile-spec": 6, "requires": { @@ -288,6 +288,14 @@ ], "version": "==0.2.0" }, + "colorama": { + "hashes": [ + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + ], + "index": "pypi", + "version": "==0.4.4" + }, "constantly": { "hashes": [ "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35", diff --git a/authentik/providers/ldap/api.py b/authentik/providers/ldap/api.py index a0612b1ea6..01f49fdc4f 100644 --- a/authentik/providers/ldap/api.py +++ b/authentik/providers/ldap/api.py @@ -19,6 +19,8 @@ class LDAPProviderSerializer(ProviderSerializer): "search_group", "certificate", "tls_server_name", + "uid_start_number", + "gid_start_number", ] @@ -48,6 +50,8 @@ class LDAPOutpostConfigSerializer(ModelSerializer): "search_group", "certificate", "tls_server_name", + "uid_start_number", + "gid_start_number", ] diff --git a/authentik/providers/ldap/migrations/0004_auto_20210713_2115.py b/authentik/providers/ldap/migrations/0004_auto_20210713_2115.py new file mode 100644 index 0000000000..1fd0bc2564 --- /dev/null +++ b/authentik/providers/ldap/migrations/0004_auto_20210713_2115.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.5 on 2021-07-13 21:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_providers_ldap", "0003_auto_20210713_1138"), + ] + + operations = [ + migrations.AddField( + model_name="ldapprovider", + name="gid_start_number", + field=models.IntegerField( + default=2000, + help_text="The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 2000 to ensure that we don't collide with local groups gidNumber", + ), + ), + migrations.AddField( + model_name="ldapprovider", + name="uid_start_number", + field=models.IntegerField( + default=2000, + help_text="The start for uidNumbers, this number is added to the user.Pk to make sure that the numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't collide with local users uidNumber", + ), + ), + ] diff --git a/authentik/providers/ldap/migrations/0005_alter_ldapprovider_gid_start_number.py b/authentik/providers/ldap/migrations/0005_alter_ldapprovider_gid_start_number.py new file mode 100644 index 0000000000..34cfb44b88 --- /dev/null +++ b/authentik/providers/ldap/migrations/0005_alter_ldapprovider_gid_start_number.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.5 on 2021-07-14 06:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_providers_ldap", "0004_auto_20210713_2115"), + ] + + operations = [ + migrations.AlterField( + model_name="ldapprovider", + name="gid_start_number", + field=models.IntegerField( + default=4000, + help_text="The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber", + ), + ), + ] diff --git a/authentik/providers/ldap/models.py b/authentik/providers/ldap/models.py index 7cf6630e67..4f871f65d1 100644 --- a/authentik/providers/ldap/models.py +++ b/authentik/providers/ldap/models.py @@ -40,6 +40,22 @@ class LDAPProvider(OutpostModel, Provider): blank=True, ) + uid_start_number = models.IntegerField( + default=2000, + help_text=_( + "The start for uidNumbers, this number is added to the user.Pk to make sure that the numbers aren't too low for POSIX users. " + "Default is 2000 to ensure that we don't collide with local users uidNumber" + ), + ) + + gid_start_number = models.IntegerField( + default=4000, + help_text=_( + "The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. " + "Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber" + ), + ) + @property def launch_url(self) -> Optional[str]: """LDAP never has a launch URL""" diff --git a/outpost/pkg/ldap/api.go b/outpost/pkg/ldap/api.go index dcfde8fdb2..77a4da4529 100644 --- a/outpost/pkg/ldap/api.go +++ b/outpost/pkg/ldap/api.go @@ -37,8 +37,10 @@ func (ls *LDAPServer) Refresh() error { boundUsersMutex: sync.RWMutex{}, boundUsers: make(map[string]UserFlags), s: ls, - log: logger, + log: log.WithField("logger", "authentik.outpost.ldap").WithField("provider", provider.Name), tlsServerName: provider.TlsServerName, + uidStartNumber: *provider.UidStartNumber, + gidStartNumber: *provider.GidStartNumber, } if provider.Certificate.Get() != nil { logger.WithField("provider", provider.Name).Debug("Enabling TLS") diff --git a/outpost/pkg/ldap/instance_search.go b/outpost/pkg/ldap/instance_search.go index 2f03605115..8b10de5d16 100644 --- a/outpost/pkg/ldap/instance_search.go +++ b/outpost/pkg/ldap/instance_search.go @@ -54,8 +54,18 @@ func (pi *ProviderInstance) Search(bindDN string, searchReq ldap.SearchRequest, return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err) } pi.log.WithField("count", len(groups.Results)).Trace("Got results from API") + for _, g := range groups.Results { - entries = append(entries, pi.GroupEntry(g)) + entries = append(entries, pi.GroupEntry(pi.APIGroupToLDAPGroup(g))) + } + + users, _, err := pi.s.ac.Client.CoreApi.CoreUsersList(context.Background()).Execute() + if err != nil { + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err) + } + + for _, u := range users.Results { + entries = append(entries, pi.GroupEntry(pi.APIUserToLDAPGroup(u))) } case UserObjectClass, "": users, _, err := pi.s.ac.Client.CoreApi.CoreUsersList(context.Background()).Execute() @@ -96,6 +106,14 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry { Name: "objectClass", Values: []string{UserObjectClass, "organizationalPerson", "goauthentik.io/ldap/user"}, }, + { + Name: "uidNumber", + Values: []string{ pi.GetUidNumber(u) }, + }, + { + Name: "gidNumber", + Values: []string{ pi.GetUidNumber(u) }, + }, } attrs = append(attrs, &ldap.EntryAttribute{Name: "memberOf", Values: pi.GroupsForUser(u)}) @@ -114,26 +132,40 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry { return &ldap.Entry{DN: dn, Attributes: attrs} } -func (pi *ProviderInstance) GroupEntry(g api.Group) *ldap.Entry { +func (pi *ProviderInstance) GroupEntry(g LDAPGroup) *ldap.Entry { attrs := []*ldap.EntryAttribute{ { Name: "cn", - Values: []string{g.Name}, + Values: []string{g.cn}, }, { Name: "uid", - Values: []string{string(g.Pk)}, + Values: []string{g.uid}, }, { - Name: "objectClass", - Values: []string{GroupObjectClass, "goauthentik.io/ldap/group"}, + Name: "gidNumber", + Values: []string{ g.gidNumber }, }, } - attrs = append(attrs, &ldap.EntryAttribute{Name: "member", Values: pi.UsersForGroup(g)}) - attrs = append(attrs, &ldap.EntryAttribute{Name: "goauthentik.io/ldap/superuser", Values: []string{BoolToString(*g.IsSuperuser)}}) - attrs = append(attrs, AKAttrsToLDAP(g.Attributes)...) + if (g.isVirtualGroup) { + attrs = append(attrs, &ldap.EntryAttribute{ + Name: "objectClass", + Values: []string{GroupObjectClass, "goauthentik.io/ldap/group", "goauthentik.io/ldap/virtual-group"}, + }) + } else { + attrs = append(attrs, &ldap.EntryAttribute{ + Name: "objectClass", + Values: []string{GroupObjectClass, "goauthentik.io/ldap/group"}, + }) + } - dn := pi.GetGroupDN(g) - return &ldap.Entry{DN: dn, Attributes: attrs} + attrs = append(attrs, &ldap.EntryAttribute{Name: "member", Values: g.member}) + attrs = append(attrs, &ldap.EntryAttribute{Name: "goauthentik.io/ldap/superuser", Values: []string{BoolToString(g.isSuperuser)}}) + + if (g.akAttributes != nil) { + attrs = append(attrs, AKAttrsToLDAP(g.akAttributes)...) + } + + return &ldap.Entry{DN: g.dn, Attributes: attrs} } diff --git a/outpost/pkg/ldap/ldap.go b/outpost/pkg/ldap/ldap.go index 0a8ad7505b..1fa6ba9f4a 100644 --- a/outpost/pkg/ldap/ldap.go +++ b/outpost/pkg/ldap/ldap.go @@ -32,6 +32,9 @@ type ProviderInstance struct { searchAllowedGroups []*strfmt.UUID boundUsersMutex sync.RWMutex boundUsers map[string]UserFlags + + uidStartNumber int32 + gidStartNumber int32 } type UserFlags struct { @@ -47,6 +50,17 @@ type LDAPServer struct { providers []*ProviderInstance } +type LDAPGroup struct { + dn string + cn string + uid string + gidNumber string + member []string + isSuperuser bool + isVirtualGroup bool + akAttributes interface{} +} + func NewServer(ac *ak.APIController) *LDAPServer { s := ldap.NewServer() s.EnforceLDAP = true diff --git a/outpost/pkg/ldap/utils.go b/outpost/pkg/ldap/utils.go index 685efa4878..a27587f5ef 100644 --- a/outpost/pkg/ldap/utils.go +++ b/outpost/pkg/ldap/utils.go @@ -2,6 +2,9 @@ package ldap import ( "fmt" + "strings" + "math/big" + "strconv" "reflect" "github.com/nmcclain/ldap" @@ -77,6 +80,34 @@ func (pi *ProviderInstance) UsersForGroup(group api.Group) []string { return users } +func (pi *ProviderInstance) APIGroupToLDAPGroup(g api.Group) LDAPGroup { + return LDAPGroup{ + dn: pi.GetGroupDN(g), + cn: g.Name, + uid: string(g.Pk), + gidNumber: pi.GetGidNumber(g), + member: pi.UsersForGroup(g), + isVirtualGroup: false, + isSuperuser: *g.IsSuperuser, + akAttributes: g.Attributes, + } +} + +func (pi *ProviderInstance) APIUserToLDAPGroup(u api.User) LDAPGroup { + dn := fmt.Sprintf("cn=%s,%s", u.Username, pi.GroupDN) + + return LDAPGroup{ + dn: dn, + cn: u.Username, + uid: u.Uid, + gidNumber: pi.GetUidNumber(u), + member: []string{dn}, + isVirtualGroup: true, + isSuperuser: false, + akAttributes: nil, + } +} + func (pi *ProviderInstance) GetUserDN(user string) string { return fmt.Sprintf("cn=%s,%s", user, pi.UserDN) } @@ -84,3 +115,26 @@ func (pi *ProviderInstance) GetUserDN(user string) string { func (pi *ProviderInstance) GetGroupDN(group api.Group) string { return fmt.Sprintf("cn=%s,%s", group.Name, pi.GroupDN) } + +func (pi *ProviderInstance) GetUidNumber(user api.User) string { + return strconv.FormatInt(int64(pi.uidStartNumber + user.Pk), 10) +} + +func (pi *ProviderInstance) GetGidNumber(group api.Group) string { + return strconv.FormatInt(int64(pi.gidStartNumber + pi.GetRIDForGroup(group.Pk)), 10) +} + +func (pi *ProviderInstance) GetRIDForGroup(uid string) int32 { + var i big.Int + i.SetString(strings.Replace(uid, "-", "", -1), 16) + intStr := i.String() + + // Get the last 5 characters/digits of the int-version of the UUID + gid, err := strconv.Atoi(intStr[len(intStr)-5:]) + + if err != nil { + panic(err) + } + + return int32(gid) +} diff --git a/schema.yml b/schema.yml index 6230104020..051c315473 100644 --- a/schema.yml +++ b/schema.yml @@ -20497,6 +20497,10 @@ components: nullable: true tls_server_name: type: string + uid_start_number: + type: integer + gid_start_number: + type: integer required: - application_slug - bind_flow_slug @@ -20615,6 +20619,10 @@ components: nullable: true tls_server_name: type: string + uid_start_number: + type: integer + gid_start_number: + type: integer required: - assigned_application_name - assigned_application_slug @@ -20654,6 +20662,10 @@ components: nullable: true tls_server_name: type: string + uid_start_number: + type: integer + gid_start_number: + type: integer required: - authorization_flow - name @@ -24996,6 +25008,10 @@ components: nullable: true tls_server_name: type: string + uid_start_number: + type: integer + gid_start_number: + type: integer PatchedLDAPSourceRequest: type: object description: LDAP Source Serializer diff --git a/web/src/pages/providers/ldap/LDAPProviderForm.ts b/web/src/pages/providers/ldap/LDAPProviderForm.ts index 3fe4c22e30..cf5bf656ce 100644 --- a/web/src/pages/providers/ldap/LDAPProviderForm.ts +++ b/web/src/pages/providers/ldap/LDAPProviderForm.ts @@ -92,7 +92,7 @@ export class LDAPProviderFormPage extends ModelForm { + name="tlsServerName">

${t`Server name for which this provider's certificate is valid for.`}

@@ -111,6 +111,20 @@ export class LDAPProviderFormPage extends ModelForm { }), html``)} + + +

${t`The start for uidNumbers, this number is added to the user.Pk to make sure that the numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't collide with local users uidNumber`}

+
+ + +

${t`The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber`}

+
`; diff --git a/website/docs/outposts/ldap/ldap.md b/website/docs/outposts/ldap/ldap.md index 7e8d92214e..661747d36f 100644 --- a/website/docs/outposts/ldap/ldap.md +++ b/website/docs/outposts/ldap/ldap.md @@ -33,6 +33,7 @@ The following fields are currently sent for users: - `cn`: User's username - `uid`: Unique user identifier +- `uidNumber`: A unique numeric identifier for the user - `name`: User's name - `displayName`: User's name - `mail`: User's email address @@ -48,12 +49,16 @@ The following fields are current set for groups: - `cn`: The group's name - `uid`: Unique group identifier -- `member`: A list of all DNs of the group's members +- `gidNumber`: A unique numeric identifier for the group +- `member`: A list of all DNs of the groups members - `objectClass`: A list of these strings: - "group" - "goauthentik.io/ldap/group" -**Additionally**, for both users and groups, any attributes you set are also present as LDAP Attributes. +A virtual group is also created for each user, they have the same fields as groups but have an additional objectClass: `goauthentik.io/ldap/group`. +The virtual groups gidNumber is equal to the uidNumber of the user. + +**Additionally**, for both users and (non-virtual) groups, any attributes you set are also present as LDAP Attributes. ## SSL