Map explicit tenant groups into OIDC claims
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 26s
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 26s
This commit is contained in:
parent
993a4dd589
commit
e8b4eded88
3 changed files with 96 additions and 3 deletions
|
|
@ -290,6 +290,7 @@ func mapEntryToUser(entry *ldap.Entry) domain.User {
|
|||
for _, dn := range memberOfs {
|
||||
groups = append(groups, groupNameFromDN(dn))
|
||||
}
|
||||
tenant, roles := identityEnvelopeFromGroups(groups)
|
||||
|
||||
return domain.User{
|
||||
ID: entry.DN,
|
||||
|
|
@ -297,10 +298,48 @@ func mapEntryToUser(entry *ldap.Entry) domain.User {
|
|||
DisplayName: displayName,
|
||||
Email: entry.GetAttributeValue("mail"),
|
||||
Groups: groups,
|
||||
Roles: roles,
|
||||
Tenant: tenant,
|
||||
Enabled: true, // LLDAP does not expose a disabled flag in base schema
|
||||
}
|
||||
}
|
||||
|
||||
// identityEnvelopeFromGroups maps explicit tenant membership groups to the
|
||||
// coarse IAM Profile envelope. Fine-grained authorization remains flex-auth's
|
||||
// responsibility. Supported names are:
|
||||
//
|
||||
// tenant:<kind>:<slug>:users
|
||||
// tenant:<kind>:<slug>:admins
|
||||
//
|
||||
// Multiple tenant envelopes are deliberately ignored because an interactive
|
||||
// token must carry one unambiguous active tenant.
|
||||
func identityEnvelopeFromGroups(groups []string) (string, []string) {
|
||||
tenants := make(map[string]bool)
|
||||
admins := make(map[string]bool)
|
||||
for _, group := range groups {
|
||||
switch {
|
||||
case strings.HasPrefix(group, "tenant:") && strings.HasSuffix(group, ":users"):
|
||||
tenants[strings.TrimSuffix(group, ":users")] = true
|
||||
case strings.HasPrefix(group, "tenant:") && strings.HasSuffix(group, ":admins"):
|
||||
tenant := strings.TrimSuffix(group, ":admins")
|
||||
tenants[tenant] = true
|
||||
admins[tenant] = true
|
||||
}
|
||||
}
|
||||
if len(tenants) != 1 {
|
||||
return "", []string{}
|
||||
}
|
||||
var tenant string
|
||||
for candidate := range tenants {
|
||||
tenant = candidate
|
||||
}
|
||||
roles := []string{"user"}
|
||||
if admins[tenant] {
|
||||
roles = append(roles, "tenant-admin")
|
||||
}
|
||||
return tenant, roles
|
||||
}
|
||||
|
||||
// groupNameFromDN extracts the cn value from an LDAP DN such as
|
||||
// "cn=admins,ou=groups,dc=netkingdom,dc=local" → "admins".
|
||||
// If parsing fails the full DN is returned unchanged.
|
||||
|
|
|
|||
|
|
@ -119,6 +119,54 @@ func TestLookupUser_Success(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLookupUser_MapsExplicitTenantAdminGroup(t *testing.T) {
|
||||
conn := &mockConn{
|
||||
searchFn: func(req *ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
return singleEntryResult(
|
||||
"uid=bernd,ou=people,dc=test,dc=local",
|
||||
"bernd", "Bernd", "Worsch", "bernd.worsch@binky-hedgehog.com",
|
||||
[]string{
|
||||
"cn=tenant:friendly:binky:users,ou=groups,dc=test,dc=local",
|
||||
"cn=tenant:friendly:binky:admins,ou=groups,dc=test,dc=local",
|
||||
},
|
||||
), nil
|
||||
},
|
||||
}
|
||||
adapter := makeAdapter(testConfig(), conn)
|
||||
user, err := adapter.LookupUser(context.Background(), "bernd")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if user.Tenant != "tenant:friendly:binky" {
|
||||
t.Fatalf("tenant = %q", user.Tenant)
|
||||
}
|
||||
if len(user.Roles) != 2 || user.Roles[1] != "tenant-admin" {
|
||||
t.Fatalf("roles = %v", user.Roles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupUser_IgnoresAmbiguousTenantGroups(t *testing.T) {
|
||||
conn := &mockConn{
|
||||
searchFn: func(req *ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
return singleEntryResult(
|
||||
"uid=multi,ou=people,dc=test,dc=local",
|
||||
"multi", "Multi", "Tenant", "multi@example.test",
|
||||
[]string{
|
||||
"cn=tenant:friendly:binky:users,ou=groups,dc=test,dc=local",
|
||||
"cn=tenant:coulomb:users,ou=groups,dc=test,dc=local",
|
||||
},
|
||||
), nil
|
||||
},
|
||||
}
|
||||
user, err := makeAdapter(testConfig(), conn).LookupUser(context.Background(), "multi")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if user.Tenant != "" || len(user.Roles) != 0 {
|
||||
t.Fatalf("ambiguous envelope must be empty, got tenant=%q roles=%v", user.Tenant, user.Roles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupUser_DisplayName_FallsBackToSN(t *testing.T) {
|
||||
dn := "uid=bob,ou=users,dc=netkingdom,dc=local"
|
||||
conn := &mockConn{
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ not move user-domain or authorization ownership into KeyCape.
|
|||
|
||||
```task
|
||||
id: KEY-WP-0007-T01
|
||||
status: progress
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ implicit flow, client secret, or dynamic registration is allowed.
|
|||
|
||||
```task
|
||||
id: KEY-WP-0007-T02
|
||||
status: wait
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
|
|
@ -48,10 +48,16 @@ unregistered callback denial and successful token exchange through the portal.
|
|||
|
||||
```task
|
||||
id: KEY-WP-0007-T03
|
||||
status: wait
|
||||
status: progress
|
||||
priority: high
|
||||
```
|
||||
|
||||
Prove issuer, audience, tenant, groups, roles and assurance claims are verified
|
||||
by the portal and that tenant administration does not imply platform-root.
|
||||
Complete the Binky user/MFA acceptance through the reusable browser path.
|
||||
|
||||
2026-07-27: The live client accepts only the exact portal callback and rejects
|
||||
an unregistered callback with `invalid_profile_usage`. The portal begins an
|
||||
S256 PKCE flow and hands authentication to Authelia. LLDAP tenant envelope
|
||||
mapping now recognizes unambiguous `tenant:<kind>:<slug>:users|admins` groups;
|
||||
ambiguous multi-tenant directory envelopes fail closed to no explicit tenant.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue