mirror of
https://github.com/zitadel/zitadel.git
synced 2025-10-17 22:03:44 +00:00
get oidc user info from projections and add actions
This commit is contained in:
@@ -34,6 +34,27 @@ func UserMetadataListFromQuery(c *actions.FieldConfig, metadata *query.UserMetad
|
||||
return c.Runtime.ToValue(result)
|
||||
}
|
||||
|
||||
func UserMetadataListFromSlice(c *actions.FieldConfig, metadata []query.UserMetadata) goja.Value {
|
||||
result := &userMetadataList{
|
||||
// Count was the only field ever queries from the DB in the old implementation,
|
||||
// so Sequence and LastRun are omitted.
|
||||
Count: uint64(len(metadata)),
|
||||
Metadata: make([]*userMetadata, len(metadata)),
|
||||
}
|
||||
for i, md := range metadata {
|
||||
result.Metadata[i] = &userMetadata{
|
||||
CreationDate: md.CreationDate,
|
||||
ChangeDate: md.ChangeDate,
|
||||
ResourceOwner: md.ResourceOwner,
|
||||
Sequence: md.Sequence,
|
||||
Key: md.Key,
|
||||
Value: metadataByteArrayToValue(md.Value, c.Runtime),
|
||||
}
|
||||
}
|
||||
|
||||
return c.Runtime.ToValue(result)
|
||||
}
|
||||
|
||||
func metadataByteArrayToValue(val []byte, runtime *goja.Runtime) goja.Value {
|
||||
var value interface{}
|
||||
if !json.Valid(val) {
|
||||
|
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
@@ -19,7 +20,8 @@ type Server struct {
|
||||
storage *OPStorage
|
||||
*op.LegacyServer
|
||||
|
||||
query *query.Queries
|
||||
query *query.Queries
|
||||
command *command.Commands
|
||||
|
||||
fallbackLogger *slog.Logger
|
||||
hashAlg crypto.HashAlgorithm
|
||||
|
@@ -2,11 +2,18 @@ package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/actions"
|
||||
"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"
|
||||
@@ -19,7 +26,7 @@ func (s *Server) getUserInfoWithRoles(ctx context.Context, userID, projectID str
|
||||
defer cancel()
|
||||
|
||||
userInfoChan := make(chan *userInfoResult)
|
||||
go s.getUserInfo(ctx, userID, scope, roleAudience, userInfoChan)
|
||||
go s.getUserInfo(ctx, userID, userInfoChan)
|
||||
|
||||
rolesChan := make(chan *assertRolesResult)
|
||||
go s.assertRoles(ctx, userID, projectID, scope, roleAudience, rolesChan)
|
||||
@@ -56,7 +63,7 @@ func (s *Server) getUserInfoWithRoles(ctx context.Context, userID, projectID str
|
||||
userInfo := userInfoToOIDC(userInfoResult.userInfo, scope)
|
||||
setUserInfoRoleClaims(userInfo, assertRolesResult.projectsRoles)
|
||||
|
||||
return userInfo, nil
|
||||
return userInfo, s.userinfoFlows(ctx, userInfoResult.userInfo, assertRolesResult.userGrants, userInfo)
|
||||
}
|
||||
|
||||
type userInfoResult struct {
|
||||
@@ -64,8 +71,8 @@ type userInfoResult struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *Server) getUserInfo(ctx context.Context, userID string, scope, roleAudience []string, rc chan<- *userInfoResult) {
|
||||
userInfo, err := s.storage.query.GetOIDCUserInfo(ctx, userID, scope, roleAudience)
|
||||
func (s *Server) getUserInfo(ctx context.Context, userID string, rc chan<- *userInfoResult) {
|
||||
userInfo, err := s.storage.query.GetOIDCUserInfo(ctx, userID)
|
||||
rc <- &userInfoResult{
|
||||
userInfo: userInfo,
|
||||
err: err,
|
||||
@@ -81,7 +88,7 @@ type assertRolesResult struct {
|
||||
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, domain.ScopeProjectsRoles) {
|
||||
if slices.Contains(scope, ScopeProjectsRoles) {
|
||||
roleAudience = domain.AddAudScopeToAudience(ctx, roleAudience, scope)
|
||||
}
|
||||
|
||||
@@ -148,19 +155,17 @@ func userInfoToOIDC(user *query.OIDCUserInfo, scope []string) *oidc.UserInfo {
|
||||
for _, s := range scope {
|
||||
switch s {
|
||||
case oidc.ScopeOpenID:
|
||||
out.Subject = user.ID
|
||||
out.Subject = user.User.ID
|
||||
case oidc.ScopeEmail:
|
||||
out.UserInfoEmail = userInfoEmailToOIDC(user)
|
||||
out.UserInfoEmail = userInfoEmailToOIDC(user.User)
|
||||
case oidc.ScopeProfile:
|
||||
out.UserInfoProfile = userInfoProfileToOidc(user)
|
||||
out.UserInfoProfile = userInfoProfileToOidc(user.User)
|
||||
case oidc.ScopePhone:
|
||||
out.UserInfoPhone = userInfoPhoneToOIDC(user)
|
||||
out.UserInfoPhone = userInfoPhoneToOIDC(user.User)
|
||||
case oidc.ScopeAddress:
|
||||
out.Address = userInfoAddressToOIDC(user)
|
||||
//TODO: handle address for human users as soon as implemented
|
||||
case ScopeUserMetaData:
|
||||
if len(user.Metadata) > 0 {
|
||||
out.AppendClaims(ClaimUserMetaData, user.Metadata)
|
||||
}
|
||||
setUserInfoMetadata(user.Metadata, out)
|
||||
case ScopeResourceOwner:
|
||||
setUserInfoOrgClaims(user, out)
|
||||
default:
|
||||
@@ -177,47 +182,173 @@ func userInfoToOIDC(user *query.OIDCUserInfo, scope []string) *oidc.UserInfo {
|
||||
return out
|
||||
}
|
||||
|
||||
func userInfoEmailToOIDC(user *query.OIDCUserInfo) oidc.UserInfoEmail {
|
||||
return oidc.UserInfoEmail{
|
||||
Email: string(user.Email),
|
||||
EmailVerified: oidc.Bool(user.IsEmailVerified),
|
||||
func userInfoEmailToOIDC(user *query.User) oidc.UserInfoEmail {
|
||||
if human := user.Human; human != nil {
|
||||
return oidc.UserInfoEmail{
|
||||
Email: string(human.Email),
|
||||
EmailVerified: oidc.Bool(human.IsEmailVerified),
|
||||
}
|
||||
}
|
||||
return oidc.UserInfoEmail{}
|
||||
}
|
||||
|
||||
func userInfoProfileToOidc(user *query.OIDCUserInfo) oidc.UserInfoProfile {
|
||||
func userInfoProfileToOidc(user *query.User) oidc.UserInfoProfile {
|
||||
if human := user.Human; human != nil {
|
||||
return oidc.UserInfoProfile{
|
||||
Name: human.DisplayName,
|
||||
GivenName: human.FirstName,
|
||||
FamilyName: human.LastName,
|
||||
Nickname: human.NickName,
|
||||
// Picture: domain.AvatarURL(o.assetAPIPrefix(ctx), user.ResourceOwner, user.Human.AvatarKey),
|
||||
Gender: getGender(human.Gender),
|
||||
Locale: oidc.NewLocale(human.PreferredLanguage),
|
||||
UpdatedAt: oidc.FromTime(user.ChangeDate),
|
||||
PreferredUsername: user.PreferredLoginName,
|
||||
}
|
||||
}
|
||||
if machine := user.Machine; machine != nil {
|
||||
return oidc.UserInfoProfile{
|
||||
Name: machine.Name,
|
||||
UpdatedAt: oidc.FromTime(user.ChangeDate),
|
||||
PreferredUsername: user.PreferredLoginName,
|
||||
}
|
||||
}
|
||||
return oidc.UserInfoProfile{
|
||||
Name: user.Name,
|
||||
GivenName: user.FirstName,
|
||||
FamilyName: user.LastName,
|
||||
Nickname: user.NickName,
|
||||
// Picture: domain.AvatarURL(o.assetAPIPrefix(ctx), user.ResourceOwner, user.Human.AvatarKey),
|
||||
Gender: getGender(user.Gender),
|
||||
Locale: oidc.NewLocale(user.PreferredLanguage),
|
||||
UpdatedAt: oidc.FromTime(user.UpdatedAt),
|
||||
// PreferredUsername: user.PreferredLoginName,
|
||||
UpdatedAt: oidc.FromTime(user.ChangeDate),
|
||||
PreferredUsername: user.PreferredLoginName,
|
||||
}
|
||||
}
|
||||
|
||||
func userInfoPhoneToOIDC(user *query.OIDCUserInfo) oidc.UserInfoPhone {
|
||||
return oidc.UserInfoPhone{
|
||||
PhoneNumber: string(user.Phone),
|
||||
PhoneNumberVerified: user.IsPhoneVerified,
|
||||
func userInfoPhoneToOIDC(user *query.User) oidc.UserInfoPhone {
|
||||
if human := user.Human; human != nil {
|
||||
return oidc.UserInfoPhone{
|
||||
PhoneNumber: string(human.Phone),
|
||||
PhoneNumberVerified: human.IsPhoneVerified,
|
||||
}
|
||||
}
|
||||
return oidc.UserInfoPhone{}
|
||||
}
|
||||
|
||||
func userInfoAddressToOIDC(user *query.OIDCUserInfo) *oidc.UserInfoAddress {
|
||||
return &oidc.UserInfoAddress{
|
||||
// Formatted: ??,
|
||||
StreetAddress: user.StreetAddress,
|
||||
Locality: user.Locality,
|
||||
Region: user.Region,
|
||||
PostalCode: user.PostalCode,
|
||||
Country: user.Country,
|
||||
func setUserInfoMetadata(metadata []query.UserMetadata, out *oidc.UserInfo) {
|
||||
if len(metadata) == 0 {
|
||||
return
|
||||
}
|
||||
mdmap := make(map[string]string, len(metadata))
|
||||
for _, md := range metadata {
|
||||
mdmap[md.Key] = base64.RawURLEncoding.EncodeToString(md.Value)
|
||||
}
|
||||
out.AppendClaims(ClaimUserMetaData, mdmap)
|
||||
}
|
||||
|
||||
func setUserInfoOrgClaims(user *query.OIDCUserInfo, out *oidc.UserInfo) {
|
||||
out.AppendClaims(ClaimResourceOwner+"id", user.OrgID)
|
||||
out.AppendClaims(ClaimResourceOwner+"name", user.OrgName)
|
||||
out.AppendClaims(ClaimResourceOwner+"primary_domain", user.OrgPrimaryDomain)
|
||||
if org := user.Org; org != nil {
|
||||
out.AppendClaims(ClaimResourceOwner+"id", org.ID)
|
||||
out.AppendClaims(ClaimResourceOwner+"name", org.Name)
|
||||
out.AppendClaims(ClaimResourceOwner+"primary_domain", org.PrimaryDomain)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctxFields := actions.SetContextFields(
|
||||
actions.SetFields("v1",
|
||||
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)
|
||||
}
|
||||
}),
|
||||
actions.SetFields("user",
|
||||
actions.SetFields("getMetadata", func(c *actions.FieldConfig) interface{} {
|
||||
return func(goja.FunctionCall) goja.Value {
|
||||
return object.UserMetadataListFromSlice(c, user.Metadata)
|
||||
}
|
||||
}),
|
||||
actions.SetFields("grants", func(c *actions.FieldConfig) interface{} {
|
||||
return object.UserGrantsFromQuery(c, userGrants)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
for _, action := range queriedActions {
|
||||
actionCtx, cancel := context.WithTimeout(ctx, action.Timeout())
|
||||
claimLogs := []string{}
|
||||
|
||||
apiFields := actions.WithAPIFields(
|
||||
actions.SetFields("v1",
|
||||
actions.SetFields("userinfo",
|
||||
actions.SetFields("setClaim", func(key string, value interface{}) {
|
||||
if userInfo.Claims[key] == nil {
|
||||
userInfo.AppendClaims(key, value)
|
||||
return
|
||||
}
|
||||
claimLogs = append(claimLogs, fmt.Sprintf("key %q already exists", key))
|
||||
}),
|
||||
actions.SetFields("appendLogIntoClaims", func(entry string) {
|
||||
claimLogs = append(claimLogs, entry)
|
||||
}),
|
||||
),
|
||||
actions.SetFields("claims",
|
||||
actions.SetFields("setClaim", func(key string, value interface{}) {
|
||||
if userInfo.Claims[key] == nil {
|
||||
userInfo.AppendClaims(key, value)
|
||||
return
|
||||
}
|
||||
claimLogs = append(claimLogs, fmt.Sprintf("key %q already exists", key))
|
||||
}),
|
||||
actions.SetFields("appendLogIntoClaims", func(entry string) {
|
||||
claimLogs = append(claimLogs, entry)
|
||||
}),
|
||||
),
|
||||
actions.SetFields("user",
|
||||
actions.SetFields("setMetadata", func(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) != 2 {
|
||||
panic("exactly 2 (key, value) arguments expected")
|
||||
}
|
||||
key := call.Arguments[0].Export().(string)
|
||||
val := call.Arguments[1].Export()
|
||||
|
||||
value, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
logging.WithError(err).Debug("unable to marshal")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
metadata := &domain.Metadata{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
if _, err = s.command.SetUserMetadata(ctx, metadata, userInfo.Subject, user.User.ResourceOwner); err != nil {
|
||||
logging.WithError(err).Info("unable to set md in action")
|
||||
panic(err)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
err = actions.Run(
|
||||
actionCtx,
|
||||
ctxFields,
|
||||
apiFields,
|
||||
action.Script,
|
||||
action.Name,
|
||||
append(actions.ActionToOptions(action), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))...,
|
||||
)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(claimLogs) > 0 {
|
||||
userInfo.AppendClaims(fmt.Sprintf(ClaimActionLogFormat, action.Name), claimLogs)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@@ -1,22 +0,0 @@
|
||||
package domain
|
||||
|
||||
import "github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
|
||||
const (
|
||||
ScopeOpenID = oidc.ScopeOpenID
|
||||
ScopeProfile = oidc.ScopeProfile
|
||||
ScopeEmail = oidc.ScopeEmail
|
||||
ScopeAddress = oidc.ScopeAddress
|
||||
ScopePhone = oidc.ScopePhone
|
||||
ScopeOfflineAccess = oidc.ScopeOfflineAccess
|
||||
|
||||
ScopeProjectRolePrefix = "urn:zitadel:iam:org:project:role:"
|
||||
ScopeProjectsRoles = "urn:zitadel:iam:org:projects:roles"
|
||||
ClaimProjectRoles = "urn:zitadel:iam:org:project:roles"
|
||||
ClaimProjectRolesFormat = "urn:zitadel:iam:org:project:%s:roles"
|
||||
ScopeUserMetaData = "urn:zitadel:iam:user:metadata"
|
||||
ClaimUserMetaData = ScopeUserMetaData
|
||||
ScopeResourceOwner = "urn:zitadel:iam:user:resourceowner"
|
||||
ClaimResourceOwner = ScopeResourceOwner + ":"
|
||||
ClaimActionLogFormat = "urn:zitadel:iam:action:%s:log"
|
||||
)
|
50
internal/query/embed/userinfo_by_id.sql
Normal file
50
internal/query/embed/userinfo_by_id.sql
Normal file
@@ -0,0 +1,50 @@
|
||||
with usr as (
|
||||
select id, creation_date, change_date, sequence, state, resource_owner, username
|
||||
from projections.users8 u
|
||||
where id = $1
|
||||
and instance_id = $2
|
||||
),
|
||||
human as (
|
||||
select $1 as user_id, row_to_json(r) as human from (
|
||||
select first_name, last_name, nick_name, display_name, avatar_key, email, is_email_verified, phone, is_phone_verified
|
||||
from projections.users8_humans
|
||||
where user_id = $1
|
||||
and instance_id = $2
|
||||
) r
|
||||
),
|
||||
machine as (
|
||||
select $1 as user_id, row_to_json(r) as machine from (
|
||||
select name, description
|
||||
from projections.users8_machines
|
||||
where user_id = $1
|
||||
and instance_id = $2
|
||||
) r
|
||||
),
|
||||
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
|
||||
from projections.user_metadata4
|
||||
where user_id = $1
|
||||
and instance_id = $2
|
||||
) r
|
||||
),
|
||||
org as (
|
||||
select row_to_json(r) as organization from (
|
||||
select name, primary_domain
|
||||
from projections.orgs1 o
|
||||
join usr u on o.id = u.resource_owner
|
||||
where instance_id = $2
|
||||
) r
|
||||
)
|
||||
select json_build_object(
|
||||
'user', (
|
||||
select row_to_json(r) as usr from (
|
||||
select u.*, h.human, m.machine
|
||||
from usr u
|
||||
left join human h on u.id = h.user_id
|
||||
left join machine m on u.id = m.user_id
|
||||
) r
|
||||
),
|
||||
'organization', (select organization from org),
|
||||
'metadata', (select metadata from metadata)
|
||||
);
|
@@ -28,32 +28,32 @@ type Users struct {
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
ResourceOwner string
|
||||
Sequence uint64
|
||||
State domain.UserState
|
||||
Type domain.UserType
|
||||
Username string
|
||||
LoginNames database.TextArray[string]
|
||||
PreferredLoginName string
|
||||
Human *Human
|
||||
Machine *Machine
|
||||
ID string `json:"id,omitempty"`
|
||||
CreationDate time.Time `json:"creation_date,omitempty"`
|
||||
ChangeDate time.Time `json:"change_date,omitempty"`
|
||||
ResourceOwner string `json:"resource_owner,omitempty"`
|
||||
Sequence uint64 `json:"sequence,omitempty"`
|
||||
State domain.UserState `json:"state,omitempty"`
|
||||
Type domain.UserType `json:"type,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
LoginNames database.TextArray[string] `json:"login_names,omitempty"`
|
||||
PreferredLoginName string `json:"preferred_login_name,omitempty"`
|
||||
Human *Human `json:"human,omitempty"`
|
||||
Machine *Machine `json:"machine,omitempty"`
|
||||
}
|
||||
|
||||
type Human struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
NickName string
|
||||
DisplayName string
|
||||
AvatarKey string
|
||||
PreferredLanguage language.Tag
|
||||
Gender domain.Gender
|
||||
Email domain.EmailAddress
|
||||
IsEmailVerified bool
|
||||
Phone domain.PhoneNumber
|
||||
IsPhoneVerified bool
|
||||
FirstName string `json:"first_name,omitempty"`
|
||||
LastName string `json:"last_name,omitempty"`
|
||||
NickName string `json:"nick_name,omitempty"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
AvatarKey string `json:"avatar_key,omitempty"`
|
||||
PreferredLanguage language.Tag `json:"preferred_language,omitempty"`
|
||||
Gender domain.Gender `json:"gender,omitempty"`
|
||||
Email domain.EmailAddress `json:"email,omitempty"`
|
||||
IsEmailVerified bool `json:"is_email_verified,omitempty"`
|
||||
Phone domain.PhoneNumber `json:"phone,omitempty"`
|
||||
IsPhoneVerified bool `json:"is_phone_verified,omitempty"`
|
||||
}
|
||||
|
||||
type Profile struct {
|
||||
@@ -92,10 +92,10 @@ type Phone struct {
|
||||
}
|
||||
|
||||
type Machine struct {
|
||||
Name string
|
||||
Description string
|
||||
HasSecret bool
|
||||
AccessTokenType domain.OIDCTokenType
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
HasSecret bool `json:"has_secret,omitempty"`
|
||||
AccessTokenType domain.OIDCTokenType `json:"access_token_type,omitempty"`
|
||||
}
|
||||
|
||||
type NotifyUser struct {
|
||||
|
@@ -24,12 +24,12 @@ type UserMetadataList struct {
|
||||
}
|
||||
|
||||
type UserMetadata struct {
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
ResourceOwner string
|
||||
Sequence uint64
|
||||
Key string
|
||||
Value []byte
|
||||
CreationDate time.Time `json:"creation_date,omitempty"`
|
||||
ChangeDate time.Time `json:"change_date,omitempty"`
|
||||
ResourceOwner string `json:"resource_owner,omitempty"`
|
||||
Sequence uint64 `json:"sequence,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Value []byte `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
type UserMetadataSearchQueries struct {
|
||||
|
@@ -2,253 +2,43 @@ package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"golang.org/x/text/language"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
func (q *Queries) GetOIDCUserInfo(ctx context.Context, userID string, scope, roleAudience []string) (_ *OIDCUserInfo, err error) {
|
||||
if slices.Contains(scope, domain.ScopeProjectsRoles) {
|
||||
roleAudience = domain.AddAudScopeToAudience(ctx, roleAudience, scope)
|
||||
// TODO: we need to get the project roles and user roles.
|
||||
}
|
||||
//go:embed embed/userinfo_by_id.sql
|
||||
var oidcUserInfoQuery string
|
||||
|
||||
user := newOidcUserinfoReadModel(userID, scope)
|
||||
if err = q.eventstore.FilterToQueryReducer(ctx, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hasOrgScope(scope) {
|
||||
org := newoidcUserinfoOrganizationReadModel(user.ResourceOwner)
|
||||
if err = q.eventstore.FilterToQueryReducer(ctx, org); err != nil {
|
||||
return nil, err
|
||||
func (q *Queries) GetOIDCUserInfo(ctx context.Context, userID string) (_ *OIDCUserInfo, err error) {
|
||||
userInfo := new(OIDCUserInfo)
|
||||
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
|
||||
var data []byte
|
||||
if err := row.Scan(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.OrgID = org.AggregateID
|
||||
user.OrgName = org.Name
|
||||
user.OrgPrimaryDomain = org.PrimaryDomain
|
||||
return json.Unmarshal(data, userInfo)
|
||||
}, oidcUserInfoQuery, userID, authz.GetInstance(ctx).InstanceID())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get oidc user info: %w", err)
|
||||
}
|
||||
if userInfo.User == nil {
|
||||
return nil, errors.ThrowNotFound(nil, "QUERY-ahs4S", "Errors.User.NotFound")
|
||||
}
|
||||
|
||||
return &user.OIDCUserInfo, nil
|
||||
}
|
||||
|
||||
func hasOrgScope(scope []string) bool {
|
||||
return slices.ContainsFunc(scope, func(s string) bool {
|
||||
return s == domain.ScopeResourceOwner || strings.HasPrefix(s, domain.OrgIDScope)
|
||||
})
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
type OIDCUserInfo struct {
|
||||
ID string
|
||||
UserName string
|
||||
Name string
|
||||
FirstName string
|
||||
LastName string
|
||||
NickName string
|
||||
PreferredLanguage language.Tag
|
||||
Gender domain.Gender
|
||||
Avatar string
|
||||
UpdatedAt time.Time
|
||||
|
||||
Email domain.EmailAddress
|
||||
IsEmailVerified bool
|
||||
|
||||
Phone domain.PhoneNumber
|
||||
IsPhoneVerified bool
|
||||
|
||||
Country string
|
||||
Locality string
|
||||
PostalCode string
|
||||
Region string
|
||||
StreetAddress string
|
||||
|
||||
UserState domain.UserState
|
||||
UserType domain.UserType
|
||||
|
||||
OrgID string
|
||||
OrgName string
|
||||
OrgPrimaryDomain string
|
||||
|
||||
Metadata map[string]string
|
||||
}
|
||||
|
||||
type oidcUserInfoReadmodel struct {
|
||||
eventstore.ReadModel
|
||||
scope []string // Scope is used to determine events
|
||||
OIDCUserInfo
|
||||
}
|
||||
|
||||
func newOidcUserinfoReadModel(userID string, scope []string) *oidcUserInfoReadmodel {
|
||||
return &oidcUserInfoReadmodel{
|
||||
ReadModel: eventstore.ReadModel{
|
||||
AggregateID: userID,
|
||||
},
|
||||
scope: scope,
|
||||
OIDCUserInfo: OIDCUserInfo{
|
||||
ID: userID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (rm *oidcUserInfoReadmodel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AwaitOpenTransactions().
|
||||
AllowTimeTravel().
|
||||
AddQuery().
|
||||
AggregateTypes(user.AggregateType).
|
||||
AggregateIDs(rm.AggregateID).
|
||||
EventTypes(rm.scopeToEventTypes()...).
|
||||
Builder()
|
||||
}
|
||||
|
||||
// scopeToEventTypes sets required user events to obtain get the correct userinfo.
|
||||
// Events such as UserLocked, UserDeactivated and UserRemoved are not checked,
|
||||
// as access tokens should already be revoked.
|
||||
func (rm *oidcUserInfoReadmodel) scopeToEventTypes() []eventstore.EventType {
|
||||
types := make([]eventstore.EventType, 0, len(rm.scope))
|
||||
types = append(types, user.HumanAddedType, user.MachineAddedEventType)
|
||||
|
||||
for _, scope := range rm.scope {
|
||||
switch scope {
|
||||
case domain.ScopeEmail:
|
||||
types = append(types, user.HumanEmailChangedType, user.HumanEmailVerifiedType)
|
||||
case domain.ScopeProfile:
|
||||
types = append(types, user.HumanProfileChangedType, user.HumanAvatarAddedType, user.HumanAvatarRemovedType)
|
||||
case domain.ScopePhone:
|
||||
types = append(types, user.HumanPhoneChangedType, user.HumanPhoneVerifiedType, user.HumanPhoneRemovedType)
|
||||
case domain.ScopeAddress:
|
||||
types = append(types, user.HumanAddressChangedType)
|
||||
case domain.ScopeUserMetaData:
|
||||
types = append(types, user.MetadataSetType, user.MetadataRemovedType, user.MetadataRemovedAllType)
|
||||
}
|
||||
}
|
||||
return slices.Compact(types)
|
||||
}
|
||||
|
||||
func (rm *oidcUserInfoReadmodel) Reduce() error {
|
||||
for _, event := range rm.Events {
|
||||
switch e := event.(type) {
|
||||
case *user.HumanAddedEvent:
|
||||
rm.UserName = e.UserName
|
||||
rm.FirstName = e.FirstName
|
||||
rm.LastName = e.LastName
|
||||
rm.NickName = e.NickName
|
||||
rm.Name = e.DisplayName
|
||||
rm.PreferredLanguage = e.PreferredLanguage
|
||||
rm.Gender = e.Gender
|
||||
rm.Email = e.EmailAddress
|
||||
rm.Phone = e.PhoneNumber
|
||||
rm.Country = e.Country
|
||||
rm.Locality = e.Locality
|
||||
rm.PostalCode = e.PostalCode
|
||||
rm.Region = e.Region
|
||||
rm.StreetAddress = e.StreetAddress
|
||||
rm.UpdatedAt = e.Creation
|
||||
case *user.MachineAddedEvent:
|
||||
rm.UserName = e.UserName
|
||||
rm.Name = e.Name
|
||||
rm.UpdatedAt = e.Creation
|
||||
case *user.HumanEmailChangedEvent:
|
||||
rm.Email = e.EmailAddress
|
||||
rm.IsEmailVerified = false
|
||||
rm.UpdatedAt = e.Creation
|
||||
case *user.HumanEmailVerifiedEvent:
|
||||
rm.IsEmailVerified = e.IsEmailVerified
|
||||
rm.UpdatedAt = e.Creation
|
||||
case *user.HumanProfileChangedEvent:
|
||||
rm.FirstName = e.FirstName
|
||||
rm.LastName = e.LastName
|
||||
rm.NickName = gu.Value(e.NickName)
|
||||
rm.Name = gu.Value(e.DisplayName)
|
||||
rm.PreferredLanguage = gu.Value(e.PreferredLanguage)
|
||||
rm.Gender = gu.Value(e.Gender)
|
||||
rm.UpdatedAt = e.Creation
|
||||
case *user.HumanAvatarAddedEvent:
|
||||
rm.Avatar = e.StoreKey
|
||||
rm.UpdatedAt = e.Creation
|
||||
case *user.HumanAvatarRemovedEvent:
|
||||
rm.Avatar = ""
|
||||
rm.UpdatedAt = e.Creation
|
||||
case *user.HumanPhoneChangedEvent:
|
||||
rm.Phone = e.PhoneNumber
|
||||
rm.IsPhoneVerified = false
|
||||
rm.UpdatedAt = e.Creation
|
||||
case *user.HumanPhoneVerifiedEvent:
|
||||
rm.IsEmailVerified = e.IsPhoneVerified
|
||||
rm.UpdatedAt = e.Creation
|
||||
case *user.HumanPhoneRemovedEvent:
|
||||
rm.Phone = ""
|
||||
rm.IsPhoneVerified = false
|
||||
rm.UpdatedAt = e.Creation
|
||||
case *user.HumanAddressChangedEvent:
|
||||
rm.Country = gu.Value(e.Country)
|
||||
rm.Locality = gu.Value(e.Locality)
|
||||
rm.PostalCode = gu.Value(e.PostalCode)
|
||||
rm.Region = gu.Value(e.Region)
|
||||
rm.StreetAddress = gu.Value(e.StreetAddress)
|
||||
rm.UpdatedAt = e.Creation
|
||||
case *user.MetadataSetEvent:
|
||||
rm.Metadata[e.Key] = base64.RawURLEncoding.EncodeToString(e.Value)
|
||||
rm.UpdatedAt = e.Creation
|
||||
case *user.MetadataRemovedEvent:
|
||||
delete(rm.Metadata, e.Key)
|
||||
rm.UpdatedAt = e.Creation
|
||||
case *user.MetadataRemovedAllEvent:
|
||||
for key := range rm.Metadata {
|
||||
delete(rm.Metadata, key)
|
||||
}
|
||||
rm.UpdatedAt = e.Creation
|
||||
}
|
||||
}
|
||||
|
||||
return rm.ReadModel.Reduce()
|
||||
}
|
||||
|
||||
type oidcUserinfoOrganizationReadModel struct {
|
||||
eventstore.ReadModel
|
||||
|
||||
Name string
|
||||
PrimaryDomain string
|
||||
}
|
||||
|
||||
func newoidcUserinfoOrganizationReadModel(orgID string) *oidcUserinfoOrganizationReadModel {
|
||||
return &oidcUserinfoOrganizationReadModel{
|
||||
ReadModel: eventstore.ReadModel{
|
||||
AggregateID: orgID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (rm *oidcUserinfoOrganizationReadModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AwaitOpenTransactions().
|
||||
AllowTimeTravel().
|
||||
AddQuery().
|
||||
AggregateTypes(org.AggregateType).
|
||||
AggregateIDs(rm.AggregateID).
|
||||
EventTypes(org.OrgAddedEventType, org.OrgChangedEventType, org.OrgDomainPrimarySetEventType).
|
||||
Builder()
|
||||
}
|
||||
|
||||
func (rm *oidcUserinfoOrganizationReadModel) Reduce() error {
|
||||
for _, event := range rm.Events {
|
||||
switch e := event.(type) {
|
||||
case *org.OrgAddedEvent:
|
||||
rm.Name = e.Name
|
||||
case *org.OrgChangedEvent:
|
||||
rm.Name = e.Name
|
||||
case *org.DomainPrimarySetEvent:
|
||||
rm.PrimaryDomain = e.Domain
|
||||
}
|
||||
}
|
||||
|
||||
return rm.ReadModel.Reduce()
|
||||
User *User `json:"user,omitempty"`
|
||||
Metadata []UserMetadata `json:"metadata,omitempty"`
|
||||
Org *struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
PrimaryDomain string `json:"primary_domain,omitempty"`
|
||||
} `json:"org,omitempty"`
|
||||
}
|
||||
|
Reference in New Issue
Block a user