mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-29 10:32:15 +00:00
feat(group): add user groups to token claims (#11009)
# Which Problems Are Solved If the groups of a user is requested through a scope, details related to the user's groups are added in the token claims. # How the Problems Are Solved By querying the user groups that the user is a part of, and setting them in the token claims. This PR introduces two scopes to request user groups: `groups` and `urn:zitadel:iam:user:groups` * `groups` returns an array of group names in the token claims with the same name * `urn:zitadel:iam:user:groups` returns an array of JSON objects with information about the user groups in the token claims with the same name # Additional Changes N/A # Additional Context - Closes #9702 - Follow-up for PRs: * #10455 * #10853 * #10758 * #10940 --------- Co-authored-by: Livio Spring <[email protected]>
This commit is contained in:
co-authored by
Livio Spring
parent
e4a959c321
commit
a9846498a9
@@ -32,6 +32,10 @@ const (
|
||||
ClaimResourceOwnerName = ScopeResourceOwner + ":name"
|
||||
ClaimResourceOwnerPrimaryDomain = ScopeResourceOwner + ":primary_domain"
|
||||
ClaimActionLogFormat = "urn:zitadel:iam:action:%s:log"
|
||||
ScopeUserGroups = "groups"
|
||||
ClaimUserGroups = ScopeUserGroups
|
||||
ScopeCustomUserGroups = "urn:zitadel:iam:user:groups"
|
||||
ClaimCustomUserGroups = ScopeCustomUserGroups
|
||||
|
||||
oidcCtx = "oidc"
|
||||
)
|
||||
|
||||
@@ -271,5 +271,8 @@ func isScopeAllowed(scope string, allowedScopes ...string) bool {
|
||||
if scope == ScopeProjectsRoles {
|
||||
return true
|
||||
}
|
||||
if scope == ScopeCustomUserGroups || scope == ScopeUserGroups {
|
||||
return true
|
||||
}
|
||||
return slices.Contains(allowedScopes, scope)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,13 @@ func TestServer_UserInfo(t *testing.T) {
|
||||
clientID, projectID := createClient(t, Instance)
|
||||
addProjectRolesGrants(t, User.GetUserId(), projectID, roleFoo, roleBar)
|
||||
|
||||
// create user groups
|
||||
group1 := Instance.CreateGroup(CTXIAM, t, Instance.DefaultOrg.GetId(), "group1")
|
||||
group2 := Instance.CreateGroup(CTXIAM, t, Instance.DefaultOrg.GetId(), "group2")
|
||||
// add the user to groups
|
||||
Instance.AddUsersToGroup(CTXIAM, t, group1.GetId(), []string{User.GetUserId()})
|
||||
Instance.AddUsersToGroup(CTXIAM, t, group2.GetId(), []string{User.GetUserId()})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
prepare func(t *testing.T, clientID string, scope []string) *oidc.Tokens[*oidc.IDTokenClaims]
|
||||
@@ -154,6 +161,39 @@ func TestServer_UserInfo(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "custom group scope",
|
||||
prepare: getTokens,
|
||||
scope: []string{oidc.ScopeProfile, oidc.ScopeOpenID, oidc.ScopeEmail, oidc.ScopeOfflineAccess,
|
||||
oidc_api.ScopeCustomUserGroups},
|
||||
assertions: []func(*testing.T, *oidc.UserInfo){
|
||||
assertUserinfo,
|
||||
func(t *testing.T, info *oidc.UserInfo) {
|
||||
expectedUserGroups := []map[string]interface{}{
|
||||
{"id": group1.GetId(), "name": "group1"},
|
||||
{"id": group2.GetId(), "name": "group2"},
|
||||
}
|
||||
userGroupsClaim := info.Claims[oidc_api.ClaimCustomUserGroups]
|
||||
require.Len(t, userGroupsClaim, len(expectedUserGroups))
|
||||
assert.ElementsMatch(t, expectedUserGroups, userGroupsClaim)
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "group scope",
|
||||
prepare: getTokens,
|
||||
scope: []string{oidc.ScopeProfile, oidc.ScopeOpenID, oidc.ScopeEmail, oidc.ScopeOfflineAccess,
|
||||
oidc_api.ScopeUserGroups},
|
||||
assertions: []func(*testing.T, *oidc.UserInfo){
|
||||
assertUserinfo,
|
||||
func(t *testing.T, info *oidc.UserInfo) {
|
||||
expectedUserGroups := []string{"group1", "group2"}
|
||||
userGroupsClaim := info.Claims[oidc_api.ClaimUserGroups]
|
||||
require.Len(t, userGroupsClaim, len(expectedUserGroups))
|
||||
assert.ElementsMatch(t, expectedUserGroups, userGroupsClaim)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -190,6 +190,10 @@ func userInfoToOIDC(user *query.OIDCUserInfo, userInfoAssertion bool, scope []st
|
||||
setUserInfoMetadata(user.Metadata, out)
|
||||
case ScopeResourceOwner:
|
||||
setUserInfoOrgClaims(user, out)
|
||||
case ScopeCustomUserGroups:
|
||||
setUserInfoCustomUserGroups(user.UserGroups, out)
|
||||
case ScopeUserGroups:
|
||||
setUserInfoUserGroups(user.UserGroups, out)
|
||||
default:
|
||||
if claim, ok := strings.CutPrefix(s, domain.OrgDomainPrimaryScope); ok {
|
||||
out.AppendClaims(domain.OrgDomainPrimaryClaim, claim)
|
||||
@@ -287,6 +291,24 @@ func setUserInfoRoleClaims(userInfo *oidc.UserInfo, roles *projectsRoles) {
|
||||
}
|
||||
}
|
||||
|
||||
func setUserInfoCustomUserGroups(userGroups []query.UserInfoUserGroup, out *oidc.UserInfo) {
|
||||
if len(userGroups) == 0 {
|
||||
return
|
||||
}
|
||||
out.AppendClaims(ClaimCustomUserGroups, userGroups)
|
||||
}
|
||||
|
||||
func setUserInfoUserGroups(userGroups []query.UserInfoUserGroup, out *oidc.UserInfo) {
|
||||
if len(userGroups) == 0 {
|
||||
return
|
||||
}
|
||||
groups := make([]string, len(userGroups))
|
||||
for i, userGroup := range userGroups {
|
||||
groups[i] = userGroup.Name
|
||||
}
|
||||
out.AppendClaims(ClaimUserGroups, groups)
|
||||
}
|
||||
|
||||
//nolint:gocognit
|
||||
func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, userInfo *oidc.UserInfo, triggerType domain.TriggerType, clientID string) (err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
|
||||
@@ -179,6 +179,16 @@ func Test_userInfoToOIDC(t *testing.T) {
|
||||
Name: "orgName",
|
||||
PrimaryDomain: "orgDomain",
|
||||
}
|
||||
userGroups := []query.UserInfoUserGroup{
|
||||
{
|
||||
Name: "group1",
|
||||
ID: "group1-id",
|
||||
},
|
||||
{
|
||||
Name: "group2",
|
||||
ID: "group2-id",
|
||||
},
|
||||
}
|
||||
humanUserInfo := &query.OIDCUserInfo{
|
||||
User: &query.User{
|
||||
ID: "human1",
|
||||
@@ -226,6 +236,7 @@ func Test_userInfoToOIDC(t *testing.T) {
|
||||
UserResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
UserGroups: userGroups,
|
||||
}
|
||||
machineUserInfo := &query.OIDCUserInfo{
|
||||
User: &query.User{
|
||||
@@ -503,6 +514,41 @@ func Test_userInfoToOIDC(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "human, scope custom user groups, found",
|
||||
args: args{
|
||||
user: humanUserInfo,
|
||||
scope: []string{ScopeCustomUserGroups},
|
||||
},
|
||||
want: &oidc.UserInfo{
|
||||
Subject: "human1",
|
||||
Claims: map[string]any{
|
||||
ClaimCustomUserGroups: []query.UserInfoUserGroup{
|
||||
{
|
||||
Name: "group1",
|
||||
ID: "group1-id",
|
||||
},
|
||||
{
|
||||
Name: "group2",
|
||||
ID: "group2-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "human, scope user groups (group names), found",
|
||||
args: args{
|
||||
user: humanUserInfo,
|
||||
scope: []string{ScopeUserGroups},
|
||||
},
|
||||
want: &oidc.UserInfo{
|
||||
Subject: "human1",
|
||||
Claims: map[string]any{
|
||||
ClaimUserGroups: []string{"group1", "group2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -51,6 +51,7 @@ func (g *groupProjection) Init() *old_handler.Check {
|
||||
},
|
||||
handler.NewPrimaryKey(GroupColumnInstanceID, GroupColumnID),
|
||||
handler.WithIndex(handler.NewIndex("resource_owner", []string{GroupColumnResourceOwner})),
|
||||
handler.WithIndex(handler.NewIndex("state", []string{GroupColumnState})),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -82,10 +82,11 @@ func (q *Queries) GetOIDCUserInfo(ctx context.Context, userID string, roleAudien
|
||||
}
|
||||
|
||||
type OIDCUserInfo struct {
|
||||
User *User `json:"user,omitempty"`
|
||||
Metadata []UserMetadata `json:"metadata,omitempty"`
|
||||
Org *UserInfoOrg `json:"org,omitempty"`
|
||||
UserGrants []UserGrant `json:"user_grants,omitempty"`
|
||||
User *User `json:"user,omitempty"`
|
||||
Metadata []UserMetadata `json:"metadata,omitempty"`
|
||||
Org *UserInfoOrg `json:"org,omitempty"`
|
||||
UserGrants []UserGrant `json:"user_grants,omitempty"`
|
||||
UserGroups []UserInfoUserGroup `json:"user_groups,omitempty"`
|
||||
}
|
||||
|
||||
type UserInfoOrg struct {
|
||||
@@ -94,6 +95,11 @@ type UserInfoOrg struct {
|
||||
PrimaryDomain string `json:"primary_domain,omitempty"`
|
||||
}
|
||||
|
||||
type UserInfoUserGroup struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
//go:embed userinfo_client_by_id.sql
|
||||
var oidcUserinfoClientQuery string
|
||||
|
||||
|
||||
@@ -62,6 +62,18 @@ user_org as (
|
||||
join usr u on o.id = u.resource_owner
|
||||
) r
|
||||
),
|
||||
-- find the user's groups
|
||||
user_groups as (
|
||||
select json_agg(row_to_json(r)) as user_groups from (
|
||||
select g.id, g.name
|
||||
from projections.group_users1 as gu
|
||||
left join projections.groups1 as g on gu.group_id = g.id and gu.instance_id = g.instance_id
|
||||
where gu.user_id = $1
|
||||
and gu.instance_id = $2
|
||||
and g.state = 1
|
||||
)
|
||||
r
|
||||
),
|
||||
-- join user grants to orgs, projects and user
|
||||
grants as (
|
||||
select json_agg(row_to_json(r)) as grants from (
|
||||
@@ -87,5 +99,6 @@ select json_build_object(
|
||||
),
|
||||
'org', (select organization from user_org),
|
||||
'metadata', (select metadata from metadata),
|
||||
'user_grants', (select grants from grants)
|
||||
'user_grants', (select grants from grants),
|
||||
'user_groups', (select user_groups from user_groups)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user