mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-09 03:12:36 +00:00
get oidc user info from projections and add actions
This commit is contained in:
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