From 3584833021e1b3d18c5d546fab74e4f478d760d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 15 Nov 2023 14:49:20 +0200 Subject: [PATCH] add user_grants to the userinfo query --- internal/actions/object/user_grant.go | 30 ++++ internal/api/oidc/client.go | 24 ++- internal/api/oidc/introspect.go | 2 +- internal/api/oidc/userinfo.go | 163 ++++-------------- internal/query/embed/userinfo_by_id.sql | 49 +++++- internal/query/testdata/userinfo_human.json | 3 +- .../query/testdata/userinfo_human_grants.json | 86 +++++++++ .../query/testdata/userinfo_human_no_md.json | 3 +- internal/query/testdata/userinfo_machine.json | 3 +- .../query/testdata/userinfo_not_found.json | 3 +- internal/query/user_grant.go | 44 ++--- internal/query/userinfo.go | 15 +- internal/query/userinfo_test.go | 116 ++++++++++++- 13 files changed, 363 insertions(+), 178 deletions(-) create mode 100644 internal/query/testdata/userinfo_human_grants.json diff --git a/internal/actions/object/user_grant.go b/internal/actions/object/user_grant.go index 73de8bd0a08..21267a611c1 100644 --- a/internal/actions/object/user_grant.go +++ b/internal/actions/object/user_grant.go @@ -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 diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index 7f9e8a2c221..4994eb9f0a8 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -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) diff --git a/internal/api/oidc/introspect.go b/internal/api/oidc/introspect.go index 6796d7ef1d4..ce63392f87c 100644 --- a/internal/api/oidc/introspect.go +++ b/internal/api/oidc/introspect.go @@ -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 } diff --git a/internal/api/oidc/userinfo.go b/internal/api/oidc/userinfo.go index afb52b1f56a..0cc0bc1b5b8 100644 --- a/internal/api/oidc/userinfo.go +++ b/internal/api/oidc/userinfo.go @@ -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) } diff --git a/internal/query/embed/userinfo_by_id.sql b/internal/query/embed/userinfo_by_id.sql index 0926efe5cd8..0d41b0e1802 100644 --- a/internal/query/embed/userinfo_by_id.sql +++ b/internal/query/embed/userinfo_by_id.sql @@ -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"}') \ No newline at end of file diff --git a/internal/query/testdata/userinfo_human.json b/internal/query/testdata/userinfo_human.json index 45fb80b1cc5..dfc0e339391 100644 --- a/internal/query/testdata/userinfo_human.json +++ b/internal/query/testdata/userinfo_human.json @@ -41,5 +41,6 @@ "key": "foo", "value": "YmFy" } - ] + ], + "user_grants": null } diff --git a/internal/query/testdata/userinfo_human_grants.json b/internal/query/testdata/userinfo_human_grants.json new file mode 100644 index 00000000000..b90fc36e8ea --- /dev/null +++ b/internal/query/testdata/userinfo_human_grants.json @@ -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" + } + ] + } + \ No newline at end of file diff --git a/internal/query/testdata/userinfo_human_no_md.json b/internal/query/testdata/userinfo_human_no_md.json index 162d34ff278..9ab42651e63 100644 --- a/internal/query/testdata/userinfo_human_no_md.json +++ b/internal/query/testdata/userinfo_human_no_md.json @@ -24,5 +24,6 @@ "name": "demo", "primary_domain": "demo.localhost" }, - "metadata": null + "metadata": null, + "user_grants": null } diff --git a/internal/query/testdata/userinfo_machine.json b/internal/query/testdata/userinfo_machine.json index b5e2cc1623e..1463eb2158a 100644 --- a/internal/query/testdata/userinfo_machine.json +++ b/internal/query/testdata/userinfo_machine.json @@ -34,5 +34,6 @@ "key": "second", "value": "QnllIFdvcmxkIQ==" } - ] + ], + "user_grants": null } diff --git a/internal/query/testdata/userinfo_not_found.json b/internal/query/testdata/userinfo_not_found.json index d2757643d79..ab27684269e 100644 --- a/internal/query/testdata/userinfo_not_found.json +++ b/internal/query/testdata/userinfo_not_found.json @@ -1,5 +1,6 @@ { "user": null, "org": null, - "metadata": null + "metadata": null, + "user_grants": null } diff --git a/internal/query/user_grant.go b/internal/query/user_grant.go index 0dd366f464c..39503735505 100644 --- a/internal/query/user_grant.go +++ b/internal/query/user_grant.go @@ -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 { diff --git a/internal/query/userinfo.go b/internal/query/userinfo.go index e6c8804b65f..2cfbb6cdecf 100644 --- a/internal/query/userinfo.go +++ b/internal/query/userinfo.go @@ -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 { diff --git a/internal/query/userinfo_test.go b/internal/query/userinfo_test.go index 1e97186b72b..e5ab98d9598 100644 --- a/internal/query/userinfo_test.go +++ b/internal/query/userinfo_test.go @@ -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) })