get oidc user info from projections and add actions

This commit is contained in:
Tim Möhlmann
2023-11-13 18:13:34 +02:00
parent d69b9999a1
commit 8eea5eccd1
8 changed files with 309 additions and 337 deletions

View File

@@ -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) {

View File

@@ -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

View File

@@ -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
}

View File

@@ -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"
)

View 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)
);

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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"`
}