mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 13:52:51 +00:00
# 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]>
561 lines
14 KiB
Go
561 lines
14 KiB
Go
package oidc
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
|
"golang.org/x/text/language"
|
|
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
"github.com/zitadel/zitadel/internal/query"
|
|
)
|
|
|
|
func Test_prepareRoles(t *testing.T) {
|
|
type args struct {
|
|
projectID string
|
|
scope []string
|
|
projectRoleAssertion bool
|
|
currentProjectOnly bool
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
wantRoleAudience []string
|
|
wantRequestedRoles []string
|
|
}{
|
|
{
|
|
name: "empty scope",
|
|
args: args{
|
|
projectID: "projID",
|
|
scope: nil,
|
|
projectRoleAssertion: false,
|
|
currentProjectOnly: false,
|
|
},
|
|
wantRoleAudience: nil,
|
|
wantRequestedRoles: nil,
|
|
},
|
|
{
|
|
name: "project role assertion",
|
|
args: args{
|
|
projectID: "projID",
|
|
projectRoleAssertion: true,
|
|
scope: nil,
|
|
currentProjectOnly: false,
|
|
},
|
|
wantRoleAudience: []string{"projID"},
|
|
wantRequestedRoles: nil,
|
|
},
|
|
{
|
|
name: "some scope, current project only",
|
|
args: args{
|
|
projectID: "projID",
|
|
projectRoleAssertion: false,
|
|
scope: []string{"openid", "profile"},
|
|
currentProjectOnly: true,
|
|
},
|
|
wantRoleAudience: []string{"projID"},
|
|
wantRequestedRoles: nil,
|
|
},
|
|
{
|
|
name: "scope projects roles",
|
|
args: args{
|
|
projectID: "projID",
|
|
projectRoleAssertion: false,
|
|
scope: []string{
|
|
"openid", "profile",
|
|
ScopeProjectsRoles,
|
|
domain.ProjectIDScope + "project2" + domain.AudSuffix,
|
|
},
|
|
currentProjectOnly: false,
|
|
},
|
|
wantRoleAudience: []string{"project2", "projID"},
|
|
wantRequestedRoles: nil,
|
|
},
|
|
{
|
|
name: "scope projects roles ignored, current project only",
|
|
args: args{
|
|
projectID: "projID",
|
|
projectRoleAssertion: false,
|
|
scope: []string{
|
|
"openid", "profile",
|
|
ScopeProjectsRoles,
|
|
domain.ProjectIDScope + "project2" + domain.AudSuffix,
|
|
},
|
|
currentProjectOnly: true,
|
|
},
|
|
wantRoleAudience: []string{"projID"},
|
|
wantRequestedRoles: nil,
|
|
},
|
|
{
|
|
name: "scope project role prefix",
|
|
args: args{
|
|
projectID: "projID",
|
|
projectRoleAssertion: false,
|
|
scope: []string{
|
|
"openid", "profile",
|
|
ScopeProjectRolePrefix + "foo",
|
|
ScopeProjectRolePrefix + "bar",
|
|
},
|
|
currentProjectOnly: false,
|
|
},
|
|
wantRoleAudience: []string{"projID"},
|
|
wantRequestedRoles: []string{"foo", "bar"},
|
|
},
|
|
{
|
|
name: "scope project role prefix and audience",
|
|
args: args{
|
|
projectID: "projID",
|
|
projectRoleAssertion: false,
|
|
scope: []string{
|
|
"openid", "profile",
|
|
ScopeProjectRolePrefix + "foo",
|
|
ScopeProjectRolePrefix + "bar",
|
|
domain.ProjectIDScope + "project2" + domain.AudSuffix,
|
|
},
|
|
currentProjectOnly: false,
|
|
},
|
|
wantRoleAudience: []string{"projID", "project2"},
|
|
wantRequestedRoles: []string{"foo", "bar"},
|
|
},
|
|
{
|
|
name: "scope project role prefix and audience ignored, current project only",
|
|
args: args{
|
|
projectID: "projID",
|
|
projectRoleAssertion: false,
|
|
scope: []string{
|
|
"openid", "profile",
|
|
ScopeProjectRolePrefix + "foo",
|
|
ScopeProjectRolePrefix + "bar",
|
|
domain.ProjectIDScope + "project2" + domain.AudSuffix,
|
|
},
|
|
currentProjectOnly: true,
|
|
},
|
|
wantRoleAudience: []string{"projID"},
|
|
wantRequestedRoles: []string{"foo", "bar"},
|
|
},
|
|
{
|
|
name: "no projectID, scope project role prefix and audience",
|
|
args: args{
|
|
projectID: "",
|
|
projectRoleAssertion: false,
|
|
scope: []string{
|
|
"openid", "profile",
|
|
ScopeProjectRolePrefix + "foo",
|
|
ScopeProjectRolePrefix + "bar",
|
|
domain.ProjectIDScope + "project2" + domain.AudSuffix,
|
|
},
|
|
currentProjectOnly: false,
|
|
},
|
|
wantRoleAudience: []string{"project2"},
|
|
wantRequestedRoles: []string{"foo", "bar"},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gotRoleAudience, gotRequestedRoles := prepareRoles(context.Background(), tt.args.scope, tt.args.projectID, tt.args.projectRoleAssertion, tt.args.currentProjectOnly)
|
|
assert.ElementsMatch(t, tt.wantRoleAudience, gotRoleAudience, "roleAudience")
|
|
assert.ElementsMatch(t, tt.wantRequestedRoles, gotRequestedRoles, "requestedRoles")
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_userInfoToOIDC(t *testing.T) {
|
|
metadata := []query.UserMetadata{
|
|
{
|
|
Key: "key1",
|
|
Value: []byte{1, 2, 3},
|
|
},
|
|
{
|
|
Key: "key2",
|
|
Value: []byte{4, 5, 6},
|
|
},
|
|
}
|
|
organization := &query.UserInfoOrg{
|
|
ID: "orgID",
|
|
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",
|
|
CreationDate: time.Unix(123, 456),
|
|
ChangeDate: time.Unix(567, 890),
|
|
ResourceOwner: "orgID",
|
|
Sequence: 22,
|
|
State: domain.UserStateActive,
|
|
Type: domain.UserTypeHuman,
|
|
Username: "username",
|
|
LoginNames: []string{"foo", "bar"},
|
|
PreferredLoginName: "foo",
|
|
Human: &query.Human{
|
|
FirstName: "user",
|
|
LastName: "name",
|
|
NickName: "foobar",
|
|
DisplayName: "xxx",
|
|
AvatarKey: "picture.png",
|
|
PreferredLanguage: language.Dutch,
|
|
Gender: domain.GenderDiverse,
|
|
Email: "[email protected]",
|
|
IsEmailVerified: true,
|
|
Phone: "+31123456789",
|
|
IsPhoneVerified: true,
|
|
},
|
|
},
|
|
Metadata: metadata,
|
|
Org: organization,
|
|
UserGrants: []query.UserGrant{
|
|
{
|
|
ID: "ug1",
|
|
CreationDate: time.Unix(444, 444),
|
|
ChangeDate: time.Unix(555, 555),
|
|
Sequence: 55,
|
|
Roles: []string{"role1", "role2"},
|
|
GrantID: "grantID",
|
|
State: domain.UserGrantStateActive,
|
|
UserID: "human1",
|
|
Username: "username",
|
|
ResourceOwner: "orgID",
|
|
ProjectID: "project1",
|
|
OrgName: "orgName",
|
|
OrgPrimaryDomain: "orgDomain",
|
|
ProjectName: "projectName",
|
|
UserResourceOwner: "org1",
|
|
},
|
|
},
|
|
UserGroups: userGroups,
|
|
}
|
|
machineUserInfo := &query.OIDCUserInfo{
|
|
User: &query.User{
|
|
ID: "machine1",
|
|
CreationDate: time.Unix(123, 456),
|
|
ChangeDate: time.Unix(567, 890),
|
|
ResourceOwner: "orgID",
|
|
Sequence: 23,
|
|
State: domain.UserStateActive,
|
|
Type: domain.UserTypeMachine,
|
|
Username: "machine",
|
|
PreferredLoginName: "meanMachine",
|
|
Machine: &query.Machine{
|
|
Name: "machine",
|
|
Description: "I'm a robot",
|
|
},
|
|
},
|
|
Org: organization,
|
|
UserGrants: []query.UserGrant{
|
|
{
|
|
ID: "ug1",
|
|
CreationDate: time.Unix(444, 444),
|
|
ChangeDate: time.Unix(555, 555),
|
|
Sequence: 55,
|
|
Roles: []string{"role1", "role2"},
|
|
GrantID: "grantID",
|
|
State: domain.UserGrantStateActive,
|
|
UserID: "human1",
|
|
Username: "username",
|
|
ResourceOwner: "orgID",
|
|
ProjectID: "project1",
|
|
OrgName: "orgName",
|
|
OrgPrimaryDomain: "orgDomain",
|
|
ProjectName: "projectName",
|
|
UserResourceOwner: "org1",
|
|
},
|
|
},
|
|
}
|
|
|
|
type args struct {
|
|
user *query.OIDCUserInfo
|
|
userInfoAssertion bool
|
|
scope []string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want *oidc.UserInfo
|
|
}{
|
|
{
|
|
name: "human, empty",
|
|
args: args{
|
|
user: humanUserInfo,
|
|
},
|
|
want: &oidc.UserInfo{
|
|
Subject: "human1",
|
|
},
|
|
},
|
|
{
|
|
name: "machine, empty",
|
|
args: args{
|
|
user: machineUserInfo,
|
|
},
|
|
want: &oidc.UserInfo{
|
|
Subject: "machine1",
|
|
},
|
|
},
|
|
{
|
|
name: "human, scope openid",
|
|
args: args{
|
|
user: humanUserInfo,
|
|
scope: []string{oidc.ScopeOpenID},
|
|
},
|
|
want: &oidc.UserInfo{
|
|
Subject: "human1",
|
|
},
|
|
},
|
|
{
|
|
name: "machine, scope openid",
|
|
args: args{
|
|
user: machineUserInfo,
|
|
scope: []string{oidc.ScopeOpenID},
|
|
},
|
|
want: &oidc.UserInfo{
|
|
Subject: "machine1",
|
|
},
|
|
},
|
|
{
|
|
name: "human, scope email, profileInfoAssertion",
|
|
args: args{
|
|
user: humanUserInfo,
|
|
userInfoAssertion: true,
|
|
scope: []string{oidc.ScopeEmail},
|
|
},
|
|
want: &oidc.UserInfo{
|
|
Subject: "human1",
|
|
UserInfoEmail: oidc.UserInfoEmail{
|
|
Email: "[email protected]",
|
|
EmailVerified: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "human, scope email",
|
|
args: args{
|
|
user: humanUserInfo,
|
|
scope: []string{oidc.ScopeEmail},
|
|
},
|
|
want: &oidc.UserInfo{
|
|
Subject: "human1",
|
|
},
|
|
},
|
|
{
|
|
name: "machine, scope email, profileInfoAssertion",
|
|
args: args{
|
|
user: machineUserInfo,
|
|
scope: []string{oidc.ScopeEmail},
|
|
},
|
|
want: &oidc.UserInfo{
|
|
Subject: "machine1",
|
|
},
|
|
},
|
|
{
|
|
name: "human, scope profile, profileInfoAssertion",
|
|
args: args{
|
|
user: humanUserInfo,
|
|
userInfoAssertion: true,
|
|
scope: []string{oidc.ScopeProfile},
|
|
},
|
|
want: &oidc.UserInfo{
|
|
Subject: "human1",
|
|
UserInfoProfile: oidc.UserInfoProfile{
|
|
Name: "xxx",
|
|
GivenName: "user",
|
|
FamilyName: "name",
|
|
Nickname: "foobar",
|
|
Picture: "https://foo.com/assets/orgID/picture.png",
|
|
Gender: "diverse",
|
|
Locale: oidc.NewLocale(language.Dutch),
|
|
UpdatedAt: oidc.FromTime(time.Unix(567, 890)),
|
|
PreferredUsername: "foo",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "machine, scope profile, profileInfoAssertion",
|
|
args: args{
|
|
user: machineUserInfo,
|
|
userInfoAssertion: true,
|
|
scope: []string{oidc.ScopeProfile},
|
|
},
|
|
want: &oidc.UserInfo{
|
|
Subject: "machine1",
|
|
UserInfoProfile: oidc.UserInfoProfile{
|
|
Name: "machine",
|
|
UpdatedAt: oidc.FromTime(time.Unix(567, 890)),
|
|
PreferredUsername: "meanMachine",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "machine, scope profile",
|
|
args: args{
|
|
user: machineUserInfo,
|
|
scope: []string{oidc.ScopeProfile},
|
|
},
|
|
want: &oidc.UserInfo{
|
|
Subject: "machine1",
|
|
},
|
|
},
|
|
{
|
|
name: "human, scope phone, profileInfoAssertion",
|
|
args: args{
|
|
user: humanUserInfo,
|
|
userInfoAssertion: true,
|
|
scope: []string{oidc.ScopePhone},
|
|
},
|
|
want: &oidc.UserInfo{
|
|
Subject: "human1",
|
|
UserInfoPhone: oidc.UserInfoPhone{
|
|
PhoneNumber: "+31123456789",
|
|
PhoneNumberVerified: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "human, scope phone",
|
|
args: args{
|
|
user: humanUserInfo,
|
|
scope: []string{oidc.ScopePhone},
|
|
},
|
|
want: &oidc.UserInfo{
|
|
Subject: "human1",
|
|
},
|
|
},
|
|
{
|
|
name: "machine, scope phone",
|
|
args: args{
|
|
user: machineUserInfo,
|
|
scope: []string{oidc.ScopePhone},
|
|
},
|
|
want: &oidc.UserInfo{
|
|
Subject: "machine1",
|
|
UserInfoPhone: oidc.UserInfoPhone{},
|
|
},
|
|
},
|
|
{
|
|
name: "human, scope metadata",
|
|
args: args{
|
|
user: humanUserInfo,
|
|
scope: []string{ScopeUserMetaData},
|
|
},
|
|
want: &oidc.UserInfo{
|
|
Subject: "human1",
|
|
UserInfoEmail: oidc.UserInfoEmail{},
|
|
Claims: map[string]any{
|
|
ClaimUserMetaData: map[string]string{
|
|
"key1": base64.RawURLEncoding.EncodeToString([]byte{1, 2, 3}),
|
|
"key2": base64.RawURLEncoding.EncodeToString([]byte{4, 5, 6}),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "machine, scope metadata, none found",
|
|
args: args{
|
|
user: machineUserInfo,
|
|
scope: []string{ScopeUserMetaData},
|
|
},
|
|
want: &oidc.UserInfo{
|
|
Subject: "machine1",
|
|
},
|
|
},
|
|
{
|
|
name: "machine, scope resource owner",
|
|
args: args{
|
|
user: machineUserInfo,
|
|
scope: []string{ScopeResourceOwner},
|
|
},
|
|
want: &oidc.UserInfo{
|
|
Subject: "machine1",
|
|
Claims: map[string]any{
|
|
ClaimResourceOwnerID: "orgID",
|
|
ClaimResourceOwnerName: "orgName",
|
|
ClaimResourceOwnerPrimaryDomain: "orgDomain",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "human, scope org primary domain prefix",
|
|
args: args{
|
|
user: humanUserInfo,
|
|
scope: []string{domain.OrgDomainPrimaryScope + "foo.com"},
|
|
},
|
|
want: &oidc.UserInfo{
|
|
Subject: "human1",
|
|
Claims: map[string]any{
|
|
domain.OrgDomainPrimaryClaim: "foo.com",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "machine, scope org id",
|
|
args: args{
|
|
user: machineUserInfo,
|
|
scope: []string{domain.OrgIDScope + "orgID"},
|
|
},
|
|
want: &oidc.UserInfo{
|
|
Subject: "machine1",
|
|
Claims: map[string]any{
|
|
domain.OrgIDClaim: "orgID",
|
|
ClaimResourceOwnerID: "orgID",
|
|
ClaimResourceOwnerName: "orgName",
|
|
ClaimResourceOwnerPrimaryDomain: "orgDomain",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
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) {
|
|
assetPrefix := "https://foo.com/assets"
|
|
got := userInfoToOIDC(tt.args.user, tt.args.userInfoAssertion, tt.args.scope, assetPrefix)
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|