providers/ldap: Added auto-generated uidNumber and guidNumber generated attributes for use with SSSD and similar software. (#1138)

* Added auto-generated uidNumber and guidNumber generated attributes for
use with SSSD and similar software.

The starting number for uid/gid can be configured iva environtment
variables and is by default 2000 which should work fine for most instances unless there are more than
999 local accounts on the server/computer.

The uidNumber is just the users Pk + the starting number.
The guidNumber is calculated by the last couple of bytes in the uuid of
the group + the starting number, this should have a low enough chance
for collisions that it's going to be fine for most use cases.

I have not added any interface stuff for configuring the environment variables as I couldn't really find my way around all the places I'd have to edit to add it and the default values should in my opinion be fine for 99% use cases.

* Add a 'fake' primary group for each user

* First attempt att adding config to interface

* Updated API to support new fields

* Refactor code, update documentation and remove obsolete comment

Simplify `GetRIDForGroup`, was a bit overcomplicated before.

Add an additional class/struct `LDAPGroup` which is the new argument
for `pi.GroupEntry` and util functions to create `LDAPGroup` from api.Group and api.User

Add proper support in the interface for changing gidNumber and uidNumber starting points

* make lint-fix for the migration files
This commit is contained in:
Lukas Söder 2021-07-14 09:17:01 +02:00 committed by GitHub
parent 7fd78a591d
commit 7f39399c32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 232 additions and 16 deletions

View File

@ -47,6 +47,7 @@ xmlsec = "*"
duo-client = "*" duo-client = "*"
ua-parser = "*" ua-parser = "*"
deepmerge = "*" deepmerge = "*"
colorama = "*"
[requires] [requires]
python_version = "3.9" python_version = "3.9"

10
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "f90d9fb4713eaf9c5ffe6a3858e64843670f79ab5007e7debf914c1f094c8d63" "sha256": "e4f2e57bd5c709809515ab2b95eb3f5fa337d4a9334f4110a24bf28c3f9d5f8f"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -288,6 +288,14 @@
], ],
"version": "==0.2.0" "version": "==0.2.0"
}, },
"colorama": {
"hashes": [
"sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
"sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
],
"index": "pypi",
"version": "==0.4.4"
},
"constantly": { "constantly": {
"hashes": [ "hashes": [
"sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35", "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35",

View File

@ -19,6 +19,8 @@ class LDAPProviderSerializer(ProviderSerializer):
"search_group", "search_group",
"certificate", "certificate",
"tls_server_name", "tls_server_name",
"uid_start_number",
"gid_start_number",
] ]
@ -48,6 +50,8 @@ class LDAPOutpostConfigSerializer(ModelSerializer):
"search_group", "search_group",
"certificate", "certificate",
"tls_server_name", "tls_server_name",
"uid_start_number",
"gid_start_number",
] ]

View File

@ -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",
),
),
]

View File

@ -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",
),
),
]

View File

@ -40,6 +40,22 @@ class LDAPProvider(OutpostModel, Provider):
blank=True, 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 @property
def launch_url(self) -> Optional[str]: def launch_url(self) -> Optional[str]:
"""LDAP never has a launch URL""" """LDAP never has a launch URL"""

View File

@ -37,8 +37,10 @@ func (ls *LDAPServer) Refresh() error {
boundUsersMutex: sync.RWMutex{}, boundUsersMutex: sync.RWMutex{},
boundUsers: make(map[string]UserFlags), boundUsers: make(map[string]UserFlags),
s: ls, s: ls,
log: logger, log: log.WithField("logger", "authentik.outpost.ldap").WithField("provider", provider.Name),
tlsServerName: provider.TlsServerName, tlsServerName: provider.TlsServerName,
uidStartNumber: *provider.UidStartNumber,
gidStartNumber: *provider.GidStartNumber,
} }
if provider.Certificate.Get() != nil { if provider.Certificate.Get() != nil {
logger.WithField("provider", provider.Name).Debug("Enabling TLS") logger.WithField("provider", provider.Name).Debug("Enabling TLS")

View File

@ -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) return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err)
} }
pi.log.WithField("count", len(groups.Results)).Trace("Got results from API") pi.log.WithField("count", len(groups.Results)).Trace("Got results from API")
for _, g := range groups.Results { 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, "": case UserObjectClass, "":
users, _, err := pi.s.ac.Client.CoreApi.CoreUsersList(context.Background()).Execute() 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", Name: "objectClass",
Values: []string{UserObjectClass, "organizationalPerson", "goauthentik.io/ldap/user"}, 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)}) 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} 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{ attrs := []*ldap.EntryAttribute{
{ {
Name: "cn", Name: "cn",
Values: []string{g.Name}, Values: []string{g.cn},
}, },
{ {
Name: "uid", Name: "uid",
Values: []string{string(g.Pk)}, Values: []string{g.uid},
}, },
{ {
Name: "objectClass", Name: "gidNumber",
Values: []string{GroupObjectClass, "goauthentik.io/ldap/group"}, 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) attrs = append(attrs, &ldap.EntryAttribute{Name: "member", Values: g.member})
return &ldap.Entry{DN: dn, Attributes: attrs} 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}
} }

