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

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