mirror of
https://github.com/zitadel/zitadel.git
synced 2025-10-20 22:29:12 +00:00
add user_grants to the userinfo query
This commit is contained in:
@@ -90,6 +90,36 @@ func UserGrantsFromQuery(c *actions.FieldConfig, userGrants *query.UserGrants) g
|
||||
return c.Runtime.ToValue(grantList)
|
||||
}
|
||||
|
||||
func UserGrantsFromSlice(c *actions.FieldConfig, userGrants []query.UserGrant) goja.Value {
|
||||
if userGrants == nil {
|
||||
return c.Runtime.ToValue(nil)
|
||||
}
|
||||
grantList := &userGrantList{
|
||||
Count: uint64(len(userGrants)),
|
||||
Grants: make([]*userGrant, len(userGrants)),
|
||||
}
|
||||
|
||||
for i, grant := range userGrants {
|
||||
grantList.Grants[i] = &userGrant{
|
||||
Id: grant.ID,
|
||||
ProjectGrantId: grant.GrantID,
|
||||
State: grant.State,
|
||||
CreationDate: grant.CreationDate,
|
||||
ChangeDate: grant.ChangeDate,
|
||||
Sequence: grant.Sequence,
|
||||
UserId: grant.UserID,
|
||||
Roles: grant.Roles,
|
||||
UserResourceOwner: grant.UserResourceOwner,
|
||||
UserGrantResourceOwner: grant.ResourceOwner,
|
||||
UserGrantResourceOwnerName: grant.OrgName,
|
||||
ProjectId: grant.ProjectID,
|
||||
ProjectName: grant.ProjectName,
|
||||
}
|
||||
}
|
||||
|
||||
return c.Runtime.ToValue(grantList)
|
||||
}
|
||||
|
||||
func UserGrantsToDomain(userID string, actionUserGrants []UserGrant) []*domain.UserGrant {
|
||||
if actionUserGrants == nil {
|
||||
return nil
|
||||
|
@@ -795,7 +795,7 @@ func (o *OPStorage) assertRoles(ctx context.Context, userID, applicationID strin
|
||||
if len(requestedRoles) > 0 {
|
||||
for _, requestedRole := range requestedRoles {
|
||||
for _, grant := range grants.UserGrants {
|
||||
checkGrantedRoles(roles, grant, requestedRole, grant.ProjectID == projectID)
|
||||
checkGrantedRoles(roles, *grant, requestedRole, grant.ProjectID == projectID)
|
||||
}
|
||||
}
|
||||
return grants, roles, nil
|
||||
@@ -838,7 +838,7 @@ func (o *OPStorage) assertUserResourceOwner(ctx context.Context, userID string)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func checkGrantedRoles(roles *projectsRoles, grant *query.UserGrant, requestedRole string, isRequested bool) {
|
||||
func checkGrantedRoles(roles *projectsRoles, grant query.UserGrant, requestedRole string, isRequested bool) {
|
||||
for _, grantedRole := range grant.Roles {
|
||||
if requestedRole == grantedRole {
|
||||
roles.Add(grant.ProjectID, grantedRole, grant.ResourceOwner, grant.OrgPrimaryDomain, isRequested)
|
||||
@@ -854,6 +854,26 @@ type projectsRoles struct {
|
||||
requestProjectID string
|
||||
}
|
||||
|
||||
func newProjectRoles(projectID string, grants []query.UserGrant, requestedRoles []string) *projectsRoles {
|
||||
roles := new(projectsRoles)
|
||||
// if specific roles where requested, check if they are granted and append them in the roles list
|
||||
if len(requestedRoles) > 0 {
|
||||
for _, requestedRole := range requestedRoles {
|
||||
for _, grant := range grants {
|
||||
checkGrantedRoles(roles, grant, requestedRole, grant.ProjectID == projectID)
|
||||
}
|
||||
}
|
||||
return roles
|
||||
}
|
||||
// no specific roles were requested, so convert any grants into roles
|
||||
for _, grant := range grants {
|
||||
for _, role := range grant.Roles {
|
||||
roles.Add(grant.ProjectID, role, grant.ResourceOwner, grant.OrgPrimaryDomain, grant.ProjectID == projectID)
|
||||
}
|
||||
}
|
||||
return roles
|
||||
}
|
||||
|
||||
func (p *projectsRoles) Add(projectID, roleKey, orgID, domain string, isRequested bool) {
|
||||
if p.projects == nil {
|
||||
p.projects = make(map[string]projectRoles, 1)
|
||||
|
@@ -87,7 +87,7 @@ func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionR
|
||||
if err = validateIntrospectionAudience(token.audience, client.clientID, client.projectID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userInfo, err := s.getUserInfoWithRoles(ctx, token.userID, client.projectID, token.scope, []string{client.projectID})
|
||||
userInfo, err := s.userInfo(ctx, token.userID, client.projectID, token.scope, []string{client.projectID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -16,141 +16,42 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/actions/object"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
func (s *Server) getUserInfoWithRoles(ctx context.Context, userID, projectID string, scope, roleAudience []string) (_ *oidc.UserInfo, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
userInfoChan := make(chan *userInfoResult)
|
||||
go s.getUserInfo(ctx, userID, userInfoChan)
|
||||
|
||||
rolesChan := make(chan *assertRolesResult)
|
||||
go s.assertRoles(ctx, userID, projectID, scope, roleAudience, rolesChan)
|
||||
|
||||
var (
|
||||
userInfoResult *userInfoResult
|
||||
assertRolesResult *assertRolesResult
|
||||
)
|
||||
|
||||
// make sure both channels are always read,
|
||||
// and cancel the context on first error
|
||||
for i := 0; i < 2; i++ {
|
||||
var resErr error
|
||||
|
||||
select {
|
||||
case userInfoResult = <-userInfoChan:
|
||||
resErr = userInfoResult.err
|
||||
case assertRolesResult = <-rolesChan:
|
||||
resErr = assertRolesResult.err
|
||||
}
|
||||
|
||||
if resErr == nil {
|
||||
continue
|
||||
}
|
||||
cancel()
|
||||
|
||||
// we only care for the first error that occured,
|
||||
// as the next error is most probably a context error.
|
||||
if err == nil {
|
||||
err = resErr
|
||||
}
|
||||
func (s *Server) userInfo(ctx context.Context, userID, projectID string, scope, roleAudience []string) (_ *oidc.UserInfo, err error) {
|
||||
roleAudience, requestedRoles := prepareRoles(ctx, projectID, scope, roleAudience)
|
||||
qu, err := s.query.GetOIDCUserInfo(ctx, userID, roleAudience)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userInfo := userInfoToOIDC(userInfoResult.userInfo, scope, s.assetAPIPrefix(ctx))
|
||||
setUserInfoRoleClaims(userInfo, assertRolesResult.projectsRoles)
|
||||
|
||||
return userInfo, s.userinfoFlows(ctx, userInfoResult.userInfo, assertRolesResult.userGrants, userInfo)
|
||||
userInfo := userInfoToOIDC(projectID, qu, scope, requestedRoles, s.assetAPIPrefix(ctx))
|
||||
return userInfo, s.userinfoFlows(ctx, qu, userInfo)
|
||||
}
|
||||
|
||||
type userInfoResult struct {
|
||||
userInfo *query.OIDCUserInfo
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *Server) getUserInfo(ctx context.Context, userID string, rc chan<- *userInfoResult) {
|
||||
userInfo, err := s.query.GetOIDCUserInfo(ctx, userID)
|
||||
rc <- &userInfoResult{
|
||||
userInfo: userInfo,
|
||||
err: err,
|
||||
func prepareRoles(ctx context.Context, projectID string, scope, roleAudience []string) (ra, requestedRoles []string) {
|
||||
// if all roles are requested take the audience for those from the scopes
|
||||
if slices.Contains(scope, ScopeProjectsRoles) {
|
||||
roleAudience = domain.AddAudScopeToAudience(ctx, roleAudience, scope)
|
||||
}
|
||||
}
|
||||
|
||||
type assertRolesResult struct {
|
||||
userGrants *query.UserGrants
|
||||
projectsRoles *projectsRoles
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *Server) assertRoles(ctx context.Context, userID, projectID string, scope, roleAudience []string, rc chan<- *assertRolesResult) {
|
||||
userGrands, projectsRoles, err := func() (*query.UserGrants, *projectsRoles, error) {
|
||||
// if all roles are requested take the audience for those from the scopes
|
||||
if slices.Contains(scope, ScopeProjectsRoles) {
|
||||
roleAudience = domain.AddAudScopeToAudience(ctx, roleAudience, scope)
|
||||
requestedRoles = make([]string, 0, len(scope))
|
||||
for _, s := range scope {
|
||||
if role, ok := strings.CutPrefix(s, ScopeProjectRolePrefix); ok {
|
||||
requestedRoles = append(requestedRoles, role)
|
||||
}
|
||||
|
||||
requestedRoles := make([]string, 0, len(scope))
|
||||
for _, s := range scope {
|
||||
if role, ok := strings.CutPrefix(s, ScopeProjectRolePrefix); ok {
|
||||
requestedRoles = append(requestedRoles, role)
|
||||
}
|
||||
}
|
||||
|
||||
if len(requestedRoles) == 0 && len(roleAudience) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// ensure the projectID of the requesting is part of the roleAudience
|
||||
if projectID != "" {
|
||||
roleAudience = append(roleAudience, projectID)
|
||||
}
|
||||
queries := make([]query.SearchQuery, 0, 2)
|
||||
projectQuery, err := query.NewUserGrantProjectIDsSearchQuery(roleAudience)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
queries = append(queries, projectQuery)
|
||||
userIDQuery, err := query.NewUserGrantUserIDSearchQuery(userID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
queries = append(queries, userIDQuery)
|
||||
grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{
|
||||
Queries: queries,
|
||||
}, false, false) // triggers disabled
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
roles := new(projectsRoles)
|
||||
// if specific roles where requested, check if they are granted and append them in the roles list
|
||||
if len(requestedRoles) > 0 {
|
||||
for _, requestedRole := range requestedRoles {
|
||||
for _, grant := range grants.UserGrants {
|
||||
checkGrantedRoles(roles, grant, requestedRole, grant.ProjectID == projectID)
|
||||
}
|
||||
}
|
||||
return grants, roles, nil
|
||||
}
|
||||
// no specific roles were requested, so convert any grants into roles
|
||||
for _, grant := range grants.UserGrants {
|
||||
for _, role := range grant.Roles {
|
||||
roles.Add(grant.ProjectID, role, grant.ResourceOwner, grant.OrgPrimaryDomain, grant.ProjectID == projectID)
|
||||
}
|
||||
}
|
||||
return grants, roles, nil
|
||||
}()
|
||||
|
||||
rc <- &assertRolesResult{
|
||||
userGrants: userGrands,
|
||||
projectsRoles: projectsRoles,
|
||||
err: err,
|
||||
}
|
||||
if len(requestedRoles) == 0 && len(roleAudience) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ensure the projectID of the requesting is part of the roleAudience
|
||||
if !slices.Contains(roleAudience, projectID) {
|
||||
roleAudience = append(roleAudience, projectID)
|
||||
}
|
||||
return roleAudience, requestedRoles
|
||||
}
|
||||
|
||||
func userInfoToOIDC(user *query.OIDCUserInfo, scope []string, assetPrefix string) *oidc.UserInfo {
|
||||
func userInfoToOIDC(projectID string, user *query.OIDCUserInfo, scope, requestedRoles []string, assetPrefix string) *oidc.UserInfo {
|
||||
out := new(oidc.UserInfo)
|
||||
for _, s := range scope {
|
||||
switch s {
|
||||
@@ -178,7 +79,7 @@ func userInfoToOIDC(user *query.OIDCUserInfo, scope []string, assetPrefix string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setUserInfoRoleClaims(out, newProjectRoles(projectID, user.UserGrants, requestedRoles))
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -259,8 +160,8 @@ func setUserInfoRoleClaims(userInfo *oidc.UserInfo, roles *projectsRoles) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) userinfoFlows(ctx context.Context, user *query.OIDCUserInfo, userGrants *query.UserGrants, userInfo *oidc.UserInfo) error {
|
||||
queriedActions, err := s.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeCustomiseToken, domain.TriggerTypePreUserinfoCreation, user.User.ResourceOwner, false)
|
||||
func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, userInfo *oidc.UserInfo) error {
|
||||
queriedActions, err := s.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeCustomiseToken, domain.TriggerTypePreUserinfoCreation, qu.User.ResourceOwner, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -270,17 +171,17 @@ func (s *Server) userinfoFlows(ctx context.Context, user *query.OIDCUserInfo, us
|
||||
actions.SetFields("claims", userinfoClaims(userInfo)),
|
||||
actions.SetFields("getUser", func(c *actions.FieldConfig) interface{} {
|
||||
return func(call goja.FunctionCall) goja.Value {
|
||||
return object.UserFromQuery(c, user.User)
|
||||
return object.UserFromQuery(c, qu.User)
|
||||
}
|
||||
}),
|
||||
actions.SetFields("user",
|
||||
actions.SetFields("getMetadata", func(c *actions.FieldConfig) interface{} {
|
||||
return func(goja.FunctionCall) goja.Value {
|
||||
return object.UserMetadataListFromSlice(c, user.Metadata)
|
||||
return object.UserMetadataListFromSlice(c, qu.Metadata)
|
||||
}
|
||||
}),
|
||||
actions.SetFields("grants", func(c *actions.FieldConfig) interface{} {
|
||||
return object.UserGrantsFromQuery(c, userGrants)
|
||||
return object.UserGrantsFromSlice(c, qu.UserGrants)
|
||||
}),
|
||||
),
|
||||
),
|
||||
@@ -334,7 +235,7 @@ func (s *Server) userinfoFlows(ctx context.Context, user *query.OIDCUserInfo, us
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
if _, err = s.command.SetUserMetadata(ctx, metadata, userInfo.Subject, user.User.ResourceOwner); err != nil {
|
||||
if _, err = s.command.SetUserMetadata(ctx, metadata, userInfo.Subject, qu.User.ResourceOwner); err != nil {
|
||||
logging.WithError(err).Info("unable to set md in action")
|
||||
panic(err)
|
||||
}
|
||||
|
@@ -1,3 +1,6 @@
|
||||
-- deallocate q;
|
||||
-- prepare q (text, text, text[]) as
|
||||
|
||||
with usr as (
|
||||
select id, creation_date, change_date, sequence, state, resource_owner, username
|
||||
from projections.users9 u
|
||||
@@ -20,6 +23,7 @@ machine as (
|
||||
and instance_id = $2
|
||||
) r
|
||||
),
|
||||
-- find the user's metadata
|
||||
metadata as (
|
||||
select json_agg(row_to_json(r)) as metadata from (
|
||||
select creation_date, change_date, sequence, resource_owner, key, encode(value, 'base64') as value
|
||||
@@ -28,14 +32,46 @@ metadata as (
|
||||
and instance_id = $2
|
||||
) r
|
||||
),
|
||||
org as (
|
||||
-- get all user grants, needed for the orgs query
|
||||
user_grants as (
|
||||
select id, grant_id, state, creation_date, change_date, sequence, user_id, roles, resource_owner, project_id
|
||||
from projections.user_grants3
|
||||
where user_id = $1
|
||||
and instance_id = $2
|
||||
and project_id = any($3)
|
||||
),
|
||||
-- filter all orgs we are interested in.
|
||||
orgs as (
|
||||
select id, name, primary_domain
|
||||
from projections.orgs1
|
||||
where id in (
|
||||
select resource_owner from user_grants
|
||||
union
|
||||
select resource_owner from usr
|
||||
)
|
||||
and instance_id = $2
|
||||
),
|
||||
-- find the user's org
|
||||
user_org as (
|
||||
select row_to_json(r) as organization from (
|
||||
select name, primary_domain
|
||||
from projections.orgs1 o
|
||||
from orgs o
|
||||
join usr u on o.id = u.resource_owner
|
||||
where instance_id = $2
|
||||
) r
|
||||
),
|
||||
-- join user grants to orgs, projects and user
|
||||
grants as (
|
||||
select json_agg(row_to_json(r)) as grants from (
|
||||
select g.*,
|
||||
o.name as org_name, o.primary_domain as org_primary_domain,
|
||||
p.name as project_name, u.resource_owner as user_resource_owner
|
||||
from user_grants g
|
||||
left join orgs o on o.id = g.resource_owner
|
||||
left join projections.projects3 p on p.id = g.project_id
|
||||
left join usr u on u.id = g.user_id
|
||||
) r
|
||||
)
|
||||
-- build the final result JSON
|
||||
select json_build_object(
|
||||
'user', (
|
||||
select row_to_json(r) as usr from (
|
||||
@@ -45,6 +81,9 @@ select json_build_object(
|
||||
left join machine m on u.id = m.user_id
|
||||
) r
|
||||
),
|
||||
'org', (select organization from org),
|
||||
'metadata', (select metadata from metadata)
|
||||
'org', (select organization from user_org),
|
||||
'metadata', (select metadata from metadata),
|
||||
'user_grants', (select grants from grants)
|
||||
);
|
||||
|
||||
-- execute q('231965491734773762','230690539048009730', '{"236645808328409090","240762134579904514"}')
|
3
internal/query/testdata/userinfo_human.json
vendored
3
internal/query/testdata/userinfo_human.json
vendored
@@ -41,5 +41,6 @@
|
||||
"key": "foo",
|
||||
"value": "YmFy"
|
||||
}
|
||||
]
|
||||
],
|
||||
"user_grants": null
|
||||
}
|
||||
|
86
internal/query/testdata/userinfo_human_grants.json
vendored
Normal file
86
internal/query/testdata/userinfo_human_grants.json
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"user": {
|
||||
"id": "231965491734773762",
|
||||
"creation_date": "2023-09-15T06:10:07.434142+00:00",
|
||||
"change_date": "2023-11-14T13:27:02.072318+00:00",
|
||||
"sequence": 1148,
|
||||
"state": 1,
|
||||
"resource_owner": "231848297847848962",
|
||||
"username": "tim+tesmail@zitadel.com",
|
||||
"human": {
|
||||
"first_name": "Tim",
|
||||
"last_name": "Mohlmann",
|
||||
"nick_name": "muhlemmer",
|
||||
"display_name": "Tim Mohlmann",
|
||||
"avatar_key": null,
|
||||
"email": "tim+tesmail@zitadel.com",
|
||||
"is_email_verified": true,
|
||||
"phone": "+40123456789",
|
||||
"is_phone_verified": false
|
||||
},
|
||||
"machine": null
|
||||
},
|
||||
"org": {
|
||||
"name": "demo",
|
||||
"primary_domain": "demo.localhost"
|
||||
},
|
||||
"metadata": [
|
||||
{
|
||||
"creation_date": "2023-11-14T13:26:03.553702+00:00",
|
||||
"change_date": "2023-11-14T13:26:03.553702+00:00",
|
||||
"sequence": 1147,
|
||||
"resource_owner": "231848297847848962",
|
||||
"key": "bar",
|
||||
"value": "Zm9v"
|
||||
},
|
||||
{
|
||||
"creation_date": "2023-11-14T13:25:57.171368+00:00",
|
||||
"change_date": "2023-11-14T13:25:57.171368+00:00",
|
||||
"sequence": 1146,
|
||||
"resource_owner": "231848297847848962",
|
||||
"key": "foo",
|
||||
"value": "YmFy"
|
||||
}
|
||||
],
|
||||
"user_grants": [
|
||||
{
|
||||
"id": "240749256523120642",
|
||||
"grant_id": "",
|
||||
"state": 1,
|
||||
"creation_date": "2023-11-14T20:28:59.168208+00:00",
|
||||
"change_date": "2023-11-14T20:50:58.822391+00:00",
|
||||
"sequence": 2,
|
||||
"user_id": "231965491734773762",
|
||||
"roles": [
|
||||
"role1",
|
||||
"role2"
|
||||
],
|
||||
"resource_owner": "231848297847848962",
|
||||
"project_id": "236645808328409090",
|
||||
"org_name": "demo",
|
||||
"org_primary_domain": "demo.localhost",
|
||||
"project_name": "tests",
|
||||
"user_resource_owner": "231848297847848962"
|
||||
},
|
||||
{
|
||||
"id": "240762315572510722",
|
||||
"grant_id": "",
|
||||
"state": 1,
|
||||
"creation_date": "2023-11-14T22:38:42.967317+00:00",
|
||||
"change_date": "2023-11-14T22:38:42.967317+00:00",
|
||||
"sequence": 1,
|
||||
"user_id": "231965491734773762",
|
||||
"roles": [
|
||||
"role3",
|
||||
"role4"
|
||||
],
|
||||
"resource_owner": "231848297847848962",
|
||||
"project_id": "240762134579904514",
|
||||
"org_name": "demo",
|
||||
"org_primary_domain": "demo.localhost",
|
||||
"project_name": "tests2",
|
||||
"user_resource_owner": "231848297847848962"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -24,5 +24,6 @@
|
||||
"name": "demo",
|
||||
"primary_domain": "demo.localhost"
|
||||
},
|
||||
"metadata": null
|
||||
"metadata": null,
|
||||
"user_grants": null
|
||||
}
|
||||
|
@@ -34,5 +34,6 @@
|
||||
"key": "second",
|
||||
"value": "QnllIFdvcmxkIQ=="
|
||||
}
|
||||
]
|
||||
],
|
||||
"user_grants": null
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"user": null,
|
||||
"org": null,
|
||||
"metadata": null
|
||||
"metadata": null,
|
||||
"user_grants": null
|
||||
}
|
||||
|
@@ -22,32 +22,32 @@ import (
|
||||
|
||||
type UserGrant struct {
|
||||
// ID represents the aggregate id (id of the user grant)
|
||||
ID string
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
Sequence uint64
|
||||
Roles database.TextArray[string]
|
||||
ID string `json:"id,omitempty"`
|
||||
CreationDate time.Time `json:"creation_date,omitempty"`
|
||||
ChangeDate time.Time `json:"change_date,omitempty"`
|
||||
Sequence uint64 `json:"sequence,omitempty"`
|
||||
Roles database.TextArray[string] `json:"roles,omitempty"`
|
||||
// GrantID represents the project grant id
|
||||
GrantID string
|
||||
State domain.UserGrantState
|
||||
GrantID string `json:"grant_id,omitempty"`
|
||||
State domain.UserGrantState `json:"state,omitempty"`
|
||||
|
||||
UserID string
|
||||
Username string
|
||||
UserType domain.UserType
|
||||
UserResourceOwner string
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string
|
||||
DisplayName string
|
||||
AvatarURL string
|
||||
PreferredLoginName string
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
UserType domain.UserType `json:"user_type,omitempty"`
|
||||
UserResourceOwner string `json:"user_resource_owner,omitempty"`
|
||||
FirstName string `json:"first_name,omitempty"`
|
||||
LastName string `json:"last_name,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
AvatarURL string `json:"avatar_url,omitempty"`
|
||||
PreferredLoginName string `json:"preferred_login_name,omitempty"`
|
||||
|
||||
ResourceOwner string
|
||||
OrgName string
|
||||
OrgPrimaryDomain string
|
||||
ResourceOwner string `json:"resource_owner,omitempty"`
|
||||
OrgName string `json:"org_name,omitempty"`
|
||||
OrgPrimaryDomain string `json:"org_primary_domain,omitempty"`
|
||||
|
||||
ProjectID string
|
||||
ProjectName string
|
||||
ProjectID string `json:"project_id,omitempty"`
|
||||
ProjectName string `json:"project_name,omitempty"`
|
||||
}
|
||||
|
||||
type UserGrants struct {
|
||||
|
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
@@ -14,14 +15,17 @@ import (
|
||||
//go:embed embed/userinfo_by_id.sql
|
||||
var oidcUserInfoQuery string
|
||||
|
||||
func (q *Queries) GetOIDCUserInfo(ctx context.Context, userID string) (_ *OIDCUserInfo, err error) {
|
||||
func (q *Queries) GetOIDCUserInfo(ctx context.Context, userID string, roleAudience []string) (_ *OIDCUserInfo, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
var data []byte
|
||||
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
|
||||
return row.Scan(&data)
|
||||
}, oidcUserInfoQuery, userID, authz.GetInstance(ctx).InstanceID())
|
||||
},
|
||||
oidcUserInfoQuery,
|
||||
userID, authz.GetInstance(ctx).InstanceID(), database.TextArray[string](roleAudience),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInternal(err, "QUERY-Oath6", "Errors.Internal")
|
||||
}
|
||||
@@ -38,9 +42,10 @@ func (q *Queries) GetOIDCUserInfo(ctx context.Context, userID string) (_ *OIDCUs
|
||||
}
|
||||
|
||||
type OIDCUserInfo struct {
|
||||
User *User `json:"user,omitempty"`
|
||||
Metadata []UserMetadata `json:"metadata,omitempty"`
|
||||
Org *userInfoOrg `json:"org,omitempty"`
|
||||
User *User `json:"user,omitempty"`
|
||||
Metadata []UserMetadata `json:"metadata,omitempty"`
|
||||
Org *userInfoOrg `json:"org,omitempty"`
|
||||
UserGrants []UserGrant `json:"user_grants,omitempty"`
|
||||
}
|
||||
|
||||
type userInfoOrg struct {
|
||||
|
@@ -22,6 +22,8 @@ var (
|
||||
testdataUserInfoHumanNoMD string
|
||||
//go:embed testdata/userinfo_human.json
|
||||
testdataUserInfoHuman string
|
||||
//go:embed testdata/userinfo_human_grants.json
|
||||
testdataUserInfoHumanGrants string
|
||||
//go:embed testdata/userinfo_machine.json
|
||||
testdataUserInfoMachine string
|
||||
)
|
||||
@@ -29,7 +31,8 @@ var (
|
||||
func TestQueries_GetOIDCUserInfo(t *testing.T) {
|
||||
expQuery := regexp.QuoteMeta(oidcUserInfoQuery)
|
||||
type args struct {
|
||||
userID string
|
||||
userID string
|
||||
roleAudience []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -43,7 +46,7 @@ func TestQueries_GetOIDCUserInfo(t *testing.T) {
|
||||
args: args{
|
||||
userID: "231965491734773762",
|
||||
},
|
||||
mock: mockQueryErr(expQuery, sql.ErrConnDone, "231965491734773762", "instanceID"),
|
||||
mock: mockQueryErr(expQuery, sql.ErrConnDone, "231965491734773762", "instanceID", nil),
|
||||
wantErr: sql.ErrConnDone,
|
||||
},
|
||||
{
|
||||
@@ -51,7 +54,7 @@ func TestQueries_GetOIDCUserInfo(t *testing.T) {
|
||||
args: args{
|
||||
userID: "231965491734773762",
|
||||
},
|
||||
mock: mockQuery(expQuery, []string{"json_build_object"}, []driver.Value{`~~~`}, "231965491734773762", "instanceID"),
|
||||
mock: mockQuery(expQuery, []string{"json_build_object"}, []driver.Value{`~~~`}, "231965491734773762", "instanceID", nil),
|
||||
wantErr: errors.ThrowInternal(nil, "QUERY-Vohs6", "Errors.Internal"),
|
||||
},
|
||||
{
|
||||
@@ -59,7 +62,7 @@ func TestQueries_GetOIDCUserInfo(t *testing.T) {
|
||||
args: args{
|
||||
userID: "231965491734773762",
|
||||
},
|
||||
mock: mockQuery(expQuery, []string{"json_build_object"}, []driver.Value{testdataUserInfoNotFound}, "231965491734773762", "instanceID"),
|
||||
mock: mockQuery(expQuery, []string{"json_build_object"}, []driver.Value{testdataUserInfoNotFound}, "231965491734773762", "instanceID", nil),
|
||||
wantErr: errors.ThrowNotFound(nil, "QUERY-ahs4S", "Errors.User.NotFound"),
|
||||
},
|
||||
{
|
||||
@@ -67,7 +70,7 @@ func TestQueries_GetOIDCUserInfo(t *testing.T) {
|
||||
args: args{
|
||||
userID: "231965491734773762",
|
||||
},
|
||||
mock: mockQuery(expQuery, []string{"json_build_object"}, []driver.Value{testdataUserInfoHumanNoMD}, "231965491734773762", "instanceID"),
|
||||
mock: mockQuery(expQuery, []string{"json_build_object"}, []driver.Value{testdataUserInfoHumanNoMD}, "231965491734773762", "instanceID", nil),
|
||||
want: &OIDCUserInfo{
|
||||
User: &User{
|
||||
ID: "231965491734773762",
|
||||
@@ -102,7 +105,7 @@ func TestQueries_GetOIDCUserInfo(t *testing.T) {
|
||||
args: args{
|
||||
userID: "231965491734773762",
|
||||
},
|
||||
mock: mockQuery(expQuery, []string{"json_build_object"}, []driver.Value{testdataUserInfoHuman}, "231965491734773762", "instanceID"),
|
||||
mock: mockQuery(expQuery, []string{"json_build_object"}, []driver.Value{testdataUserInfoHuman}, "231965491734773762", "instanceID", nil),
|
||||
want: &OIDCUserInfo{
|
||||
User: &User{
|
||||
ID: "231965491734773762",
|
||||
@@ -149,12 +152,109 @@ func TestQueries_GetOIDCUserInfo(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "human with metadata and grants",
|
||||
args: args{
|
||||
userID: "231965491734773762",
|
||||
roleAudience: []string{"236645808328409090", "240762134579904514"},
|
||||
},
|
||||
mock: mockQuery(expQuery,
|
||||
[]string{"json_build_object"},
|
||||
[]driver.Value{testdataUserInfoHumanGrants},
|
||||
"231965491734773762", "instanceID", database.TextArray[string]{"236645808328409090", "240762134579904514"},
|
||||
),
|
||||
want: &OIDCUserInfo{
|
||||
User: &User{
|
||||
ID: "231965491734773762",
|
||||
CreationDate: time.Date(2023, time.September, 15, 6, 10, 7, 434142000, time.FixedZone("", 0)),
|
||||
ChangeDate: time.Date(2023, time.November, 14, 13, 27, 2, 72318000, time.FixedZone("", 0)),
|
||||
Sequence: 1148,
|
||||
State: 1,
|
||||
ResourceOwner: "231848297847848962",
|
||||
Username: "tim+tesmail@zitadel.com",
|
||||
Human: &Human{
|
||||
FirstName: "Tim",
|
||||
LastName: "Mohlmann",
|
||||
NickName: "muhlemmer",
|
||||
DisplayName: "Tim Mohlmann",
|
||||
AvatarKey: "",
|
||||
Email: "tim+tesmail@zitadel.com",
|
||||
IsEmailVerified: true,
|
||||
Phone: "+40123456789",
|
||||
IsPhoneVerified: false,
|
||||
},
|
||||
Machine: nil,
|
||||
},
|
||||
Org: &userInfoOrg{
|
||||
Name: "demo",
|
||||
PrimaryDomain: "demo.localhost",
|
||||
},
|
||||
Metadata: []UserMetadata{
|
||||
{
|
||||
CreationDate: time.Date(2023, time.November, 14, 13, 26, 3, 553702000, time.FixedZone("", 0)),
|
||||
ChangeDate: time.Date(2023, time.November, 14, 13, 26, 3, 553702000, time.FixedZone("", 0)),
|
||||
Sequence: 1147,
|
||||
ResourceOwner: "231848297847848962",
|
||||
Key: "bar",
|
||||
Value: []byte("foo"),
|
||||
},
|
||||
{
|
||||
CreationDate: time.Date(2023, time.November, 14, 13, 25, 57, 171368000, time.FixedZone("", 0)),
|
||||
ChangeDate: time.Date(2023, time.November, 14, 13, 25, 57, 171368000, time.FixedZone("", 0)),
|
||||
Sequence: 1146,
|
||||
ResourceOwner: "231848297847848962",
|
||||
Key: "foo",
|
||||
Value: []byte("bar"),
|
||||
},
|
||||
},
|
||||
UserGrants: []UserGrant{
|
||||
{
|
||||
ID: "240749256523120642",
|
||||
GrantID: "",
|
||||
State: 1,
|
||||
CreationDate: time.Date(2023, time.November, 14, 20, 28, 59, 168208000, time.FixedZone("", 0)),
|
||||
ChangeDate: time.Date(2023, time.November, 14, 20, 50, 58, 822391000, time.FixedZone("", 0)),
|
||||
Sequence: 2,
|
||||
UserID: "231965491734773762",
|
||||
Roles: []string{
|
||||
"role1",
|
||||
"role2",
|
||||
},
|
||||
ResourceOwner: "231848297847848962",
|
||||
ProjectID: "236645808328409090",
|
||||
OrgName: "demo",
|
||||
OrgPrimaryDomain: "demo.localhost",
|
||||
ProjectName: "tests",
|
||||
UserResourceOwner: "231848297847848962",
|
||||
},
|
||||
{
|
||||
ID: "240762315572510722",
|
||||
GrantID: "",
|
||||
State: 1,
|
||||
CreationDate: time.Date(2023, time.November, 14, 22, 38, 42, 967317000, time.FixedZone("", 0)),
|
||||
ChangeDate: time.Date(2023, time.November, 14, 22, 38, 42, 967317000, time.FixedZone("", 0)),
|
||||
Sequence: 1,
|
||||
UserID: "231965491734773762",
|
||||
Roles: []string{
|
||||
"role3",
|
||||
"role4",
|
||||
},
|
||||
ResourceOwner: "231848297847848962",
|
||||
ProjectID: "240762134579904514",
|
||||
OrgName: "demo",
|
||||
OrgPrimaryDomain: "demo.localhost",
|
||||
ProjectName: "tests2",
|
||||
UserResourceOwner: "231848297847848962",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "machine with metadata",
|
||||
args: args{
|
||||
userID: "240707570677841922",
|
||||
},
|
||||
mock: mockQuery(expQuery, []string{"json_build_object"}, []driver.Value{testdataUserInfoMachine}, "240707570677841922", "instanceID"),
|
||||
mock: mockQuery(expQuery, []string{"json_build_object"}, []driver.Value{testdataUserInfoMachine}, "240707570677841922", "instanceID", nil),
|
||||
want: &OIDCUserInfo{
|
||||
User: &User{
|
||||
ID: "240707570677841922",
|
||||
@@ -206,7 +306,7 @@ func TestQueries_GetOIDCUserInfo(t *testing.T) {
|
||||
}
|
||||
ctx := authz.NewMockContext("instanceID", "orgID", "loginClient")
|
||||
|
||||
got, err := q.GetOIDCUserInfo(ctx, tt.args.userID)
|
||||
got, err := q.GetOIDCUserInfo(ctx, tt.args.userID, tt.args.roleAudience)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
|
Reference in New Issue
Block a user