View File

@ -32,6 +32,9 @@ type ProviderInstance struct {
searchAllowedGroups []*strfmt.UUID searchAllowedGroups []*strfmt.UUID
boundUsersMutex sync.RWMutex boundUsersMutex sync.RWMutex
boundUsers map[string]UserFlags boundUsers map[string]UserFlags
uidStartNumber int32
gidStartNumber int32
} }
type UserFlags struct { type UserFlags struct {
@ -47,6 +50,17 @@ type LDAPServer struct {
providers []*ProviderInstance 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 { func NewServer(ac *ak.APIController) *LDAPServer {
s := ldap.NewServer() s := ldap.NewServer()
s.EnforceLDAP = true s.EnforceLDAP = true

View File

@ -2,6 +2,9 @@ package ldap
import ( import (
"fmt" "fmt"
"strings"
"math/big"
"strconv"
"reflect" "reflect"
"github.com/nmcclain/ldap" "github.com/nmcclain/ldap"
@ -77,6 +80,34 @@ func (pi *ProviderInstance) UsersForGroup(group api.Group) []string {
return users 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 { func (pi *ProviderInstance) GetUserDN(user string) string {
return fmt.Sprintf("cn=%s,%s", user, pi.UserDN) 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 { func (pi *ProviderInstance) GetGroupDN(group api.Group) string {
return fmt.Sprintf("cn=%s,%s", group.Name, pi.GroupDN) 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)
}

View File

@ -20497,6 +20497,10 @@ components:
nullable: true nullable: true
tls_server_name: tls_server_name:
type: string type: string
uid_start_number:
type: integer
gid_start_number:
type: integer
required: required:
- application_slug - application_slug
- bind_flow_slug - bind_flow_slug
@ -20615,6 +20619,10 @@ components:
nullable: true nullable: true
tls_server_name: tls_server_name:
type: string type: string
uid_start_number:
type: integer
gid_start_number:
type: integer
required: required:
- assigned_application_name - assigned_application_name
- assigned_application_slug - assigned_application_slug
@ -20654,6 +20662,10 @@ components:
nullable: true nullable: true
tls_server_name: tls_server_name:
type: string type: string
uid_start_number:
type: integer
gid_start_number:
type: integer
required: required:
- authorization_flow - authorization_flow
- name - name
@ -24996,6 +25008,10 @@ components:
nullable: true nullable: true
tls_server_name: tls_server_name:
type: string type: string
uid_start_number:
type: integer
gid_start_number:
type: integer
PatchedLDAPSourceRequest: PatchedLDAPSourceRequest:
type: object type: object
description: LDAP Source Serializer description: LDAP Source Serializer

View File

@ -92,7 +92,7 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
label=${t`TLS Server name`} label=${t`TLS Server name`}
name="baseDn"> name="tlsServerName">
<input type="text" value="${first(this.instance?.tlsServerName, "")}" class="pf-c-form-control"> <input type="text" value="${first(this.instance?.tlsServerName, "")}" class="pf-c-form-control">
<p class="pf-c-form__helper-text">${t`Server name for which this provider's certificate is valid for.`}</p> <p class="pf-c-form__helper-text">${t`Server name for which this provider's certificate is valid for.`}</p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
@ -111,6 +111,20 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
}), html`<option>${t`Loading...`}</option>`)} }), html`<option>${t`Loading...`}</option>`)}
</select> </select>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`UID start number`}
?required=${true}
name="uidStartNumber">
<input type="number" value="${first(this.instance?.uidStartNumber, 2000)}" class="pf-c-form-control" required>
<p class="pf-c-form__helper-text">${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`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`GID start number`}
?required=${true}
name="gidStartNumber">
<input type="number" value="${first(this.instance?.gidStartNumber, 4000)}" class="pf-c-form-control" required>
<p class="pf-c-form__helper-text">${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`}</p>
</ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
</form>`; </form>`;

View File

@ -33,6 +33,7 @@ The following fields are currently sent for users:
- `cn`: User's username - `cn`: User's username
- `uid`: Unique user identifier - `uid`: Unique user identifier
- `uidNumber`: A unique numeric identifier for the user
- `name`: User's name - `name`: User's name
- `displayName`: User's name - `displayName`: User's name
- `mail`: User's email address - `mail`: User's email address
@ -48,12 +49,16 @@ The following fields are current set for groups:
- `cn`: The group's name - `cn`: The group's name
- `uid`: Unique group identifier - `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: - `objectClass`: A list of these strings:
- "group" - "group"
- "goauthentik.io/ldap/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 ## SSL