2023-11-02 17:27:30 +02:00
|
|
|
package query
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/base64"
|
|
|
|
"slices"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"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"
|
|
|
|
)
|
|
|
|
|
2023-11-06 20:27:25 +02:00
|
|
|
func (q *Queries) GetOIDCUserInfo(ctx context.Context, userID string, scope, roleAudience []string) (_ *OIDCUserInfo, err error) {
|
2023-11-02 17:27:30 +02:00
|
|
|
if slices.Contains(scope, domain.ScopeProjectsRoles) {
|
|
|
|
roleAudience = domain.AddAudScopeToAudience(ctx, roleAudience, scope)
|
|
|
|
// TODO: we need to get the project roles and user roles.
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
user.OrgID = org.AggregateID
|
|
|
|
user.OrgName = org.Name
|
|
|
|
user.OrgPrimaryDomain = org.PrimaryDomain
|
|
|
|
}
|
|
|
|
|
2023-11-06 20:27:25 +02:00
|
|
|
return &user.OIDCUserInfo, nil
|
2023-11-02 17:27:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func hasOrgScope(scope []string) bool {
|
|
|
|
return slices.ContainsFunc(scope, func(s string) bool {
|
|
|
|
return s == domain.ScopeResourceOwner || strings.HasPrefix(s, domain.OrgIDScope)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-11-06 20:27:25 +02:00
|
|
|
type OIDCUserInfo struct {
|
2023-11-02 17:27:30 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-11-06 20:27:25 +02:00
|
|
|
type oidcUserInfoReadmodel struct {
|
2023-11-02 17:27:30 +02:00
|
|
|
eventstore.ReadModel
|
|
|
|
scope []string // Scope is used to determine events
|
2023-11-06 20:27:25 +02:00
|
|
|
OIDCUserInfo
|
2023-11-02 17:27:30 +02:00
|
|
|
}
|
|
|
|
|
2023-11-06 20:27:25 +02:00
|
|
|
func newOidcUserinfoReadModel(userID string, scope []string) *oidcUserInfoReadmodel {
|
|
|
|
return &oidcUserInfoReadmodel{
|
2023-11-02 17:27:30 +02:00
|
|
|
ReadModel: eventstore.ReadModel{
|
|
|
|
AggregateID: userID,
|
|
|
|
},
|
|
|
|
scope: scope,
|
2023-11-06 20:27:25 +02:00
|
|
|
OIDCUserInfo: OIDCUserInfo{
|
2023-11-02 17:27:30 +02:00
|
|
|
ID: userID,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-06 20:27:25 +02:00
|
|
|
func (rm *oidcUserInfoReadmodel) Query() *eventstore.SearchQueryBuilder {
|
2023-11-02 17:27:30 +02:00
|
|
|
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.
|
2023-11-06 20:27:25 +02:00
|
|
|
func (rm *oidcUserInfoReadmodel) scopeToEventTypes() []eventstore.EventType {
|
2023-11-02 17:27:30 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2023-11-06 20:27:25 +02:00
|
|
|
func (rm *oidcUserInfoReadmodel) Reduce() error {
|
2023-11-02 17:27:30 +02:00
|
|
|
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()
|
|
|
|
}
|