diff --git a/history/2026-09-28-railiance-admin-role.md b/history/2026-09-28-railiance-admin-role.md new file mode 100644 index 0000000..2c9e75f --- /dev/null +++ b/history/2026-09-28-railiance-admin-role.md @@ -0,0 +1,12 @@ +# Railiance admin role — 2026-09-28 + +Implemented under railiance-telemetry RTEL-WP-0002-T04 and explicit Bernd Worsch +approval. Directory group `railiance-admins` now maps to the distinct +`railiance-admin` role and platform tenant. It does not grant platform-operator; +existing net-kingdom-admins behavior is preserved. All Go tests pass. + +A contained identity-provisioner operation verified that `tegwick` has the +requested notification address and added only that group; other memberships +were preserved. Signed claim proof awaits issuer rollout and real login. +No credential value was printed, copied to source or returned to the agent. +The native directory operation is recorded in telemetry's session evidence. diff --git a/src/internal/adapters/lldap/adapter.go b/src/internal/adapters/lldap/adapter.go index 5ac362a..e15cecd 100644 --- a/src/internal/adapters/lldap/adapter.go +++ b/src/internal/adapters/lldap/adapter.go @@ -358,8 +358,11 @@ func identityEnvelopeFromGroups(groups []string) (string, []string) { tenants := make(map[string]bool) admins := make(map[string]bool) platformOperator := false + railianceAdmin := false for _, group := range groups { switch { + case group == "railiance-admins": + railianceAdmin = true case group == "net-kingdom-admins": platformOperator = true case strings.HasPrefix(group, "tenant:") && strings.HasSuffix(group, ":users"): @@ -374,7 +377,16 @@ func identityEnvelopeFromGroups(groups []string) (string, []string) { // platform control-plane identity. Tenant envelopes must never narrow or // ambiguously reinterpret platform authority. if platformOperator { - return "tenant:platform", []string{"user", "platform-operator"} + roles := []string{"user", "platform-operator"} + if railianceAdmin { + roles = append(roles, "railiance-admin") + } + return "tenant:platform", roles + } + // This explicit directory group grants the named Railiance role only. + // It does not imply net-kingdom-admins or platform-operator authority. + if railianceAdmin { + return "tenant:platform", []string{"user", "railiance-admin"} } if len(tenants) != 1 { return "", []string{} diff --git a/src/internal/adapters/lldap/railiance_admin_test.go b/src/internal/adapters/lldap/railiance_admin_test.go new file mode 100644 index 0000000..d76e938 --- /dev/null +++ b/src/internal/adapters/lldap/railiance_admin_test.go @@ -0,0 +1,26 @@ +package lldap + +import ( + "reflect" + "testing" +) + +func TestRailianceAdminGroupIsExplicitAndScoped(t *testing.T) { + cases := []struct { + groups []string + tenant string + roles []string + }{ + {[]string{"railiance-admins"}, "tenant:platform", []string{"user", "railiance-admin"}}, + {[]string{"net-kingdom-admins"}, "tenant:platform", []string{"user", "platform-operator"}}, + {[]string{"net-kingdom-admins", "railiance-admins"}, "tenant:platform", []string{"user", "platform-operator", "railiance-admin"}}, + {[]string{"railiance-admin"}, "", []string{}}, + {[]string{"tenant:friendly:other:admins"}, "tenant:friendly:other", []string{"user", "tenant-admin"}}, + } + for _, c := range cases { + tenant, roles := identityEnvelopeFromGroups(c.groups) + if tenant != c.tenant || !reflect.DeepEqual(roles, c.roles) { + t.Fatalf("groups %v: got %q %v", c.groups, tenant, roles) + } + } +}