fix: add avatar url in members, user grants, session and oidc responses (#1852)

* fix: add avatar url in members, user grants, session and oidc responses

* fix auth request tests
This commit is contained in:
Livio Amstutz 2021-06-11 13:20:39 +02:00 committed by GitHub
parent 1e77b8aeae
commit 770994e143
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 368 additions and 207 deletions

View File

@ -90,6 +90,7 @@ AuthZ:
Auth:
SearchLimit: 1000
Domain: $ZITADEL_DEFAULT_DOMAIN
APIDomain: $ZITADEL_API_DOMAIN
Eventstore:
ServiceName: 'authAPI'
Repository:
@ -139,6 +140,7 @@ Auth:
Admin:
SearchLimit: 1000
Domain: $ZITADEL_DEFAULT_DOMAIN
APIDomain: $ZITADEL_API_DOMAIN
Eventstore:
ServiceName: 'Admin'
Repository:
@ -176,6 +178,7 @@ Admin:
Mgmt:
SearchLimit: 1000
Domain: $ZITADEL_DEFAULT_DOMAIN
APIDomain: $ZITADEL_API_DOMAIN
Eventstore:
ServiceName: 'ManagementAPI'
Repository:

View File

@ -59,6 +59,7 @@ title: zitadel/member.proto
| first_name | string | - | |
| last_name | string | - | |
| display_name | string | - | |
| avatar_url | string | - | |

View File

@ -237,6 +237,7 @@ this query is always equals
| display_name | string | - | |
| preferred_language | string | - | |
| gender | Gender | - | |
| avatar_url | string | - | |
@ -291,6 +292,7 @@ this query is always equals
| login_name | string | - | |
| display_name | string | - | |
| details | zitadel.v1.ObjectDetails | - | |
| avatar_url | string | - | |
@ -357,6 +359,7 @@ UserTypeQuery is always equals
| project_id | string | - | |
| project_name | string | - | |
| project_grant_id | string | - | |
| avatar_url | string | - | |

1
go.sum
View File

@ -1116,7 +1116,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@ -23,11 +23,12 @@ import (
)
type IAMRepository struct {
Eventstore v1.Eventstore
SearchLimit uint64
View *admin_view.View
SystemDefaults systemdefaults.SystemDefaults
Roles []string
Eventstore v1.Eventstore
SearchLimit uint64
View *admin_view.View
SystemDefaults systemdefaults.SystemDefaults
Roles []string
PrefixAvatarURL string
}
func (repo *IAMRepository) IAMMemberByID(ctx context.Context, iamID, userID string) (*iam_model.IAMMemberView, error) {
@ -35,7 +36,7 @@ func (repo *IAMRepository) IAMMemberByID(ctx context.Context, iamID, userID stri
if err != nil {
return nil, err
}
return iam_es_model.IAMMemberToModel(member), nil
return iam_es_model.IAMMemberToModel(member, repo.PrefixAvatarURL), nil
}
func (repo *IAMRepository) SearchIAMMembers(ctx context.Context, request *iam_model.IAMMemberSearchRequest) (*iam_model.IAMMemberSearchResponse, error) {
@ -53,7 +54,7 @@ func (repo *IAMRepository) SearchIAMMembers(ctx context.Context, request *iam_mo
Offset: request.Offset,
Limit: request.Limit,
TotalResult: count,
Result: iam_es_model.IAMMembersToModel(members),
Result: iam_es_model.IAMMembersToModel(members, repo.PrefixAvatarURL),
}
if err == nil {
result.Sequence = sequence.CurrentSequence
@ -340,7 +341,7 @@ func (repo *IAMRepository) SearchIAMMembersx(ctx context.Context, request *iam_m
Offset: request.Offset,
Limit: request.Limit,
TotalResult: count,
Result: iam_es_model.IAMMembersToModel(members),
Result: iam_es_model.IAMMembersToModel(members, repo.PrefixAvatarURL),
}
if err == nil {
result.Sequence = sequence.CurrentSequence

View File

@ -13,10 +13,11 @@ import (
)
type UserRepo struct {
SearchLimit uint64
Eventstore v1.Eventstore
View *view.View
SystemDefaults systemdefaults.SystemDefaults
SearchLimit uint64
Eventstore v1.Eventstore
View *view.View
SystemDefaults systemdefaults.SystemDefaults
PrefixAvatarURL string
}
func (repo *UserRepo) Health(ctx context.Context) error {
@ -34,7 +35,7 @@ func (repo *UserRepo) SearchUsers(ctx context.Context, request *model.UserSearch
Offset: request.Offset,
Limit: request.Limit,
TotalResult: count,
Result: usr_view_model.UsersToModel(users),
Result: usr_view_model.UsersToModel(users, repo.PrefixAvatarURL),
}
if sequenceErr == nil {
result.Sequence = sequence.CurrentSequence

View File

@ -132,7 +132,9 @@ func (m *IAMMember) processUser(event *es_models.Event) (err error) {
usr_es_model.UserEmailChanged,
usr_es_model.HumanProfileChanged,
usr_es_model.HumanEmailChanged,
usr_es_model.MachineChanged:
usr_es_model.MachineChanged,
usr_es_model.HumanAvatarAdded,
usr_es_model.HumanAvatarRemoved:
members, err := m.view.IAMMembersByUserID(event.AggregateID)
if err != nil {
return err
@ -165,6 +167,9 @@ func (m *IAMMember) fillData(member *iam_view_model.IAMMemberView) (err error) {
func (m *IAMMember) fillUserData(member *iam_view_model.IAMMemberView, user *view_model.UserView) error {
org, err := m.getOrgByID(context.Background(), user.ResourceOwner)
if err != nil {
return err
}
policy := org.OrgIamPolicy
if policy == nil {
policy, err = m.getDefaultOrgIAMPolicy(context.TODO())
@ -174,11 +179,13 @@ func (m *IAMMember) fillUserData(member *iam_view_model.IAMMemberView, user *vie
}
member.UserName = user.UserName
member.PreferredLoginName = user.GenerateLoginName(org.GetPrimaryDomain().Domain, policy.UserLoginMustBeDomain)
member.UserResourceOwner = user.ResourceOwner
if user.HumanView != nil {
member.FirstName = user.FirstName
member.LastName = user.LastName
member.DisplayName = user.FirstName + " " + user.LastName
member.Email = user.Email
member.AvatarKey = user.AvatarKey
}
if user.MachineView != nil {
member.DisplayName = user.MachineView.Name

View File

@ -2,6 +2,7 @@ package eventsourcing
import (
"context"
"github.com/caos/zitadel/internal/admin/repository/eventsourcing/eventstore"
"github.com/caos/zitadel/internal/admin/repository/eventsourcing/spooler"
admin_view "github.com/caos/zitadel/internal/admin/repository/eventsourcing/view"
@ -18,6 +19,7 @@ type Config struct {
View types.SQL
Spooler spooler.SpoolerConfig
Domain string
APIDomain string
}
type EsRepository struct {
@ -44,6 +46,7 @@ func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, s
}
spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, systemDefaults, static, localDevMode)
assetsAPI := conf.APIDomain + "/assets/v1/"
return &EsRepository{
spooler: spool,
@ -54,11 +57,12 @@ func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, s
SystemDefaults: systemDefaults,
},
IAMRepository: eventstore.IAMRepository{
Eventstore: es,
View: view,
SystemDefaults: systemDefaults,
SearchLimit: conf.SearchLimit,
Roles: roles,
Eventstore: es,
View: view,
SystemDefaults: systemDefaults,
SearchLimit: conf.SearchLimit,
Roles: roles,
PrefixAvatarURL: assetsAPI,
},
AdministratorRepo: eventstore.AdministratorRepo{
View: view,
@ -70,10 +74,11 @@ func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, s
SystemDefaults: systemDefaults,
},
UserRepo: eventstore.UserRepo{
Eventstore: es,
View: view,
SearchLimit: conf.SearchLimit,
SystemDefaults: systemDefaults,
Eventstore: es,
View: view,
SearchLimit: conf.SearchLimit,
SystemDefaults: systemDefaults,
PrefixAvatarURL: assetsAPI,
},
}, nil
}

View File

@ -24,6 +24,7 @@ func IAMMemberToPb(m *iam_model.IAMMemberView) *member_pb.Member {
FirstName: m.FirstName,
LastName: m.LastName,
DisplayName: m.DisplayName,
AvatarUrl: m.AvatarURL,
Details: object.ToViewDetailsPb(
m.Sequence,
m.CreationDate,

View File

@ -24,6 +24,7 @@ func OrgMemberToPb(m *org_model.OrgMemberView) *member_pb.Member {
FirstName: m.FirstName,
LastName: m.LastName,
DisplayName: m.DisplayName,
AvatarUrl: m.AvatarURL,
Details: object.ToViewDetailsPb(
m.Sequence,
m.CreationDate,

View File

@ -24,6 +24,7 @@ func ProjectGrantMemberToPb(m *proj_model.ProjectGrantMemberView) *member_pb.Mem
FirstName: m.FirstName,
LastName: m.LastName,
DisplayName: m.DisplayName,
AvatarUrl: m.AvatarURL,
Details: object.ToViewDetailsPb(
m.Sequence,
m.CreationDate,

View File

@ -24,6 +24,7 @@ func ProjectMemberToPb(m *proj_model.ProjectMemberView) *member_pb.Member {
FirstName: m.FirstName,
LastName: m.LastName,
DisplayName: m.DisplayName,
AvatarUrl: m.AvatarURL,
Details: object.ToViewDetailsPb(
m.Sequence,
m.CreationDate,

View File

@ -56,6 +56,7 @@ func HumanToPb(view *model.HumanView) *user_pb.Human {
DisplayName: view.DisplayName,
PreferredLanguage: view.PreferredLanguage,
Gender: GenderToPb(view.Gender),
AvatarUrl: view.AvatarURL,
},
Email: &user_pb.Email{
Email: view.Email,
@ -83,6 +84,7 @@ func ProfileToPb(profile *model.Profile) *user_pb.Profile {
DisplayName: profile.DisplayName,
PreferredLanguage: profile.PreferredLanguage.String(),
Gender: GenderToPb(profile.Gender),
AvatarUrl: profile.AvatarURL,
}
}

View File

@ -24,6 +24,7 @@ func UserSessionToPb(session *user_model.UserSessionView) *user.Session {
LoginName: session.LoginName,
DisplayName: session.DisplayName,
AuthState: SessionStateToPb(session.State),
AvatarUrl: session.AvatarURL,
Details: object.ToViewDetailsPb(
session.Sequence,
session.CreationDate,

View File

@ -32,6 +32,7 @@ func UserGrantToPb(grant *usr_grant_model.UserGrantView) *user_pb.UserGrant {
ProjectId: grant.ProjectID,
ProjectName: grant.ProjectName,
ProjectGrantId: grant.GrantID,
AvatarUrl: grant.AvatarURL,
Details: object.ToViewDetailsPb(
grant.Sequence,
grant.CreationDate,

View File

@ -160,6 +160,7 @@ func (o *OPStorage) SetUserinfoFromScopes(ctx context.Context, userInfo oidc.Use
userInfo.SetGender(oidc.Gender(getGender(user.Gender)))
locale, _ := language.Parse(user.PreferredLanguage)
userInfo.SetLocale(locale)
userInfo.SetPicture(user.AvatarURL)
} else {
userInfo.SetName(user.MachineView.Name)
}

View File

@ -2,9 +2,10 @@ package eventstore
import (
"context"
"time"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/domain"
"time"
"github.com/caos/logging"
@ -57,9 +58,11 @@ type AuthRequestRepo struct {
type userSessionViewProvider interface {
UserSessionByIDs(string, string) (*user_view_model.UserSessionView, error)
UserSessionsByAgentID(string) ([]*user_view_model.UserSessionView, error)
PrefixAvatarURL() string
}
type userViewProvider interface {
UserByID(string) (*user_view_model.UserView, error)
PrefixAvatarURL() string
}
type loginPolicyViewProvider interface {
@ -616,6 +619,7 @@ func (repo *AuthRequestRepo) usersForUserSelection(request *domain.AuthRequest)
DisplayName: session.DisplayName,
UserName: session.UserName,
LoginName: session.LoginName,
ResourceOwner: session.ResourceOwner,
AvatarKey: session.AvatarKey,
UserSessionState: auth_req_model.UserSessionStateToDomain(session.State),
SelectionPossible: request.RequestedOrgID == "" || request.RequestedOrgID == session.ResourceOwner,
@ -767,7 +771,7 @@ func userSessionsByUserAgentID(provider userSessionViewProvider, agentID string)
if err != nil {
return nil, err
}
return user_view_model.UserSessionsToModel(session), nil
return user_view_model.UserSessionsToModel(session, provider.PrefixAvatarURL()), nil
}
func userSessionByIDs(ctx context.Context, provider userSessionViewProvider, eventProvider userEventProvider, agentID string, user *user_model.UserView) (*user_model.UserSessionView, error) {
@ -781,7 +785,7 @@ func userSessionByIDs(ctx context.Context, provider userSessionViewProvider, eve
events, err := eventProvider.UserEventsByID(ctx, user.ID, session.Sequence)
if err != nil {
logging.Log("EVENT-Hse6s").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("error retrieving new events")
return user_view_model.UserSessionToModel(session), nil
return user_view_model.UserSessionToModel(session, provider.PrefixAvatarURL()), nil
}
sessionCopy := *session
for _, event := range events {
@ -806,7 +810,7 @@ func userSessionByIDs(ctx context.Context, provider userSessionViewProvider, eve
eventData, err := user_view_model.UserSessionFromEvent(event)
if err != nil {
logging.Log("EVENT-sdgT3").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("error getting event data")
return user_view_model.UserSessionToModel(session), nil
return user_view_model.UserSessionToModel(session, provider.PrefixAvatarURL()), nil
}
if eventData.UserAgentID != agentID {
continue
@ -817,7 +821,7 @@ func userSessionByIDs(ctx context.Context, provider userSessionViewProvider, eve
err := sessionCopy.AppendEvent(event)
logging.Log("EVENT-qbhj3").OnError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Warn("error appending event")
}
return user_view_model.UserSessionToModel(&sessionCopy), nil
return user_view_model.UserSessionToModel(&sessionCopy, provider.PrefixAvatarURL()), nil
}
func activeUserByID(ctx context.Context, userViewProvider userViewProvider, userEventProvider userEventProvider, orgViewProvider orgViewProvider, userID string) (*user_model.UserView, error) {
@ -856,24 +860,24 @@ func userByID(ctx context.Context, viewProvider userViewProvider, eventProvider
events, err := eventProvider.UserEventsByID(ctx, userID, user.Sequence)
if err != nil {
logging.Log("EVENT-dfg42").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("error retrieving new events")
return user_view_model.UserToModel(user), nil
return user_view_model.UserToModel(user, viewProvider.PrefixAvatarURL()), nil
}
if len(events) == 0 {
if viewErr != nil {
return nil, viewErr
}
return user_view_model.UserToModel(user), viewErr
return user_view_model.UserToModel(user, viewProvider.PrefixAvatarURL()), viewErr
}
userCopy := *user
for _, event := range events {
if err := userCopy.AppendEvent(event); err != nil {
return user_view_model.UserToModel(user), nil
return user_view_model.UserToModel(user, viewProvider.PrefixAvatarURL()), nil
}
}
if userCopy.State == int32(user_model.UserStateDeleted) {
return nil, errors.ThrowNotFound(nil, "EVENT-3F9so", "Errors.User.NotFound")
}
return user_view_model.UserToModel(&userCopy), nil
return user_view_model.UserToModel(&userCopy, viewProvider.PrefixAvatarURL()), nil
}
func linkExternalIDPs(ctx context.Context, userCommandProvider userCommandProvider, request *domain.AuthRequest) error {

View File

@ -3,20 +3,19 @@ package eventstore
import (
"context"
"encoding/json"
"github.com/caos/zitadel/internal/domain"
"testing"
"time"
iam_model "github.com/caos/zitadel/internal/iam/model"
iam_view_model "github.com/caos/zitadel/internal/iam/repository/view/model"
"github.com/stretchr/testify/assert"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
"github.com/caos/zitadel/internal/auth_request/model"
"github.com/caos/zitadel/internal/auth_request/repository/cache"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
iam_model "github.com/caos/zitadel/internal/iam/model"
iam_view_model "github.com/caos/zitadel/internal/iam/repository/view/model"
org_model "github.com/caos/zitadel/internal/org/model"
org_view_model "github.com/caos/zitadel/internal/org/repository/view/model"
proj_view_model "github.com/caos/zitadel/internal/project/repository/view/model"
@ -36,6 +35,10 @@ func (m *mockViewNoUserSession) UserSessionsByAgentID(string) ([]*user_view_mode
return nil, nil
}
func (m *mockViewNoUserSession) PrefixAvatarURL() string {
return ""
}
type mockViewErrUserSession struct{}
func (m *mockViewErrUserSession) UserSessionByIDs(string, string) (*user_view_model.UserSessionView, error) {
@ -46,6 +49,10 @@ func (m *mockViewErrUserSession) UserSessionsByAgentID(string) ([]*user_view_mod
return nil, errors.ThrowInternal(nil, "id", "internal error")
}
func (m *mockViewErrUserSession) PrefixAvatarURL() string {
return ""
}
type mockViewUserSession struct {
ExternalLoginVerification time.Time
PasswordlessVerification time.Time
@ -83,12 +90,20 @@ func (m *mockViewUserSession) UserSessionsByAgentID(string) ([]*user_view_model.
return sessions, nil
}
func (m *mockViewUserSession) PrefixAvatarURL() string {
return "prefix/"
}
type mockViewNoUser struct{}
func (m *mockViewNoUser) UserByID(string) (*user_view_model.UserView, error) {
return nil, errors.ThrowNotFound(nil, "id", "user not found")
}
func (m *mockViewNoUser) PrefixAvatarURL() string {
return ""
}
type mockEventUser struct {
Event *es_models.Event
}
@ -152,6 +167,10 @@ func (m *mockViewUser) UserByID(string) (*user_view_model.UserView, error) {
}, nil
}
func (m *mockViewUser) PrefixAvatarURL() string {
return ""
}
type mockViewOrg struct {
State org_model.OrgState
}
@ -291,11 +310,13 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
UserID: "id1",
LoginName: "loginname1",
SelectionPossible: true,
ResourceOwner: "orgID1",
},
{
UserID: "id2",
LoginName: "loginname2",
SelectionPossible: true,
ResourceOwner: "orgID2",
},
},
}},
@ -329,11 +350,13 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
UserID: "id1",
LoginName: "loginname1",
SelectionPossible: true,
ResourceOwner: "orgID1",
},
{
UserID: "id2",
LoginName: "loginname2",
SelectionPossible: false,
ResourceOwner: "orgID2",
},
},
}},

View File

@ -24,10 +24,11 @@ import (
)
type UserRepo struct {
SearchLimit uint64
Eventstore v1.Eventstore
View *view.View
SystemDefaults systemdefaults.SystemDefaults
SearchLimit uint64
Eventstore v1.Eventstore
View *view.View
SystemDefaults systemdefaults.SystemDefaults
PrefixAvatarURL string
}
func (repo *UserRepo) Health(ctx context.Context) error {
@ -153,18 +154,18 @@ func (repo *UserRepo) UserByID(ctx context.Context, id string) (*model.UserView,
events, err := repo.getUserEvents(ctx, id, user.Sequence)
if err != nil {
logging.Log("EVENT-PSoc3").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("error retrieving new events")
return usr_view_model.UserToModel(user), nil
return usr_view_model.UserToModel(user, repo.PrefixAvatarURL), nil
}
userCopy := *user
for _, event := range events {
if err := userCopy.AppendEvent(event); err != nil {
return usr_view_model.UserToModel(user), nil
return usr_view_model.UserToModel(user, repo.PrefixAvatarURL), nil
}
}
if userCopy.State == int32(model.UserStateDeleted) {
return nil, errors.ThrowNotFound(nil, "EVENT-vZ8us", "Errors.User.NotFound")
}
return usr_view_model.UserToModel(&userCopy), nil
return usr_view_model.UserToModel(&userCopy, repo.PrefixAvatarURL), nil
}
func (repo *UserRepo) UserEventsByID(ctx context.Context, id string, sequence uint64) ([]*models.Event, error) {
@ -179,18 +180,18 @@ func (repo *UserRepo) UserByLoginName(ctx context.Context, loginname string) (*m
events, err := repo.getUserEvents(ctx, user.ID, user.Sequence)
if err != nil {
logging.Log("EVENT-PSoc3").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("error retrieving new events")
return usr_view_model.UserToModel(user), nil
return usr_view_model.UserToModel(user, repo.PrefixAvatarURL), nil
}
userCopy := *user
for _, event := range events {
if err := userCopy.AppendEvent(event); err != nil {
return usr_view_model.UserToModel(user), nil
return usr_view_model.UserToModel(user, repo.PrefixAvatarURL), nil
}
}
if userCopy.State == int32(model.UserStateDeleted) {
return nil, errors.ThrowNotFound(nil, "EVENT-vZ8us", "Errors.User.NotFound")
}
return usr_view_model.UserToModel(&userCopy), nil
return usr_view_model.UserToModel(&userCopy, repo.PrefixAvatarURL), nil
}
func (repo *UserRepo) MyUserChanges(ctx context.Context, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.UserChanges, error) {
changes, err := repo.getUserChanges(ctx, authz.GetCtxData(ctx).UserID, lastSequence, limit, sortAscending, retention)
@ -233,7 +234,7 @@ func (repo *UserRepo) SearchUsers(ctx context.Context, request *model.UserSearch
Offset: request.Offset,
Limit: request.Limit,
TotalResult: count,
Result: usr_view_model.UsersToModel(users),
Result: usr_view_model.UsersToModel(users, repo.PrefixAvatarURL),
}
if sequenceErr == nil {
result.Sequence = sequence.CurrentSequence

View File

@ -21,11 +21,12 @@ import (
)
type UserGrantRepo struct {
SearchLimit uint64
View *view.View
IamID string
Auth authz.Config
AuthZRepo *authz_repo.EsRepository
SearchLimit uint64
View *view.View
IamID string
Auth authz.Config
AuthZRepo *authz_repo.EsRepository
PrefixAvatarURL string
}
func (repo *UserGrantRepo) SearchMyUserGrants(ctx context.Context, request *grant_model.UserGrantSearchRequest) (*grant_model.UserGrantSearchResponse, error) {
@ -44,7 +45,7 @@ func (repo *UserGrantRepo) SearchMyUserGrants(ctx context.Context, request *gran
Offset: request.Offset,
Limit: request.Limit,
TotalResult: count,
Result: model.UserGrantsToModel(grants),
Result: model.UserGrantsToModel(grants, repo.PrefixAvatarURL),
}
if err == nil {
result.Sequence = sequence.CurrentSequence
@ -234,7 +235,7 @@ func (repo *UserGrantRepo) UserGrantsByProjectAndUserID(projectID, userID string
if err != nil {
return nil, err
}
return model.UserGrantsToModel(grants), nil
return model.UserGrantsToModel(grants, repo.PrefixAvatarURL), nil
}
func (repo *UserGrantRepo) userOrg(ctxData authz.CtxData) (*grant_model.ProjectOrgSearchResponse, error) {

View File

@ -18,7 +18,7 @@ func (repo *UserSessionRepo) GetMyUserSessions(ctx context.Context) ([]*usr_mode
if err != nil {
return nil, err
}
return model.UserSessionsToModel(userSessions), nil
return model.UserSessionsToModel(userSessions, repo.View.PrefixAvatarURL()), nil
}
func (repo *UserSessionRepo) ActiveUserSessionCount() int64 {

View File

@ -2,10 +2,11 @@ package handler
import (
"context"
"strings"
"github.com/caos/zitadel/internal/eventstore/v1"
iam_model "github.com/caos/zitadel/internal/iam/model"
iam_view "github.com/caos/zitadel/internal/iam/repository/view"
"strings"
es_sdk "github.com/caos/zitadel/internal/eventstore/v1/sdk"
org_view "github.com/caos/zitadel/internal/org/repository/view"
@ -149,7 +150,9 @@ func (u *UserGrant) processUser(event *es_models.Event) (err error) {
usr_es_model.UserEmailChanged,
usr_es_model.HumanProfileChanged,
usr_es_model.HumanEmailChanged,
usr_es_model.MachineChanged:
usr_es_model.MachineChanged,
usr_es_model.HumanAvatarAdded,
usr_es_model.HumanAvatarRemoved:
grants, err := u.view.UserGrantsByUserID(event.AggregateID)
if err != nil {
return err
@ -396,11 +399,13 @@ func (u *UserGrant) fillData(grant *view_model.UserGrantView, resourceOwner stri
func (u *UserGrant) fillUserData(grant *view_model.UserGrantView, user *model.UserView) {
grant.UserName = user.UserName
grant.UserResourceOwner = user.ResourceOwner
if user.HumanView != nil {
grant.FirstName = user.FirstName
grant.LastName = user.LastName
grant.DisplayName = user.FirstName + " " + user.LastName
grant.Email = user.Email
grant.AvatarKey = user.AvatarKey
}
if user.MachineView != nil {
grant.DisplayName = user.MachineView.Name

View File

@ -24,6 +24,7 @@ import (
type Config struct {
SearchLimit uint64
Domain string
APIDomain string
Eventstore v1.Config
AuthRequest cache.Config
View types.SQL
@ -63,7 +64,9 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, co
}
idGenerator := id.SonyFlakeGenerator
view, err := auth_view.StartView(sqlClient, keyAlgorithm, idGenerator)
assetsAPI := conf.APIDomain + "/assets/v1/"
view, err := auth_view.StartView(sqlClient, keyAlgorithm, idGenerator, assetsAPI)
if err != nil {
return nil, err
}
@ -78,10 +81,11 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, co
locker := spooler.NewLocker(sqlClient)
userRepo := eventstore.UserRepo{
SearchLimit: conf.SearchLimit,
Eventstore: es,
View: view,
SystemDefaults: systemDefaults,
SearchLimit: conf.SearchLimit,
Eventstore: es,
View: view,
SystemDefaults: systemDefaults,
PrefixAvatarURL: assetsAPI,
}
return &EsRepository{
spool,

View File

@ -10,23 +10,29 @@ import (
)
type View struct {
Db *gorm.DB
keyAlgorithm crypto.EncryptionAlgorithm
idGenerator id.Generator
Db *gorm.DB
keyAlgorithm crypto.EncryptionAlgorithm
idGenerator id.Generator
prefixAvatarURL string
}
func StartView(sqlClient *sql.DB, keyAlgorithm crypto.EncryptionAlgorithm, idGenerator id.Generator) (*View, error) {
func StartView(sqlClient *sql.DB, keyAlgorithm crypto.EncryptionAlgorithm, idGenerator id.Generator, prefixAvatarURL string) (*View, error) {
gorm, err := gorm.Open("postgres", sqlClient)
if err != nil {
return nil, err
}
return &View{
Db: gorm,
keyAlgorithm: keyAlgorithm,
idGenerator: idGenerator,
Db: gorm,
keyAlgorithm: keyAlgorithm,
idGenerator: idGenerator,
prefixAvatarURL: prefixAvatarURL,
}, nil
}
func (v *View) Health() (err error) {
return v.Db.DB().Ping()
}
func (v *View) PrefixAvatarURL() string {
return v.prefixAvatarURL
}

View File

@ -1,8 +1,9 @@
package domain
import (
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
"golang.org/x/text/language"
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
)
type Profile struct {
@ -21,3 +22,10 @@ type Profile struct {
func (p *Profile) IsValid() bool {
return p.FirstName != "" && p.LastName != ""
}
func AvatarURL(prefix, resourceOwner, key string) string {
if prefix == "" || resourceOwner == "" || key == "" {
return ""
}
return prefix + resourceOwner + "/" + key
}

View File

@ -48,6 +48,7 @@ type UserSelection struct {
UserSessionState UserSessionState
SelectionPossible bool
AvatarKey string
ResourceOwner string
}
type UserSessionState int32

View File

@ -16,6 +16,8 @@ type IAMMemberView struct {
LastName string
DisplayName string
PreferredLoginName string
AvatarURL string
UserResourceOwner string
Roles []string
CreationDate time.Time
ChangeDate time.Time

View File

@ -38,22 +38,6 @@ func LabelPolicyToModel(policy *LabelPolicy) *iam_model.LabelPolicy {
}
}
func LabelPolicyFromModel(policy *iam_model.LabelPolicy) *LabelPolicy {
return &LabelPolicy{
ObjectRoot: policy.ObjectRoot,
State: int32(policy.State),
PrimaryColor: policy.PrimaryColor,
BackgroundColor: policy.BackgroundColor,
WarnColor: policy.WarnColor,
FontColor: policy.FontColor,
PrimaryColorDark: policy.PrimaryColorDark,
BackgroundColorDark: policy.BackgroundColorDark,
WarnColorDark: policy.WarnColorDark,
FontColorDark: policy.FontColorDark,
HideLoginNameSuffix: policy.HideLoginNameSuffix,
}
}
func (i *IAM) appendAddLabelPolicyEvent(event *es_models.Event) error {
i.DefaultLabelPolicy = new(LabelPolicy)
err := i.DefaultLabelPolicy.SetDataLabel(event)
@ -71,7 +55,7 @@ func (i *IAM) appendChangeLabelPolicyEvent(event *es_models.Event) error {
func (p *LabelPolicy) SetDataLabel(event *es_models.Event) error {
err := json.Unmarshal(event.Data, p)
if err != nil {
return errors.ThrowInternal(err, "MODEL-ikjhf", "unable to unmarshal data")
return errors.ThrowInternal(err, "MODEL-Gdgwq", "unable to unmarshal data")
}
return nil
}

View File

@ -4,13 +4,14 @@ import (
"encoding/json"
"time"
es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
"github.com/caos/logging"
"github.com/lib/pq"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/iam/model"
"github.com/lib/pq"
es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
)
const (
@ -33,28 +34,14 @@ type IAMMemberView struct {
Roles pq.StringArray `json:"roles" gorm:"column:roles"`
Sequence uint64 `json:"-" gorm:"column:sequence"`
PreferredLoginName string `json:"-" gorm:"column:preferred_login_name"`
AvatarKey string `json:"-" gorm:"column:avatar_key"`
UserResourceOwner string `json:"-" gorm:"column:user_resource_owner"`
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
}
func IAMMemberViewFromModel(member *model.IAMMemberView) *IAMMemberView {
return &IAMMemberView{
UserID: member.UserID,
IAMID: member.IAMID,
UserName: member.UserName,
Email: member.Email,
FirstName: member.FirstName,
LastName: member.LastName,
DisplayName: member.DisplayName,
Roles: member.Roles,
Sequence: member.Sequence,
CreationDate: member.CreationDate,
ChangeDate: member.ChangeDate,
}
}
func IAMMemberToModel(member *IAMMemberView) *model.IAMMemberView {
func IAMMemberToModel(member *IAMMemberView, prefixAvatarURL string) *model.IAMMemberView {
return &model.IAMMemberView{
UserID: member.UserID,
IAMID: member.IAMID,
@ -64,6 +51,8 @@ func IAMMemberToModel(member *IAMMemberView) *model.IAMMemberView {
LastName: member.LastName,
DisplayName: member.DisplayName,
PreferredLoginName: member.PreferredLoginName,
AvatarURL: domain.AvatarURL(prefixAvatarURL, member.UserResourceOwner, member.AvatarKey),
UserResourceOwner: member.UserResourceOwner,
Roles: member.Roles,
Sequence: member.Sequence,
CreationDate: member.CreationDate,
@ -71,10 +60,10 @@ func IAMMemberToModel(member *IAMMemberView) *model.IAMMemberView {
}
}
func IAMMembersToModel(roles []*IAMMemberView) []*model.IAMMemberView {
func IAMMembersToModel(roles []*IAMMemberView, prefixAvatarURL string) []*model.IAMMemberView {
result := make([]*model.IAMMemberView, len(roles))
for i, r := range roles {
result[i] = IAMMemberToModel(r)
result[i] = IAMMemberToModel(r, prefixAvatarURL)
}
return result
}

View File

@ -31,11 +31,12 @@ import (
)
type OrgRepository struct {
SearchLimit uint64
Eventstore v1.Eventstore
View *mgmt_view.View
Roles []string
SystemDefaults systemdefaults.SystemDefaults
SearchLimit uint64
Eventstore v1.Eventstore
View *mgmt_view.View
Roles []string
SystemDefaults systemdefaults.SystemDefaults
PrefixAvatarURL string
}
func (repo *OrgRepository) OrgByID(ctx context.Context, id string) (*org_model.OrgView, error) {
@ -121,7 +122,7 @@ func (repo *OrgRepository) OrgMemberByID(ctx context.Context, orgID, userID stri
if err != nil {
return nil, err
}
return model.OrgMemberToModel(member), nil
return model.OrgMemberToModel(member, repo.PrefixAvatarURL), nil
}
func (repo *OrgRepository) SearchMyOrgMembers(ctx context.Context, request *org_model.OrgMemberSearchRequest) (*org_model.OrgMemberSearchResponse, error) {
@ -140,7 +141,7 @@ func (repo *OrgRepository) SearchMyOrgMembers(ctx context.Context, request *org_
Offset: request.Offset,
Limit: request.Limit,
TotalResult: count,
Result: model.OrgMembersToModel(members),
Result: model.OrgMembersToModel(members, repo.PrefixAvatarURL),
}
if sequenceErr == nil {
result.Sequence = sequence.CurrentSequence
@ -653,18 +654,18 @@ func (repo *OrgRepository) userByID(ctx context.Context, id string) (*usr_model.
}
if esErr != nil {
logging.Log("EVENT-PSoc3").WithError(esErr).Debug("error retrieving new events")
return usr_es_model.UserToModel(user), nil
return usr_es_model.UserToModel(user, repo.PrefixAvatarURL), nil
}
userCopy := *user
for _, event := range events {
if err := userCopy.AppendEvent(event); err != nil {
return usr_es_model.UserToModel(user), nil
return usr_es_model.UserToModel(user, repo.PrefixAvatarURL), nil
}
}
if userCopy.State == int32(usr_es_model.UserStateDeleted) {
return nil, errors.ThrowNotFound(nil, "EVENT-3n8Fs", "Errors.User.NotFound")
}
return usr_es_model.UserToModel(&userCopy), nil
return usr_es_model.UserToModel(&userCopy, repo.PrefixAvatarURL), nil
}
func (r *OrgRepository) getUserEvents(ctx context.Context, userID string, sequence uint64) ([]*models.Event, error) {

View File

@ -31,10 +31,11 @@ import (
type ProjectRepo struct {
v1.Eventstore
SearchLimit uint64
View *view.View
Roles []string
IAMID string
SearchLimit uint64
View *view.View
Roles []string
IAMID string
PrefixAvatarURL string
}
func (repo *ProjectRepo) ProjectByID(ctx context.Context, id string) (*proj_model.ProjectView, error) {
@ -136,7 +137,7 @@ func (repo *ProjectRepo) ProjectMemberByID(ctx context.Context, projectID, userI
if err != nil {
return nil, err
}
return model.ProjectMemberToModel(member), nil
return model.ProjectMemberToModel(member, repo.PrefixAvatarURL), nil
}
func (repo *ProjectRepo) SearchProjectMembers(ctx context.Context, request *proj_model.ProjectMemberSearchRequest) (*proj_model.ProjectMemberSearchResponse, error) {
@ -154,7 +155,7 @@ func (repo *ProjectRepo) SearchProjectMembers(ctx context.Context, request *proj
Offset: request.Offset,
Limit: request.Limit,
TotalResult: uint64(count),
Result: model.ProjectMembersToModel(members),
Result: model.ProjectMembersToModel(members, repo.PrefixAvatarURL),
}
if sequenceErr == nil {
result.Sequence = sequence.CurrentSequence
@ -442,7 +443,7 @@ func (repo *ProjectRepo) ProjectGrantMemberByID(ctx context.Context, projectID,
if err != nil {
return nil, err
}
return model.ProjectGrantMemberToModel(member), nil
return model.ProjectGrantMemberToModel(member, repo.PrefixAvatarURL), nil
}
func (repo *ProjectRepo) SearchProjectGrantRoles(ctx context.Context, projectID, grantID string, request *proj_model.ProjectRoleSearchRequest) (*proj_model.ProjectRoleSearchResponse, error) {
@ -491,7 +492,7 @@ func (repo *ProjectRepo) SearchProjectGrantMembers(ctx context.Context, request
Offset: request.Offset,
Limit: request.Limit,
TotalResult: uint64(count),
Result: model.ProjectGrantMembersToModel(members),
Result: model.ProjectGrantMembersToModel(members, repo.PrefixAvatarURL),
}
if sequenceErr == nil {
result.Sequence = sequence.CurrentSequence
@ -542,18 +543,18 @@ func (repo *ProjectRepo) userByID(ctx context.Context, id string) (*usr_model.Us
}
if esErr != nil {
logging.Log("EVENT-PSoc3").WithError(esErr).Debug("error retrieving new events")
return usr_es_model.UserToModel(user), nil
return usr_es_model.UserToModel(user, repo.PrefixAvatarURL), nil
}
userCopy := *user
for _, event := range events {
if err := userCopy.AppendEvent(event); err != nil {
return usr_es_model.UserToModel(user), nil
return usr_es_model.UserToModel(user, repo.PrefixAvatarURL), nil
}
}
if userCopy.State == int32(usr_model.UserStateDeleted) {
return nil, caos_errs.ThrowNotFound(nil, "EVENT-2m0Fs", "Errors.User.NotFound")
}
return usr_es_model.UserToModel(&userCopy), nil
return usr_es_model.UserToModel(&userCopy, repo.PrefixAvatarURL), nil
}
func (r *ProjectRepo) getUserEvents(ctx context.Context, userID string, sequence uint64) ([]*models.Event, error) {

View File

@ -27,9 +27,10 @@ import (
type UserRepo struct {
v1.Eventstore
SearchLimit uint64
View *view.View
SystemDefaults systemdefaults.SystemDefaults
SearchLimit uint64
View *view.View
SystemDefaults systemdefaults.SystemDefaults
PrefixAvatarURL string
}
func (repo *UserRepo) UserByID(ctx context.Context, id string) (*usr_model.UserView, error) {
@ -46,18 +47,18 @@ func (repo *UserRepo) UserByID(ctx context.Context, id string) (*usr_model.UserV
}
if esErr != nil {
logging.Log("EVENT-PSoc3").WithError(esErr).Debug("error retrieving new events")
return model.UserToModel(user), nil
return model.UserToModel(user, repo.PrefixAvatarURL), nil
}
userCopy := *user
for _, event := range events {
if err := userCopy.AppendEvent(event); err != nil {
return model.UserToModel(user), nil
return model.UserToModel(user, repo.PrefixAvatarURL), nil
}
}
if userCopy.State == int32(usr_model.UserStateDeleted) {
return nil, caos_errs.ThrowNotFound(nil, "EVENT-4Fm9s", "Errors.User.NotFound")
}
return model.UserToModel(&userCopy), nil
return model.UserToModel(&userCopy, repo.PrefixAvatarURL), nil
}
func (repo *UserRepo) SearchUsers(ctx context.Context, request *usr_model.UserSearchRequest, ensureLimit bool) (*usr_model.UserSearchResponse, error) {
@ -78,7 +79,7 @@ func (repo *UserRepo) SearchUsers(ctx context.Context, request *usr_model.UserSe
Offset: request.Offset,
Limit: request.Limit,
TotalResult: count,
Result: model.UsersToModel(users),
Result: model.UsersToModel(users, repo.PrefixAvatarURL),
}
if sequenceErr == nil {
result.Sequence = sequence.CurrentSequence
@ -118,7 +119,7 @@ func (repo *UserRepo) GetUserByLoginNameGlobal(ctx context.Context, loginName st
if err != nil {
return nil, err
}
return model.UserToModel(user), nil
return model.UserToModel(user, repo.PrefixAvatarURL), nil
}
func (repo *UserRepo) IsUserUnique(ctx context.Context, userName, email string) (bool, error) {

View File

@ -13,8 +13,9 @@ import (
)
type UserGrantRepo struct {
SearchLimit uint64
View *view.View
SearchLimit uint64
View *view.View
PrefixAvatarURL string
}
func (repo *UserGrantRepo) UserGrantByID(ctx context.Context, grantID string) (*grant_model.UserGrantView, error) {
@ -22,7 +23,7 @@ func (repo *UserGrantRepo) UserGrantByID(ctx context.Context, grantID string) (*
if err != nil {
return nil, err
}
return model.UserGrantToModel(grant), nil
return model.UserGrantToModel(grant, repo.PrefixAvatarURL), nil
}
func (repo *UserGrantRepo) UserGrantsByProjectID(ctx context.Context, projectID string) ([]*grant_model.UserGrantView, error) {
@ -30,7 +31,7 @@ func (repo *UserGrantRepo) UserGrantsByProjectID(ctx context.Context, projectID
if err != nil {
return nil, err
}
return model.UserGrantsToModel(grants), nil
return model.UserGrantsToModel(grants, repo.PrefixAvatarURL), nil
}
func (repo *UserGrantRepo) UserGrantsByProjectIDAndRoleKey(ctx context.Context, projectID, roleKey string) ([]*grant_model.UserGrantView, error) {
@ -38,7 +39,7 @@ func (repo *UserGrantRepo) UserGrantsByProjectIDAndRoleKey(ctx context.Context,
if err != nil {
return nil, err
}
return model.UserGrantsToModel(grants), nil
return model.UserGrantsToModel(grants, repo.PrefixAvatarURL), nil
}
func (repo *UserGrantRepo) UserGrantsByProjectAndGrantID(ctx context.Context, projectID, grantID string) ([]*grant_model.UserGrantView, error) {
@ -46,7 +47,7 @@ func (repo *UserGrantRepo) UserGrantsByProjectAndGrantID(ctx context.Context, pr
if err != nil {
return nil, err
}
return model.UserGrantsToModel(grants), nil
return model.UserGrantsToModel(grants, repo.PrefixAvatarURL), nil
}
func (repo *UserGrantRepo) UserGrantsByUserID(ctx context.Context, userID string) ([]*grant_model.UserGrantView, error) {
@ -54,7 +55,7 @@ func (repo *UserGrantRepo) UserGrantsByUserID(ctx context.Context, userID string
if err != nil {
return nil, err
}
return model.UserGrantsToModel(grants), nil
return model.UserGrantsToModel(grants, repo.PrefixAvatarURL), nil
}
func (repo *UserGrantRepo) SearchUserGrants(ctx context.Context, request *grant_model.UserGrantSearchRequest) (*grant_model.UserGrantSearchResponse, error) {
@ -79,7 +80,7 @@ func (repo *UserGrantRepo) SearchUserGrants(ctx context.Context, request *grant_
Offset: request.Offset,
Limit: request.Limit,
TotalResult: count,
Result: model.UserGrantsToModel(grants),
Result: model.UserGrantsToModel(grants, repo.PrefixAvatarURL),
}
if sequenceErr == nil {
result.Sequence = sequence.CurrentSequence

View File

@ -131,7 +131,9 @@ func (m *OrgMember) processUser(event *es_models.Event) (err error) {
usr_es_model.UserEmailChanged,
usr_es_model.HumanProfileChanged,
usr_es_model.HumanEmailChanged,
usr_es_model.MachineChanged:
usr_es_model.MachineChanged,
usr_es_model.HumanAvatarAdded,
usr_es_model.HumanAvatarRemoved:
members, err := m.view.OrgMembersByUserID(event.AggregateID)
if err != nil {
return err
@ -164,6 +166,9 @@ func (m *OrgMember) fillData(member *org_view_model.OrgMemberView) (err error) {
func (m *OrgMember) fillUserData(member *org_view_model.OrgMemberView, user *usr_view_model.UserView) error {
org, err := m.getOrgByID(context.Background(), user.ResourceOwner)
if err != nil {
return err
}
policy := org.OrgIamPolicy
if policy == nil {
policy, err = m.getDefaultOrgIAMPolicy(context.TODO())
@ -173,11 +178,13 @@ func (m *OrgMember) fillUserData(member *org_view_model.OrgMemberView, user *usr
}
member.UserName = user.UserName
member.PreferredLoginName = user.GenerateLoginName(org.GetPrimaryDomain().Domain, policy.UserLoginMustBeDomain)
member.UserResourceOwner = user.ResourceOwner
if user.HumanView != nil {
member.FirstName = user.FirstName
member.LastName = user.LastName
member.DisplayName = user.DisplayName
member.Email = user.Email
member.AvatarKey = user.AvatarKey
}
if user.MachineView != nil {
member.DisplayName = user.MachineView.Name

View File

@ -2,6 +2,7 @@ package handler
import (
"context"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1"
@ -139,7 +140,9 @@ func (p *ProjectGrantMember) processUser(event *es_models.Event) (err error) {
usr_es_model.UserEmailChanged,
usr_es_model.HumanProfileChanged,
usr_es_model.HumanEmailChanged,
usr_es_model.MachineChanged:
usr_es_model.MachineChanged,
usr_es_model.HumanAvatarAdded,
usr_es_model.HumanAvatarRemoved:
members, err := p.view.ProjectGrantMembersByUserID(event.AggregateID)
if err != nil {
return err
@ -183,11 +186,13 @@ func (p *ProjectGrantMember) fillUserData(member *view_model.ProjectGrantMemberV
}
member.UserName = user.UserName
member.PreferredLoginName = user.GenerateLoginName(org.GetPrimaryDomain().Domain, policy.UserLoginMustBeDomain)
member.UserResourceOwner = user.ResourceOwner
if user.HumanView != nil {
member.FirstName = user.FirstName
member.LastName = user.LastName
member.DisplayName = user.DisplayName
member.Email = user.Email
member.AvatarKey = user.AvatarKey
}
if user.MachineView != nil {
member.DisplayName = user.MachineView.Name

View File

@ -2,6 +2,7 @@ package handler
import (
"context"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1"
@ -134,7 +135,9 @@ func (p *ProjectMember) processUser(event *es_models.Event) (err error) {
usr_es_model.UserEmailChanged,
usr_es_model.HumanProfileChanged,
usr_es_model.HumanEmailChanged,
usr_es_model.MachineChanged:
usr_es_model.MachineChanged,
usr_es_model.HumanAvatarAdded,
usr_es_model.HumanAvatarRemoved:
members, err := p.view.ProjectMembersByUserID(event.AggregateID)
if err != nil {
return err
@ -168,6 +171,9 @@ func (p *ProjectMember) fillData(member *view_model.ProjectMemberView) (err erro
func (p *ProjectMember) fillUserData(member *view_model.ProjectMemberView, user *usr_view_model.UserView) error {
org, err := p.getOrgByID(context.Background(), user.ResourceOwner)
if err != nil {
return err
}
policy := org.OrgIamPolicy
if policy == nil {
policy, err = p.getDefaultOrgIAMPolicy(context.TODO())
@ -177,11 +183,13 @@ func (p *ProjectMember) fillUserData(member *view_model.ProjectMemberView, user
}
member.UserName = user.UserName
member.PreferredLoginName = user.GenerateLoginName(org.GetPrimaryDomain().Domain, policy.UserLoginMustBeDomain)
member.UserResourceOwner = user.ResourceOwner
if user.HumanView != nil {
member.FirstName = user.FirstName
member.LastName = user.LastName
member.Email = user.Email
member.DisplayName = user.DisplayName
member.AvatarKey = user.AvatarKey
}
if user.MachineView != nil {
member.DisplayName = user.MachineView.Name

View File

@ -129,7 +129,9 @@ func (u *UserGrant) processUser(event *es_models.Event) (err error) {
usr_es_model.UserEmailChanged,
usr_es_model.HumanProfileChanged,
usr_es_model.HumanEmailChanged,
usr_es_model.MachineChanged:
usr_es_model.MachineChanged,
usr_es_model.HumanAvatarAdded,
usr_es_model.HumanAvatarRemoved:
grants, err := u.view.UserGrantsByUserID(event.AggregateID)
if err != nil {
return err
@ -218,11 +220,13 @@ func (u *UserGrant) fillData(grant *view_model.UserGrantView, resourceOwner stri
func (u *UserGrant) fillUserData(grant *view_model.UserGrantView, user *usr_view_model.UserView) {
grant.UserName = user.UserName
grant.UserResourceOwner = user.ResourceOwner
if user.HumanView != nil {
grant.FirstName = user.FirstName
grant.LastName = user.LastName
grant.DisplayName = user.FirstName + " " + user.LastName
grant.Email = user.Email
grant.AvatarKey = user.AvatarKey
}
if user.MachineView != nil {
grant.DisplayName = user.MachineView.Name

View File

@ -16,6 +16,7 @@ import (
type Config struct {
SearchLimit uint64
Domain string
APIDomain string
Eventstore v1.Config
View types.SQL
Spooler spooler.SpoolerConfig
@ -49,13 +50,14 @@ func Start(conf Config, systemDefaults sd.SystemDefaults, roles []string, querie
}
spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, systemDefaults, staticStorage)
assetsAPI := conf.APIDomain + "/assets/v1/"
return &EsRepository{
spooler: spool,
OrgRepository: eventstore.OrgRepository{conf.SearchLimit, es, view, roles, systemDefaults},
ProjectRepo: eventstore.ProjectRepo{es, conf.SearchLimit, view, roles, systemDefaults.IamID},
UserRepo: eventstore.UserRepo{es, conf.SearchLimit, view, systemDefaults},
UserGrantRepo: eventstore.UserGrantRepo{conf.SearchLimit, view},
OrgRepository: eventstore.OrgRepository{conf.SearchLimit, es, view, roles, systemDefaults, assetsAPI},
ProjectRepo: eventstore.ProjectRepo{es, conf.SearchLimit, view, roles, systemDefaults.IamID, assetsAPI},
UserRepo: eventstore.UserRepo{es, conf.SearchLimit, view, systemDefaults, assetsAPI},
UserGrantRepo: eventstore.UserGrantRepo{conf.SearchLimit, view, assetsAPI},
IAMRepository: eventstore.IAMRepository{IAMV2Query: queries},
FeaturesRepo: eventstore.FeaturesRepo{es, view, conf.SearchLimit, systemDefaults},
view: view,

View File

@ -16,6 +16,8 @@ type OrgMemberView struct {
LastName string
DisplayName string
PreferredLoginName string
AvatarURL string
UserResourceOwner string
Roles []string
CreationDate time.Time
ChangeDate time.Time

View File

@ -4,13 +4,14 @@ import (
"encoding/json"
"time"
es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
"github.com/caos/logging"
"github.com/lib/pq"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/org/model"
"github.com/lib/pq"
es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
)
const (
@ -33,12 +34,14 @@ type OrgMemberView struct {
Roles pq.StringArray `json:"roles" gorm:"column:roles"`
Sequence uint64 `json:"-" gorm:"column:sequence"`
PreferredLoginName string `json:"-" gorm:"column:preferred_login_name"`
AvatarKey string `json:"-" gorm:"column:avatar_key"`
UserResourceOwner string `json:"-" gorm:"column:user_resource_owner"`
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
}
func OrgMemberToModel(member *OrgMemberView) *model.OrgMemberView {
func OrgMemberToModel(member *OrgMemberView, prefixAvatarURL string) *model.OrgMemberView {
return &model.OrgMemberView{
UserID: member.UserID,
OrgID: member.OrgID,
@ -49,16 +52,18 @@ func OrgMemberToModel(member *OrgMemberView) *model.OrgMemberView {
DisplayName: member.DisplayName,
PreferredLoginName: member.PreferredLoginName,
Roles: member.Roles,
AvatarURL: domain.AvatarURL(prefixAvatarURL, member.UserResourceOwner, member.AvatarKey),
UserResourceOwner: member.UserResourceOwner,
Sequence: member.Sequence,
CreationDate: member.CreationDate,
ChangeDate: member.ChangeDate,
}
}
func OrgMembersToModel(roles []*OrgMemberView) []*model.OrgMemberView {
func OrgMembersToModel(roles []*OrgMemberView, prefixAvatarURL string) []*model.OrgMemberView {
result := make([]*model.OrgMemberView, len(roles))
for i, r := range roles {
result[i] = OrgMemberToModel(r)
result[i] = OrgMemberToModel(r, prefixAvatarURL)
}
return result
}

View File

@ -17,6 +17,8 @@ type ProjectGrantMemberView struct {
LastName string
DisplayName string
PreferredLoginName string
AvatarURL string
UserResourceOwner string
Roles []string
CreationDate time.Time
ChangeDate time.Time

View File

@ -16,6 +16,8 @@ type ProjectMemberView struct {
LastName string
DisplayName string
PreferredLoginName string
AvatarURL string
UserResourceOwner string
Roles []string
CreationDate time.Time
ChangeDate time.Time

View File

@ -7,6 +7,7 @@ import (
"github.com/caos/logging"
"github.com/lib/pq"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/project/model"
@ -35,12 +36,14 @@ type ProjectGrantMemberView struct {
Roles pq.StringArray `json:"roles" gorm:"column:roles"`
Sequence uint64 `json:"-" gorm:"column:sequence"`
PreferredLoginName string `json:"-" gorm:"column:preferred_login_name"`
AvatarKey string `json:"-" gorm:"column:avatar_key"`
UserResourceOwner string `json:"-" gorm:"column:user_resource_owner"`
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
}
func ProjectGrantMemberToModel(member *ProjectGrantMemberView) *model.ProjectGrantMemberView {
func ProjectGrantMemberToModel(member *ProjectGrantMemberView, prefixAvatarURL string) *model.ProjectGrantMemberView {
return &model.ProjectGrantMemberView{
UserID: member.UserID,
GrantID: member.GrantID,
@ -51,6 +54,8 @@ func ProjectGrantMemberToModel(member *ProjectGrantMemberView) *model.ProjectGra
LastName: member.LastName,
DisplayName: member.DisplayName,
PreferredLoginName: member.PreferredLoginName,
AvatarURL: domain.AvatarURL(prefixAvatarURL, member.UserResourceOwner, member.AvatarKey),
UserResourceOwner: member.UserResourceOwner,
Roles: member.Roles,
Sequence: member.Sequence,
CreationDate: member.CreationDate,
@ -58,10 +63,10 @@ func ProjectGrantMemberToModel(member *ProjectGrantMemberView) *model.ProjectGra
}
}
func ProjectGrantMembersToModel(roles []*ProjectGrantMemberView) []*model.ProjectGrantMemberView {
func ProjectGrantMembersToModel(roles []*ProjectGrantMemberView, prefixAvatarURL string) []*model.ProjectGrantMemberView {
result := make([]*model.ProjectGrantMemberView, len(roles))
for i, r := range roles {
result[i] = ProjectGrantMemberToModel(r)
result[i] = ProjectGrantMemberToModel(r, prefixAvatarURL)
}
return result
}

View File

@ -5,11 +5,13 @@ import (
"time"
"github.com/caos/logging"
"github.com/lib/pq"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/project/model"
es_model "github.com/caos/zitadel/internal/project/repository/eventsourcing/model"
"github.com/lib/pq"
)
const (
@ -32,31 +34,36 @@ type ProjectMemberView struct {
Roles pq.StringArray `json:"roles" gorm:"column:roles"`
Sequence uint64 `json:"-" gorm:"column:sequence"`
PreferredLoginName string `json:"-" gorm:"column:preferred_login_name"`
AvatarKey string `json:"-" gorm:"column:avatar_key"`
UserResourceOwner string `json:"-" gorm:"column:user_resource_owner"`
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
}
func ProjectMemberToModel(member *ProjectMemberView) *model.ProjectMemberView {
func ProjectMemberToModel(member *ProjectMemberView, prefixAvatarURL string) *model.ProjectMemberView {
return &model.ProjectMemberView{
UserID: member.UserID,
ProjectID: member.ProjectID,
UserName: member.UserName,
Email: member.Email,
FirstName: member.FirstName,
LastName: member.LastName,
DisplayName: member.DisplayName,
Roles: member.Roles,
Sequence: member.Sequence,
CreationDate: member.CreationDate,
ChangeDate: member.ChangeDate,
UserID: member.UserID,
ProjectID: member.ProjectID,
UserName: member.UserName,
Email: member.Email,
FirstName: member.FirstName,
LastName: member.LastName,
DisplayName: member.DisplayName,
PreferredLoginName: member.PreferredLoginName,
AvatarURL: domain.AvatarURL(prefixAvatarURL, member.UserResourceOwner, member.AvatarKey),
UserResourceOwner: member.UserResourceOwner,
Roles: member.Roles,
Sequence: member.Sequence,
CreationDate: member.CreationDate,
ChangeDate: member.ChangeDate,
}
}
func ProjectMembersToModel(roles []*ProjectMemberView) []*model.ProjectMemberView {
func ProjectMembersToModel(roles []*ProjectMemberView, prefixAvatarURL string) []*model.ProjectMemberView {
result := make([]*model.ProjectMemberView, len(roles))
for i, r := range roles {
result[i] = ProjectMemberToModel(r)
result[i] = ProjectMemberToModel(r, prefixAvatarURL)
}
return result
}

View File

@ -24,7 +24,6 @@
<div class="lgn-account-selection">
{{ if .Users }}
{{ $displayLoginNameSuffix := and .OrgID (not .DisplayLoginNameSuffix)}}
{{ $orgID := .OrgID }}
{{ range $user := .Users }}
{{ $sessionState := (printf "UserSelection.SessionState%v" $user.UserSessionState) }}
<button type="submit" name="userID" value="{{$user.UserID}}" class="lgn-account"
@ -32,7 +31,7 @@
<div class="left">
<div class="lgn-avatar" {{if not $user.AvatarKey}}loginname="{{$user.LoginName}}"{{end}}>
{{if $user.AvatarKey}}
<img class="avatar-img" src="{{ avatarResource $orgID $user.AvatarKey }}" alt="user-avatar">
<img class="avatar-img" src="{{ avatarResource $user.ResourceOwner $user.AvatarKey }}" alt="user-avatar">
{{else}}
<span class="initials">A</span>
{{end}}

View File

@ -17,7 +17,7 @@ type Profile struct {
Gender Gender
PreferredLoginName string
LoginNames []string
Avatar string
AvatarURL string
}
func (p *Profile) IsValid() bool {

View File

@ -20,6 +20,7 @@ type UserSessionView struct {
LoginName string
DisplayName string
AvatarKey string
AvatarURL string
SelectedIDPConfigID string
PasswordVerification time.Time
PasswordlessVerification time.Time

View File

@ -41,6 +41,7 @@ type HumanView struct {
NickName string
DisplayName string
AvatarKey string
AvatarURL string
PreSignedAvatar *url.URL
PreferredLanguage string
Gender Gender
@ -251,6 +252,7 @@ func (u *UserView) GetProfile() (*Profile, error) {
Gender: u.Gender,
PreferredLoginName: u.PreferredLoginName,
LoginNames: u.LoginNames,
AvatarURL: u.AvatarURL,
}, nil
}

View File

@ -9,6 +9,7 @@ import (
"github.com/lib/pq"
req_model "github.com/caos/zitadel/internal/auth_request/model"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1/models"
iam_model "github.com/caos/zitadel/internal/iam/model"
@ -135,7 +136,7 @@ func (m *MachineView) IsZero() bool {
return m == nil || m.Name == ""
}
func UserToModel(user *UserView) *model.UserView {
func UserToModel(user *UserView, prefixAvatarURL string) *model.UserView {
userView := &model.UserView{
ID: user.ID,
UserName: user.UserName,
@ -160,6 +161,7 @@ func UserToModel(user *UserView) *model.UserView {
NickName: user.NickName,
DisplayName: user.DisplayName,
AvatarKey: user.AvatarKey,
AvatarURL: domain.AvatarURL(prefixAvatarURL, user.ResourceOwner, user.AvatarKey),
PreferredLanguage: user.PreferredLanguage,
Gender: model.Gender(user.Gender),
Email: user.Email,
@ -187,10 +189,10 @@ func UserToModel(user *UserView) *model.UserView {
return userView
}
func UsersToModel(users []*UserView) []*model.UserView {
func UsersToModel(users []*UserView, prefixAvatarURL string) []*model.UserView {
result := make([]*model.UserView, len(users))
for i, p := range users {
result[i] = UserToModel(p)
result[i] = UserToModel(p, prefixAvatarURL)
}
return result
}
@ -340,7 +342,7 @@ func (u *UserView) AppendEvent(event *models.Event) (err error) {
es_model.InitializedHumanCheckSucceeded:
u.InitRequired = false
case es_model.HumanAvatarAdded:
u.setData(event)
err = u.setData(event)
case es_model.HumanAvatarRemoved:
u.AvatarKey = ""
}

View File

@ -7,6 +7,7 @@ import (
"github.com/caos/logging"
req_model "github.com/caos/zitadel/internal/auth_request/model"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/user/model"
@ -51,7 +52,7 @@ func UserSessionFromEvent(event *models.Event) (*UserSessionView, error) {
return v, nil
}
func UserSessionToModel(userSession *UserSessionView) *model.UserSessionView {
func UserSessionToModel(userSession *UserSessionView, prefixAvatarURL string) *model.UserSessionView {
return &model.UserSessionView{
ChangeDate: userSession.ChangeDate,
CreationDate: userSession.CreationDate,
@ -63,6 +64,7 @@ func UserSessionToModel(userSession *UserSessionView) *model.UserSessionView {
LoginName: userSession.LoginName,
DisplayName: userSession.DisplayName,
AvatarKey: userSession.AvatarKey,
AvatarURL: domain.AvatarURL(prefixAvatarURL, userSession.ResourceOwner, userSession.AvatarKey),
SelectedIDPConfigID: userSession.SelectedIDPConfigID,
PasswordVerification: userSession.PasswordVerification,
PasswordlessVerification: userSession.PasswordlessVerification,
@ -75,10 +77,10 @@ func UserSessionToModel(userSession *UserSessionView) *model.UserSessionView {
}
}
func UserSessionsToModel(userSessions []*UserSessionView) []*model.UserSessionView {
func UserSessionsToModel(userSessions []*UserSessionView, prefixAvatarURL string) []*model.UserSessionView {
result := make([]*model.UserSessionView, len(userSessions))
for i, s := range userSessions {
result[i] = UserSessionToModel(s)
result[i] = UserSessionToModel(s, prefixAvatarURL)
}
return result
}

View File

@ -21,6 +21,7 @@ type UserGrantView struct {
ProjectName string
OrgName string
OrgPrimaryDomain string
AvatarURL string
RoleKeys []string
CreationDate time.Time

View File

@ -7,6 +7,7 @@ import (
"github.com/caos/logging"
"github.com/lib/pq"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/usergrant/model"
@ -32,21 +33,23 @@ const (
)
type UserGrantView struct {
ID string `json:"-" gorm:"column:id;primary_key"`
ResourceOwner string `json:"-" gorm:"resource_owner"`
UserID string `json:"userId" gorm:"user_id"`
ProjectID string `json:"projectId" gorm:"column:project_id"`
GrantID string `json:"grantId" gorm:"column:grant_id"`
UserName string `json:"-" gorm:"column:user_name"`
FirstName string `json:"-" gorm:"column:first_name"`
LastName string `json:"-" gorm:"column:last_name"`
DisplayName string `json:"-" gorm:"column:display_name"`
Email string `json:"-" gorm:"column:email"`
ProjectName string `json:"-" gorm:"column:project_name"`
ProjectOwner string `json:"-" gorm:"column:project_owner"`
OrgName string `json:"-" gorm:"column:org_name"`
OrgPrimaryDomain string `json:"-" gorm:"column:org_primary_domain"`
RoleKeys pq.StringArray `json:"roleKeys" gorm:"column:role_keys"`
ID string `json:"-" gorm:"column:id;primary_key"`
ResourceOwner string `json:"-" gorm:"resource_owner"`
UserID string `json:"userId" gorm:"user_id"`
ProjectID string `json:"projectId" gorm:"column:project_id"`
GrantID string `json:"grantId" gorm:"column:grant_id"`
UserName string `json:"-" gorm:"column:user_name"`
FirstName string `json:"-" gorm:"column:first_name"`
LastName string `json:"-" gorm:"column:last_name"`
DisplayName string `json:"-" gorm:"column:display_name"`
Email string `json:"-" gorm:"column:email"`
ProjectName string `json:"-" gorm:"column:project_name"`
ProjectOwner string `json:"-" gorm:"column:project_owner"`
OrgName string `json:"-" gorm:"column:org_name"`
OrgPrimaryDomain string `json:"-" gorm:"column:org_primary_domain"`
RoleKeys pq.StringArray `json:"roleKeys" gorm:"column:role_keys"`
AvatarKey string `json:"-" gorm:"column:avatar_key"`
UserResourceOwner string `json:"-" gorm:"column:user_resource_owner"`
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
@ -55,7 +58,7 @@ type UserGrantView struct {
Sequence uint64 `json:"-" gorm:"column:sequence"`
}
func UserGrantToModel(grant *UserGrantView) *model.UserGrantView {
func UserGrantToModel(grant *UserGrantView, prefixAvatarURL string) *model.UserGrantView {
return &model.UserGrantView{
ID: grant.ID,
ResourceOwner: grant.ResourceOwner,
@ -73,15 +76,16 @@ func UserGrantToModel(grant *UserGrantView) *model.UserGrantView {
OrgName: grant.OrgName,
OrgPrimaryDomain: grant.OrgPrimaryDomain,
RoleKeys: grant.RoleKeys,
AvatarURL: domain.AvatarURL(prefixAvatarURL, grant.ResourceOwner, grant.AvatarKey),
Sequence: grant.Sequence,
GrantID: grant.GrantID,
}
}
func UserGrantsToModel(grants []*UserGrantView) []*model.UserGrantView {
func UserGrantsToModel(grants []*UserGrantView, prefixAvatarURL string) []*model.UserGrantView {
result := make([]*model.UserGrantView, len(grants))
for i, g := range grants {
result[i] = UserGrantToModel(g)
result[i] = UserGrantToModel(g, prefixAvatarURL)
}
return result
}

View File

@ -0,0 +1,16 @@
ALTER TABLE adminapi.iam_members ADD COLUMN avatar_key TEXT;
ALTER TABLE auth.user_grants ADD COLUMN avatar_key TEXT;
ALTER TABLE authz.user_grants ADD COLUMN avatar_key TEXT;
ALTER TABLE management.org_members ADD COLUMN avatar_key TEXT;
ALTER TABLE management.project_members ADD COLUMN avatar_key TEXT;
ALTER TABLE management.project_grant_members ADD COLUMN avatar_key TEXT;
ALTER TABLE management.user_grants ADD COLUMN avatar_key TEXT;
ALTER TABLE adminapi.iam_members ADD COLUMN user_resource_owner TEXT;
ALTER TABLE management.org_members ADD COLUMN user_resource_owner TEXT;
ALTER TABLE management.project_members ADD COLUMN user_resource_owner TEXT;
ALTER TABLE management.project_grant_members ADD COLUMN user_resource_owner TEXT;
ALTER TABLE auth.user_grants ADD COLUMN user_resource_owner TEXT;
ALTER TABLE authz.user_grants ADD COLUMN user_resource_owner TEXT;
ALTER TABLE management.user_grants ADD COLUMN user_resource_owner TEXT;

View File

@ -51,6 +51,12 @@ message Member {
example: "\"Gigi Giraffe\"";
}
];
string avatar_url = 9 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "avatar url of the user"
example: "\"https://api.zitadel.ch/assets/v1/avatar-32432jkh4kj32\"";
}
];
}
message SearchQuery {

View File

@ -113,6 +113,12 @@ message Profile {
description: "the gender of the human";
}
];
string avatar_url = 7 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "avatar url of the user"
example: "\"https://api.zitadel.ch/assets/v1/avatar-32432jkh4kj32\"";
}
];
}
message Email {
@ -509,6 +515,12 @@ message Session {
}
];
zitadel.v1.ObjectDetails details = 9;
string avatar_url = 10 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "avatar url of the user"
example: "\"https://api.zitadel.ch/assets/v1/avatar-32432jkh4kj32\"";
}
];
}
enum SessionState {
@ -641,6 +653,12 @@ message UserGrant {
example: "\"69629023906488334\""
}
];
string avatar_url = 17 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "avatar url of the user"
example: "\"https://api.zitadel.ch/assets/v1/avatar-32432jkh4kj32\"";
}
];
}
enum UserGrantState {