From eb0eed21fa682774e476d0c720c501b37f0f7793 Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Mon, 26 May 2025 13:23:38 +0200 Subject: [PATCH 01/35] fix(api): correct mapping of user state queries (#9956) # Which Problems Are Solved the mapping of `ListUsers` was wrong for user states. # How the Problems Are Solved mapping of user state introduced to correctly map it # Additional Changes mapping of user type introduced to prevent same issue # Additional Context Requires backport to 2.x and 3.x Co-authored-by: Livio Spring --- internal/api/grpc/user/query.go | 4 +-- internal/api/grpc/user/v2/query.go | 4 +-- internal/api/grpc/user/v2beta/query.go | 4 +-- .../api/scim/resources/user_query_builder.go | 2 +- internal/query/user.go | 4 +-- pkg/grpc/user/user.go | 36 +++++++++++++++++++ pkg/grpc/user/v2/user.go | 34 ++++++++++++++++++ pkg/grpc/user/v2beta/user.go | 34 ++++++++++++++++++ 8 files changed, 113 insertions(+), 9 deletions(-) diff --git a/internal/api/grpc/user/query.go b/internal/api/grpc/user/query.go index 41cce01a8c..66edbac90e 100644 --- a/internal/api/grpc/user/query.go +++ b/internal/api/grpc/user/query.go @@ -84,11 +84,11 @@ func EmailQueryToQuery(q *user_pb.EmailQuery) (query.SearchQuery, error) { } func StateQueryToQuery(q *user_pb.StateQuery) (query.SearchQuery, error) { - return query.NewUserStateSearchQuery(int32(q.State)) + return query.NewUserStateSearchQuery(q.State.ToDomain()) } func TypeQueryToQuery(q *user_pb.TypeQuery) (query.SearchQuery, error) { - return query.NewUserTypeSearchQuery(int32(q.Type)) + return query.NewUserTypeSearchQuery(q.Type.ToDomain()) } func LoginNameQueryToQuery(q *user_pb.LoginNameQuery) (query.SearchQuery, error) { diff --git a/internal/api/grpc/user/v2/query.go b/internal/api/grpc/user/v2/query.go index 136a4a0932..dc886462be 100644 --- a/internal/api/grpc/user/v2/query.go +++ b/internal/api/grpc/user/v2/query.go @@ -298,11 +298,11 @@ func phoneQueryToQuery(q *user.PhoneQuery) (query.SearchQuery, error) { } func stateQueryToQuery(q *user.StateQuery) (query.SearchQuery, error) { - return query.NewUserStateSearchQuery(int32(q.State)) + return query.NewUserStateSearchQuery(q.State.ToDomain()) } func typeQueryToQuery(q *user.TypeQuery) (query.SearchQuery, error) { - return query.NewUserTypeSearchQuery(int32(q.Type)) + return query.NewUserTypeSearchQuery(q.Type.ToDomain()) } func loginNameQueryToQuery(q *user.LoginNameQuery) (query.SearchQuery, error) { diff --git a/internal/api/grpc/user/v2beta/query.go b/internal/api/grpc/user/v2beta/query.go index e3602abc33..46b009a72e 100644 --- a/internal/api/grpc/user/v2beta/query.go +++ b/internal/api/grpc/user/v2beta/query.go @@ -292,11 +292,11 @@ func phoneQueryToQuery(q *user.PhoneQuery) (query.SearchQuery, error) { } func stateQueryToQuery(q *user.StateQuery) (query.SearchQuery, error) { - return query.NewUserStateSearchQuery(int32(q.State)) + return query.NewUserStateSearchQuery(q.State.ToDomain()) } func typeQueryToQuery(q *user.TypeQuery) (query.SearchQuery, error) { - return query.NewUserTypeSearchQuery(int32(q.Type)) + return query.NewUserTypeSearchQuery(q.Type.ToDomain()) } func loginNameQueryToQuery(q *user.LoginNameQuery) (query.SearchQuery, error) { diff --git a/internal/api/scim/resources/user_query_builder.go b/internal/api/scim/resources/user_query_builder.go index b86b171fb5..7a06e4b3fd 100644 --- a/internal/api/scim/resources/user_query_builder.go +++ b/internal/api/scim/resources/user_query_builder.go @@ -70,7 +70,7 @@ func (h *UsersHandler) buildListQuery(ctx context.Context, request *ListRequest) } // the zitadel scim implementation only supports humans for now - userTypeQuery, err := query.NewUserTypeSearchQuery(int32(domain.UserTypeHuman)) + userTypeQuery, err := query.NewUserTypeSearchQuery(domain.UserTypeHuman) if err != nil { return nil, err } diff --git a/internal/query/user.go b/internal/query/user.go index a97e3bbd14..3d47847cac 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -776,11 +776,11 @@ func NewUserVerifiedPhoneSearchQuery(value string, comparison TextComparison) (S return NewTextQuery(NotifyVerifiedPhoneCol, value, comparison) } -func NewUserStateSearchQuery(value int32) (SearchQuery, error) { +func NewUserStateSearchQuery(value domain.UserState) (SearchQuery, error) { return NewNumberQuery(UserStateCol, value, NumberEquals) } -func NewUserTypeSearchQuery(value int32) (SearchQuery, error) { +func NewUserTypeSearchQuery(value domain.UserType) (SearchQuery, error) { return NewNumberQuery(UserTypeCol, value, NumberEquals) } diff --git a/pkg/grpc/user/user.go b/pkg/grpc/user/user.go index 450370e704..a86c957fd8 100644 --- a/pkg/grpc/user/user.go +++ b/pkg/grpc/user/user.go @@ -1,5 +1,7 @@ package user +import "github.com/zitadel/zitadel/internal/domain" + type SearchQuery_ResourceOwner struct { ResourceOwner *ResourceOwnerQuery } @@ -13,3 +15,37 @@ type ResourceOwnerQuery struct { type UserType = isUser_Type type MembershipType = isMembership_Type + +func (s UserState) ToDomain() domain.UserState { + switch s { + case UserState_USER_STATE_UNSPECIFIED: + return domain.UserStateUnspecified + case UserState_USER_STATE_ACTIVE: + return domain.UserStateActive + case UserState_USER_STATE_INACTIVE: + return domain.UserStateInactive + case UserState_USER_STATE_DELETED: + return domain.UserStateDeleted + case UserState_USER_STATE_LOCKED: + return domain.UserStateLocked + case UserState_USER_STATE_SUSPEND: + return domain.UserStateSuspend + case UserState_USER_STATE_INITIAL: + return domain.UserStateInitial + default: + return domain.UserStateUnspecified + } +} + +func (t Type) ToDomain() domain.UserType { + switch t { + case Type_TYPE_UNSPECIFIED: + return domain.UserTypeUnspecified + case Type_TYPE_HUMAN: + return domain.UserTypeHuman + case Type_TYPE_MACHINE: + return domain.UserTypeMachine + default: + return domain.UserTypeUnspecified + } +} diff --git a/pkg/grpc/user/v2/user.go b/pkg/grpc/user/v2/user.go index ec9245c8eb..20c3c6fe9b 100644 --- a/pkg/grpc/user/v2/user.go +++ b/pkg/grpc/user/v2/user.go @@ -1,3 +1,37 @@ package user +import "github.com/zitadel/zitadel/internal/domain" + type UserType = isUser_Type + +func (s UserState) ToDomain() domain.UserState { + switch s { + case UserState_USER_STATE_UNSPECIFIED: + return domain.UserStateUnspecified + case UserState_USER_STATE_ACTIVE: + return domain.UserStateActive + case UserState_USER_STATE_INACTIVE: + return domain.UserStateInactive + case UserState_USER_STATE_DELETED: + return domain.UserStateDeleted + case UserState_USER_STATE_LOCKED: + return domain.UserStateLocked + case UserState_USER_STATE_INITIAL: + return domain.UserStateInitial + default: + return domain.UserStateUnspecified + } +} + +func (t Type) ToDomain() domain.UserType { + switch t { + case Type_TYPE_UNSPECIFIED: + return domain.UserTypeUnspecified + case Type_TYPE_HUMAN: + return domain.UserTypeHuman + case Type_TYPE_MACHINE: + return domain.UserTypeMachine + default: + return domain.UserTypeUnspecified + } +} diff --git a/pkg/grpc/user/v2beta/user.go b/pkg/grpc/user/v2beta/user.go index ec9245c8eb..20c3c6fe9b 100644 --- a/pkg/grpc/user/v2beta/user.go +++ b/pkg/grpc/user/v2beta/user.go @@ -1,3 +1,37 @@ package user +import "github.com/zitadel/zitadel/internal/domain" + type UserType = isUser_Type + +func (s UserState) ToDomain() domain.UserState { + switch s { + case UserState_USER_STATE_UNSPECIFIED: + return domain.UserStateUnspecified + case UserState_USER_STATE_ACTIVE: + return domain.UserStateActive + case UserState_USER_STATE_INACTIVE: + return domain.UserStateInactive + case UserState_USER_STATE_DELETED: + return domain.UserStateDeleted + case UserState_USER_STATE_LOCKED: + return domain.UserStateLocked + case UserState_USER_STATE_INITIAL: + return domain.UserStateInitial + default: + return domain.UserStateUnspecified + } +} + +func (t Type) ToDomain() domain.UserType { + switch t { + case Type_TYPE_UNSPECIFIED: + return domain.UserTypeUnspecified + case Type_TYPE_HUMAN: + return domain.UserTypeHuman + case Type_TYPE_MACHINE: + return domain.UserTypeMachine + default: + return domain.UserTypeUnspecified + } +} From 833f6279e11c43652a579f2211311e2fc6c0e2a7 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Mon, 26 May 2025 13:59:20 +0200 Subject: [PATCH 02/35] fix: allow invite codes for users with verified mails (#9962) # Which Problems Are Solved Users who started the invitation code verification, but haven't set up any authentication method, need to be able to do so. This might require a new invitation code, which was currently not possible since creation was prevented for users with verified emails. # How the Problems Are Solved - Allow creation of invitation emails for users with verified emails. - Merged the creation and resend into a single method, defaulting the urlTemplate, applicatioName and authRequestID from the previous code (if one exists). On the user service API, the `ResendInviteCode` endpoint has been deprecated in favor of the `CreateInviteCode` # Additional Changes None # Additional Context - Noticed while investigating something internally. - requires backport to 2.x and 3.x --- .../user/v2/integration_test/user_test.go | 27 ++++++ internal/command/user_v2_invite.go | 83 ++++++++----------- internal/command/user_v2_invite_model.go | 2 +- internal/command/user_v2_invite_test.go | 75 +---------------- proto/zitadel/user/v2/user.proto | 4 +- proto/zitadel/user/v2/user_service.proto | 5 ++ 6 files changed, 71 insertions(+), 125 deletions(-) diff --git a/internal/api/grpc/user/v2/integration_test/user_test.go b/internal/api/grpc/user/v2/integration_test/user_test.go index 832268dc8c..7f211afd6f 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -3190,6 +3190,33 @@ func TestServer_CreateInviteCode(t *testing.T) { }, }, }, + { + name: "recreate", + args: args{ + ctx: CTX, + req: &user.CreateInviteCodeRequest{}, + prepare: func(request *user.CreateInviteCodeRequest) error { + resp := Instance.CreateHumanUser(CTX) + request.UserId = resp.GetUserId() + _, err := Instance.Client.UserV2.CreateInviteCode(CTX, &user.CreateInviteCodeRequest{ + UserId: resp.GetUserId(), + Verification: &user.CreateInviteCodeRequest_SendCode{ + SendCode: &user.SendInviteCode{ + UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"), + ApplicationName: gu.Ptr("TestApp"), + }, + }, + }) + return err + }, + }, + want: &user.CreateInviteCodeResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Instance.DefaultOrg.Id, + }, + }, + }, { name: "create, return code, ok", args: args{ diff --git a/internal/command/user_v2_invite.go b/internal/command/user_v2_invite.go index 7760107146..430ba8c7d1 100644 --- a/internal/command/user_v2_invite.go +++ b/internal/command/user_v2_invite.go @@ -19,14 +19,34 @@ type CreateUserInvite struct { URLTemplate string ReturnCode bool ApplicationName string + AuthRequestID string } func (c *Commands) CreateInviteCode(ctx context.Context, invite *CreateUserInvite) (details *domain.ObjectDetails, returnCode *string, err error) { + return c.sendInviteCode(ctx, invite, "", false) +} + +// ResendInviteCode resends the invite mail with a new code and an optional authRequestID. +// It will reuse the applicationName from the previous code. +func (c *Commands) ResendInviteCode(ctx context.Context, userID, resourceOwner, authRequestID string) (objectDetails *domain.ObjectDetails, err error) { + details, _, err := c.sendInviteCode( + ctx, + &CreateUserInvite{ + UserID: userID, + AuthRequestID: authRequestID, + }, + resourceOwner, + true, + ) + return details, err +} + +func (c *Commands) sendInviteCode(ctx context.Context, invite *CreateUserInvite, resourceOwner string, requireExisting bool) (details *domain.ObjectDetails, returnCode *string, err error) { invite.UserID = strings.TrimSpace(invite.UserID) if invite.UserID == "" { return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4jio3", "Errors.User.UserIDMissing") } - wm, err := c.userInviteCodeWriteModel(ctx, invite.UserID, "") + wm, err := c.userInviteCodeWriteModel(ctx, invite.UserID, resourceOwner) if err != nil { return nil, nil, err } @@ -39,10 +59,22 @@ func (c *Commands) CreateInviteCode(ctx context.Context, invite *CreateUserInvit if !wm.CreationAllowed() { return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-EF34g", "Errors.User.AlreadyInitialised") } + if requireExisting && wm.InviteCode == nil || wm.CodeReturned { + return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wr3gq", "Errors.User.Code.NotFound") + } code, err := c.newUserInviteCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint if err != nil { return nil, nil, err } + if invite.URLTemplate == "" { + invite.URLTemplate = wm.URLTemplate + } + if invite.ApplicationName == "" { + invite.ApplicationName = wm.ApplicationName + } + if invite.AuthRequestID == "" { + invite.AuthRequestID = wm.AuthRequestID + } err = c.pushAppendAndReduce(ctx, wm, user.NewHumanInviteCodeAddedEvent( ctx, UserAggregateFromWriteModelCtx(ctx, &wm.WriteModel), @@ -51,7 +83,7 @@ func (c *Commands) CreateInviteCode(ctx context.Context, invite *CreateUserInvit invite.URLTemplate, invite.ReturnCode, invite.ApplicationName, - "", + invite.AuthRequestID, )) if err != nil { return nil, nil, err @@ -62,53 +94,6 @@ func (c *Commands) CreateInviteCode(ctx context.Context, invite *CreateUserInvit return writeModelToObjectDetails(&wm.WriteModel), returnCode, nil } -// ResendInviteCode resends the invite mail with a new code and an optional authRequestID. -// It will reuse the applicationName from the previous code. -func (c *Commands) ResendInviteCode(ctx context.Context, userID, resourceOwner, authRequestID string) (objectDetails *domain.ObjectDetails, err error) { - if userID == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-2n8vs", "Errors.User.UserIDMissing") - } - - existingCode, err := c.userInviteCodeWriteModel(ctx, userID, resourceOwner) - if err != nil { - return nil, err - } - if !existingCode.UserState.Exists() { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-H3b2a", "Errors.User.NotFound") - } - if err := c.checkPermissionUpdateUser(ctx, existingCode.ResourceOwner, userID); err != nil { - return nil, err - } - if !existingCode.CreationAllowed() { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Gg42s", "Errors.User.AlreadyInitialised") - } - if existingCode.InviteCode == nil || existingCode.CodeReturned { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wr3gq", "Errors.User.Code.NotFound") - } - code, err := c.newUserInviteCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint - if err != nil { - return nil, err - } - if authRequestID == "" { - authRequestID = existingCode.AuthRequestID - } - err = c.pushAppendAndReduce(ctx, existingCode, - user.NewHumanInviteCodeAddedEvent( - ctx, - UserAggregateFromWriteModelCtx(ctx, &existingCode.WriteModel), - code.Crypted, - code.Expiry, - existingCode.URLTemplate, - false, - existingCode.ApplicationName, - authRequestID, - )) - if err != nil { - return nil, err - } - return writeModelToObjectDetails(&existingCode.WriteModel), nil -} - func (c *Commands) InviteCodeSent(ctx context.Context, userID, orgID string) (err error) { if userID == "" { return zerrors.ThrowInvalidArgument(nil, "COMMAND-Sgf31", "Errors.User.UserIDMissing") diff --git a/internal/command/user_v2_invite_model.go b/internal/command/user_v2_invite_model.go index 23f6322a19..6b2ab62e0d 100644 --- a/internal/command/user_v2_invite_model.go +++ b/internal/command/user_v2_invite_model.go @@ -28,7 +28,7 @@ type UserV2InviteWriteModel struct { } func (wm *UserV2InviteWriteModel) CreationAllowed() bool { - return !wm.EmailVerified && !wm.AuthMethodSet + return !wm.AuthMethodSet } func newUserV2InviteWriteModel(userID, orgID string) *UserV2InviteWriteModel { diff --git a/internal/command/user_v2_invite_test.go b/internal/command/user_v2_invite_test.go index 817987e7e4..04c00d876e 100644 --- a/internal/command/user_v2_invite_test.go +++ b/internal/command/user_v2_invite_test.go @@ -11,7 +11,6 @@ import ( "go.uber.org/mock/gomock" "golang.org/x/text/language" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -316,7 +315,7 @@ func TestCommands_ResendInviteCode(t *testing.T) { userID: "", }, want{ - err: zerrors.ThrowInvalidArgument(nil, "COMMAND-2n8vs", "Errors.User.UserIDMissing"), + err: zerrors.ThrowInvalidArgument(nil, "COMMAND-4jio3", "Errors.User.UserIDMissing"), }, }, { @@ -362,7 +361,7 @@ func TestCommands_ResendInviteCode(t *testing.T) { userID: "unknown", }, want{ - err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-H3b2a", "Errors.User.NotFound"), + err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wgvn4", "Errors.User.NotFound"), }, }, { @@ -580,76 +579,6 @@ func TestCommands_ResendInviteCode(t *testing.T) { }, }, }, - { - "resend with own user ok", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - user.NewHumanAddedEvent(context.Background(), - &user.NewAggregate("userID", "org1").Aggregate, - "username", "firstName", - "lastName", - "nickName", - "displayName", - language.Afrikaans, - domain.GenderUnspecified, - "email", - false, - ), - ), - eventFromEventPusher( - user.NewHumanInviteCodeAddedEvent(context.Background(), - &user.NewAggregate("userID", "org1").Aggregate, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("code"), - }, - time.Hour, - "", - false, - "", - "authRequestID", - ), - ), - ), - expectPush( - eventFromEventPusher( - user.NewHumanInviteCodeAddedEvent(authz.NewMockContext("instanceID", "org1", "userID"), - &user.NewAggregate("userID", "org1").Aggregate, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("code"), - }, - time.Hour, - "", - false, - "", - "authRequestID2", - ), - ), - ), - ), - checkPermission: newMockPermissionCheckNotAllowed(), // user does not have permission, is allowed in the own context - newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code", time.Hour), - defaultSecretGenerators: &SecretGenerators{}, - }, - args{ - ctx: authz.NewMockContext("instanceID", "org1", "userID"), - userID: "userID", - authRequestID: "authRequestID2", - }, - want{ - details: &domain.ObjectDetails{ - ResourceOwner: "org1", - ID: "userID", - }, - }, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/proto/zitadel/user/v2/user.proto b/proto/zitadel/user/v2/user.proto index e2a140ea27..9ea2b8906e 100644 --- a/proto/zitadel/user/v2/user.proto +++ b/proto/zitadel/user/v2/user.proto @@ -334,7 +334,7 @@ message AuthFactorU2F { message SendInviteCode { // Optionally set a url_template, which will be used in the invite mail sent by ZITADEL to guide the user to your invitation page. - // If no template is set, the default ZITADEL url will be used. + // If no template is set and no previous code was created, the default ZITADEL url will be used. // // The following placeholders can be used: UserID, OrgID, Code optional string url_template = 1 [ @@ -346,7 +346,7 @@ message SendInviteCode { } ]; // Optionally set an application name, which will be used in the invite mail sent by ZITADEL. - // If no application name is set, ZITADEL will be used as default. + // If no application name is set and no previous code was created, ZITADEL will be used as default. optional string application_name = 2 [ (validate.rules).string = {min_len: 1, max_len: 200}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { diff --git a/proto/zitadel/user/v2/user_service.proto b/proto/zitadel/user/v2/user_service.proto index 15bc2d7775..44d25c07b3 100644 --- a/proto/zitadel/user/v2/user_service.proto +++ b/proto/zitadel/user/v2/user_service.proto @@ -1135,6 +1135,8 @@ service UserService { // Create an invite code for a user // // Create an invite code for a user to initialize their first authentication method (password, passkeys, IdP) depending on the organization's available methods. + // If an invite code has been created previously, it's url template and application name will be used as defaults for the new code. + // The new code will overwrite the previous one and make it invalid. rpc CreateInviteCode (CreateInviteCodeRequest) returns (CreateInviteCodeResponse) { option (google.api.http) = { post: "/v2/users/{user_id}/invite_code" @@ -1158,6 +1160,8 @@ service UserService { // Resend an invite code for a user // + // Deprecated: Use [CreateInviteCode](apis/resources/user_service_v2/user-service-create-invite-code.api.mdx) instead. + // // Resend an invite code for a user to initialize their first authentication method (password, passkeys, IdP) depending on the organization's available methods. // A resend is only possible if a code has been created previously and sent to the user. If there is no code or it was directly returned, an error will be returned. rpc ResendInviteCode (ResendInviteCodeRequest) returns (ResendInviteCodeResponse) { @@ -1172,6 +1176,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200" value: { From 4d66a786c88ba624bb8ca7bd67a128b920e0cb7f Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Tue, 27 May 2025 16:26:46 +0200 Subject: [PATCH 03/35] feat: JWT IdP intent (#9966) # Which Problems Are Solved The login v1 allowed to use JWTs as IdP using the JWT IDP. The login V2 uses idp intents for such cases, which were not yet able to handle JWT IdPs. # How the Problems Are Solved - Added handling of JWT IdPs in `StartIdPIntent` and `RetrieveIdPIntent` - The redirect returned by the start, uses the existing `authRequestID` and `userAgentID` parameter names for compatibility reasons. - Added `/idps/jwt` endpoint to handle the proxied (callback) endpoint , which extracts and validates the JWT against the configured endpoint. # Additional Changes None # Additional Context - closes #9758 --- .../integrate/identity-providers/jwt_idp.md | 2 +- docs/static/img/guides/jwt_idp.png | Bin 275050 -> 38738 bytes .../user/v2/integration_test/user_test.go | 112 ++++++++++++++++++ internal/api/grpc/user/v2/intent.go | 2 +- internal/api/idp/idp.go | 86 ++++++++++++++ internal/idp/providers/jwt/jwt.go | 22 ++-- internal/idp/providers/jwt/jwt_test.go | 28 ++++- internal/idp/providers/jwt/session.go | 13 ++ internal/integration/client.go | 18 +++ internal/integration/sink/server.go | 52 ++++++++ 10 files changed, 319 insertions(+), 16 deletions(-) diff --git a/docs/docs/guides/integrate/identity-providers/jwt_idp.md b/docs/docs/guides/integrate/identity-providers/jwt_idp.md index 2a0e8b8e7a..52324ad5c2 100644 --- a/docs/docs/guides/integrate/identity-providers/jwt_idp.md +++ b/docs/docs/guides/integrate/identity-providers/jwt_idp.md @@ -49,7 +49,7 @@ The **JWT IdP Configuration** might then be: Therefore, if the user is redirected from ZITADEL to the JWT Endpoint on the WAF (`https://apps.test.com/existing/auth-new`), the session cookies previously issued by the WAF, will be sent along by the browser due to the path being on the same domain as the exiting application. -The WAF will reuse the session and send the JWT in the HTTP header `x-custom-tkn` to its upstream, the ZITADEL JWT Endpoint (`https://accounts.test.com/ui/login/login/jwt/authorize`). +The WAF will reuse the session and send the JWT in the HTTP header `x-custom-tkn` to its upstream, the ZITADEL JWT Endpoint (`https://accounts.test.com/ipds/jwt`). For the signature validation, ZITADEL must be able to connect to Keys Endpoint (`https://issuer.test.internal/keys`) and it will check if the token was signed (claim `iss`) by the defined Issuer (`https://issuer.test.internal`). diff --git a/docs/static/img/guides/jwt_idp.png b/docs/static/img/guides/jwt_idp.png index 73d5353521a9eb844c801ad9fe3bfc6291181dc9..218996aef756b0033575c54ff604034108a7f46a 100644 GIT binary patch literal 38738 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 literal 275050 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

xuQ1w*QK&q!^g}p!>(K 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

<<~ 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 diff --git a/internal/api/grpc/user/v2/integration_test/user_test.go b/internal/api/grpc/user/v2/integration_test/user_test.go index 7f211afd6f..4cf4ab21f8 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -1875,6 +1875,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { samlIdpID := Instance.AddSAMLProvider(IamCTX) samlRedirectIdpID := Instance.AddSAMLRedirectProvider(IamCTX, "") samlPostIdpID := Instance.AddSAMLPostProvider(IamCTX) + jwtIdPID := Instance.AddJWTProvider(IamCTX) type args struct { ctx context.Context req *user.StartIdentityProviderIntentRequest @@ -2097,6 +2098,30 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { }, wantErr: false, }, + { + name: "next step jwt idp", + args: args{ + CTX, + &user.StartIdentityProviderIntentRequest{ + IdpId: jwtIdPID, + Content: &user.StartIdentityProviderIntentRequest_Urls{ + Urls: &user.RedirectURLs{ + SuccessUrl: "https://example.com/success", + FailureUrl: "https://example.com/failure", + }, + }, + }, + }, + want: want{ + details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Instance.ID(), + }, + url: "https://example.com/jwt", + parametersExisting: []string{"authRequestID", "userAgentID"}, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -2134,6 +2159,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { oidcIdpID := Instance.AddGenericOIDCProvider(IamCTX, gofakeit.AppName()).GetId() samlIdpID := Instance.AddSAMLPostProvider(IamCTX) ldapIdpID := Instance.AddLDAPProvider(IamCTX) + jwtIdPID := Instance.AddJWTProvider(IamCTX) authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl()) require.NoError(t, err) intentID := authURL.Query().Get("state") @@ -2168,6 +2194,10 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { require.NoError(t, err) samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user", expiry) require.NoError(t, err) + jwtSuccessfulID, jwtToken, jwtChangeDate, jwtSequence, err := sink.SuccessfulJWTIntent(Instance.ID(), jwtIdPID, "id", "", expiry) + require.NoError(t, err) + jwtSuccessfulWithUserID, jwtWithUserToken, jwtWithUserChangeDate, jwtWithUserSequence, err := sink.SuccessfulJWTIntent(Instance.ID(), jwtIdPID, "id", "user", expiry) + require.NoError(t, err) type args struct { ctx context.Context req *user.RetrieveIdentityProviderIntentRequest @@ -2591,6 +2621,88 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }, wantErr: false, }, + { + name: "retrieve successful jwt intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: jwtSuccessfulID, + IdpIntentToken: jwtToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(jwtChangeDate), + ResourceOwner: Instance.ID(), + Sequence: jwtSequence, + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: jwtIdPID, + UserId: "id", + UserName: "", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "sub": "id", + }) + require.NoError(t, err) + return s + }(), + }, + AddHumanUser: &user.AddHumanUserRequest{ + Profile: &user.SetHumanProfile{ + PreferredLanguage: gu.Ptr("und"), + }, + IdpLinks: []*user.IDPLink{ + {IdpId: jwtIdPID, UserId: "id"}, + }, + Email: &user.SetHumanEmail{ + Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}}, + }, + }, + }, + wantErr: false, + }, + { + name: "retrieve successful jwt intent with linked user", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: jwtSuccessfulWithUserID, + IdpIntentToken: jwtWithUserToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(jwtWithUserChangeDate), + ResourceOwner: Instance.ID(), + Sequence: jwtWithUserSequence, + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: jwtIdPID, + UserId: "id", + UserName: "", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "sub": "id", + }) + require.NoError(t, err) + return s + }(), + }, + UserId: "user", + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/api/grpc/user/v2/intent.go b/internal/api/grpc/user/v2/intent.go index afb34deb83..5514b6ef03 100644 --- a/internal/api/grpc/user/v2/intent.go +++ b/internal/api/grpc/user/v2/intent.go @@ -173,7 +173,7 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.R case *oidc.Provider: idpUser, err = unmarshalIdpUser(intent.IDPUser, oidc.InitUser()) case *jwt.Provider: - idpUser, err = unmarshalIdpUser(intent.IDPUser, &jwt.User{}) + idpUser, err = unmarshalIdpUser(intent.IDPUser, jwt.InitUser()) case *azuread.Provider: idpUser, err = unmarshalRawIdpUser(intent.IDPUser, p.User()) case *github.Provider: diff --git a/internal/api/idp/idp.go b/internal/api/idp/idp.go index f688ba2352..8b1c24134a 100644 --- a/internal/api/idp/idp.go +++ b/internal/api/idp/idp.go @@ -3,6 +3,7 @@ package idp import ( "bytes" "context" + "encoding/base64" "encoding/xml" "errors" "fmt" @@ -48,6 +49,7 @@ const ( acsPath = idpPrefix + "/saml/acs" certificatePath = idpPrefix + "/saml/certificate" sloPath = idpPrefix + "/saml/slo" + jwtPath = "/jwt" paramIntentID = "id" paramToken = "token" @@ -129,6 +131,7 @@ func NewHandler( router.HandleFunc(certificatePath, h.handleCertificate) router.HandleFunc(acsPath, h.handleACS) router.HandleFunc(sloPath, h.handleSLO) + router.HandleFunc(jwtPath, h.handleJWT) return router } @@ -307,6 +310,89 @@ func (h *Handler) handleACS(w http.ResponseWriter, r *http.Request) { redirectToSuccessURL(w, r, intent, token, userID) } +func (h *Handler) handleJWT(w http.ResponseWriter, r *http.Request) { + intentID, err := h.intentIDFromJWTRequest(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + intent, err := h.commands.GetActiveIntent(r.Context(), intentID) + if err != nil { + if zerrors.IsNotFound(err) { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + redirectToFailureURLErr(w, r, intent, err) + return + } + idpConfig, err := h.getProvider(r.Context(), intent.IDPID) + if err != nil { + cmdErr := h.commands.FailIDPIntent(r.Context(), intent, err.Error()) + logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent") + redirectToFailureURLErr(w, r, intent, err) + return + } + jwtIDP, ok := idpConfig.(*jwt.Provider) + if !ok { + err := zerrors.ThrowInvalidArgument(nil, "IDP-JK23ed", "Errors.ExternalIDP.IDPTypeNotImplemented") + cmdErr := h.commands.FailIDPIntent(r.Context(), intent, err.Error()) + logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent") + redirectToFailureURLErr(w, r, intent, err) + return + } + h.handleJWTExtraction(w, r, intent, jwtIDP) +} + +func (h *Handler) intentIDFromJWTRequest(r *http.Request) (string, error) { + // for compatibility of the old JWT provider we use the auth request id parameter to pass the intent id + intentID := r.FormValue(jwt.QueryAuthRequestID) + // for compatibility of the old JWT provider we use the user agent id parameter to pass the encrypted intent id + encryptedIntentID := r.FormValue(jwt.QueryUserAgentID) + if err := h.checkIntentID(intentID, encryptedIntentID); err != nil { + return "", err + } + return intentID, nil +} + +func (h *Handler) checkIntentID(intentID, encryptedIntentID string) error { + if intentID == "" || encryptedIntentID == "" { + return zerrors.ThrowInvalidArgument(nil, "LOGIN-adfzz", "Errors.AuthRequest.MissingParameters") + } + id, err := base64.RawURLEncoding.DecodeString(encryptedIntentID) + if err != nil { + return err + } + decryptedIntentID, err := h.encryptionAlgorithm.DecryptString(id, h.encryptionAlgorithm.EncryptionKeyID()) + if err != nil { + return err + } + if intentID != decryptedIntentID { + return zerrors.ThrowInvalidArgument(nil, "LOGIN-adfzz", "Errors.AuthRequest.MissingParameters") + } + return nil +} + +func (h *Handler) handleJWTExtraction(w http.ResponseWriter, r *http.Request, intent *command.IDPIntentWriteModel, identityProvider *jwt.Provider) { + session := jwt.NewSessionFromRequest(identityProvider, r) + user, err := session.FetchUser(r.Context()) + if err != nil { + cmdErr := h.commands.FailIDPIntent(r.Context(), intent, err.Error()) + logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent") + redirectToFailureURLErr(w, r, intent, err) + return + } + + userID, err := h.checkExternalUser(r.Context(), intent.IDPID, user.GetID()) + logging.WithFields("intent", intent.AggregateID).OnError(err).Error("could not check if idp user already exists") + + token, err := h.commands.SucceedIDPIntent(r.Context(), intent, user, session, userID) + if err != nil { + redirectToFailureURLErr(w, r, intent, err) + return + } + redirectToSuccessURL(w, r, intent, token, userID) +} + func (h *Handler) handleCallback(w http.ResponseWriter, r *http.Request) { ctx := r.Context() data, err := h.parseCallbackRequest(r) diff --git a/internal/idp/providers/jwt/jwt.go b/internal/idp/providers/jwt/jwt.go index 99347f31a3..d972102b01 100644 --- a/internal/idp/providers/jwt/jwt.go +++ b/internal/idp/providers/jwt/jwt.go @@ -11,14 +11,14 @@ import ( ) const ( - queryAuthRequestID = "authRequestID" - queryUserAgentID = "userAgentID" + QueryAuthRequestID = "authRequestID" + QueryUserAgentID = "userAgentID" ) var _ idp.Provider = (*Provider)(nil) var ( - ErrMissingUserAgentID = errors.New("userAgentID missing") + ErrMissingState = errors.New("state missing") ) // Provider is the [idp.Provider] implementation for a JWT provider @@ -92,32 +92,32 @@ func (p *Provider) Name() string { // It will create a [Session] with an AuthURL, pointing to the jwtEndpoint // with the authRequest and encrypted userAgent ids. func (p *Provider) BeginAuth(ctx context.Context, state string, params ...idp.Parameter) (idp.Session, error) { - userAgentID, err := userAgentIDFromParams(params...) - if err != nil { - return nil, err + if state == "" { + return nil, ErrMissingState } + userAgentID := userAgentIDFromParams(state, params...) redirect, err := url.Parse(p.jwtEndpoint) if err != nil { return nil, err } q := redirect.Query() - q.Set(queryAuthRequestID, state) + q.Set(QueryAuthRequestID, state) nonce, err := p.encryptionAlg.Encrypt([]byte(userAgentID)) if err != nil { return nil, err } - q.Set(queryUserAgentID, base64.RawURLEncoding.EncodeToString(nonce)) + q.Set(QueryUserAgentID, base64.RawURLEncoding.EncodeToString(nonce)) redirect.RawQuery = q.Encode() return &Session{AuthURL: redirect.String()}, nil } -func userAgentIDFromParams(params ...idp.Parameter) (string, error) { +func userAgentIDFromParams(state string, params ...idp.Parameter) string { for _, param := range params { if id, ok := param.(idp.UserAgentID); ok { - return string(id), nil + return string(id) } } - return "", ErrMissingUserAgentID + return state } // IsLinkingAllowed implements the [idp.Provider] interface. diff --git a/internal/idp/providers/jwt/jwt_test.go b/internal/idp/providers/jwt/jwt_test.go index 59e32b4690..5756c58e07 100644 --- a/internal/idp/providers/jwt/jwt_test.go +++ b/internal/idp/providers/jwt/jwt_test.go @@ -23,6 +23,7 @@ func TestProvider_BeginAuth(t *testing.T) { encryptionAlg func(t *testing.T) crypto.EncryptionAlgorithm } type args struct { + state string params []idp.Parameter } type want struct { @@ -36,7 +37,7 @@ func TestProvider_BeginAuth(t *testing.T) { want want }{ { - name: "missing userAgentID error", + name: "missing state, error", fields: fields{ issuer: "https://jwt.com", jwtEndpoint: "https://auth.com/jwt", @@ -47,14 +48,34 @@ func TestProvider_BeginAuth(t *testing.T) { }, }, args: args{ + state: "", params: nil, }, want: want{ err: func(err error) bool { - return errors.Is(err, ErrMissingUserAgentID) + return errors.Is(err, ErrMissingState) }, }, }, + { + name: "missing userAgentID, fallback to state", + fields: fields{ + issuer: "https://jwt.com", + jwtEndpoint: "https://auth.com/jwt", + keysEndpoint: "https://jwt.com/keys", + headerName: "jwt-header", + encryptionAlg: func(t *testing.T) crypto.EncryptionAlgorithm { + return crypto.CreateMockEncryptionAlg(gomock.NewController(t)) + }, + }, + args: args{ + state: "testState", + params: nil, + }, + want: want{ + session: &Session{AuthURL: "https://auth.com/jwt?authRequestID=testState&userAgentID=dGVzdFN0YXRl"}, + }, + }, { name: "successful auth", fields: fields{ @@ -67,6 +88,7 @@ func TestProvider_BeginAuth(t *testing.T) { }, }, args: args{ + state: "testState", params: []idp.Parameter{ idp.UserAgentID("agent"), }, @@ -91,7 +113,7 @@ func TestProvider_BeginAuth(t *testing.T) { require.NoError(t, err) ctx := context.Background() - session, err := provider.BeginAuth(ctx, "testState", tt.args.params...) + session, err := provider.BeginAuth(ctx, tt.args.state, tt.args.params...) if tt.want.err != nil && !tt.want.err(err) { a.Fail("invalid error", err) } diff --git a/internal/idp/providers/jwt/session.go b/internal/idp/providers/jwt/session.go index 5138812f3c..85b164a9c5 100644 --- a/internal/idp/providers/jwt/session.go +++ b/internal/idp/providers/jwt/session.go @@ -5,11 +5,13 @@ import ( "errors" "fmt" "net/http" + "strings" "time" "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v3/pkg/oidc" + "golang.org/x/oauth2" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/domain" @@ -34,6 +36,11 @@ func NewSession(provider *Provider, tokens *oidc.Tokens[*oidc.IDTokenClaims]) *S return &Session{Provider: provider, Tokens: tokens} } +func NewSessionFromRequest(provider *Provider, r *http.Request) *Session { + token := strings.TrimPrefix(r.Header.Get(provider.headerName), oidc.PrefixBearer) + return NewSession(provider, &oidc.Tokens[*oidc.IDTokenClaims]{IDToken: token, Token: &oauth2.Token{}}) +} + // GetAuth implements the [idp.Session] interface. func (s *Session) GetAuth(ctx context.Context) (string, bool) { return idp.Redirect(s.AuthURL) @@ -99,6 +106,12 @@ func (s *Session) validateToken(ctx context.Context, token string) (*oidc.IDToke return claims, nil } +func InitUser() *User { + return &User{ + IDTokenClaims: &oidc.IDTokenClaims{}, + } +} + type User struct { *oidc.IDTokenClaims } diff --git a/internal/integration/client.go b/internal/integration/client.go index 3efd682ee1..320809a7e8 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -684,6 +684,24 @@ func (i *Instance) AddLDAPProvider(ctx context.Context) string { return resp.GetId() } +func (i *Instance) AddJWTProvider(ctx context.Context) string { + resp, err := i.Client.Admin.AddJWTProvider(ctx, &admin.AddJWTProviderRequest{ + Name: "jwt-idp", + Issuer: "https://example.com", + JwtEndpoint: "https://example.com/jwt", + KeysEndpoint: "https://example.com/keys", + HeaderName: "Authorization", + ProviderOptions: &idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }) + logging.OnError(err).Panic("create jwt idp") + return resp.GetId() +} + func (i *Instance) CreateIntent(ctx context.Context, idpID string) *user_v2.StartIdentityProviderIntentResponse { resp, err := i.Client.UserV2.StartIdentityProviderIntent(ctx, &user_v2.StartIdentityProviderIntentRequest{ IdpId: idpID, diff --git a/internal/integration/sink/server.go b/internal/integration/sink/server.go index 8abb31a63e..653c5236d6 100644 --- a/internal/integration/sink/server.go +++ b/internal/integration/sink/server.go @@ -27,6 +27,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/idp/providers/jwt" "github.com/zitadel/zitadel/internal/idp/providers/ldap" "github.com/zitadel/zitadel/internal/idp/providers/oauth" openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" @@ -124,6 +125,25 @@ func SuccessfulLDAPIntent(instanceID, idpID, idpUserID, userID string) (string, return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil } +func SuccessfulJWTIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) { + u := url.URL{ + Scheme: "http", + Host: host, + Path: successfulIntentJWTPath(), + } + resp, err := callIntent(u.String(), &SuccessfulIntentRequest{ + InstanceID: instanceID, + IDPID: idpID, + IDPUserID: idpUserID, + UserID: userID, + Expiry: expiry, + }) + if err != nil { + return "", "", time.Time{}, uint64(0), err + } + return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil +} + // StartServer starts a simple HTTP server on localhost:8081 // ZITADEL can use the server to send HTTP requests which can be // used to validate tests through [Subscribe]rs. @@ -145,6 +165,7 @@ func StartServer(commands *command.Commands) (close func()) { router.HandleFunc(successfulIntentOIDCPath(), successfulIntentHandler(commands, createSuccessfulOIDCIntent)) router.HandleFunc(successfulIntentSAMLPath(), successfulIntentHandler(commands, createSuccessfulSAMLIntent)) router.HandleFunc(successfulIntentLDAPPath(), successfulIntentHandler(commands, createSuccessfulLDAPIntent)) + router.HandleFunc(successfulIntentJWTPath(), successfulIntentHandler(commands, createSuccessfulJWTIntent)) } s := &http.Server{ Addr: listenAddr, @@ -195,6 +216,10 @@ func successfulIntentLDAPPath() string { return path.Join(successfulIntentPath(), "/", "ldap") } +func successfulIntentJWTPath() string { + return path.Join(successfulIntentPath(), "/", "jwt") +} + // forwarder handles incoming HTTP requests from ZITADEL and // forwards them to all subscribed web sockets. type forwarder struct { @@ -497,3 +522,30 @@ func createSuccessfulLDAPIntent(ctx context.Context, cmd *command.Commands, req writeModel.ProcessedSequence, }, nil } + +func createSuccessfulJWTIntent(ctx context.Context, cmd *command.Commands, req *SuccessfulIntentRequest) (*SuccessfulIntentResponse, error) { + intentID, err := createIntent(ctx, cmd, req.InstanceID, req.IDPID) + writeModel, err := cmd.GetIntentWriteModel(ctx, intentID, req.InstanceID) + idpUser := &jwt.User{ + IDTokenClaims: &oidc.IDTokenClaims{ + TokenClaims: oidc.TokenClaims{ + Subject: req.IDPUserID, + }, + }, + } + session := &jwt.Session{ + Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ + IDToken: "idToken", + }, + } + token, err := cmd.SucceedIDPIntent(ctx, writeModel, idpUser, session, req.UserID) + if err != nil { + return nil, err + } + return &SuccessfulIntentResponse{ + intentID, + token, + writeModel.ChangeDate, + writeModel.ProcessedSequence, + }, nil +} From 77b433367ef8ac643e5988d54d9a9ce30e127ddc Mon Sep 17 00:00:00 2001 From: Connor <80653133+connorHashDash@users.noreply.github.com> Date: Wed, 28 May 2025 07:06:27 +0100 Subject: [PATCH 04/35] fix(login): Copy to clipboard button in MFA login step now compatible in non-chrome browser (#9880) related to issue [#9379](https://github.com/zitadel/zitadel/issues/9379) # Which Problems Are Solved Copy to clipboard button was not compatible with Webkit/ Firefox browsers. # How the Problems Are Solved The previous function used addEventListener without a callback function as a second argument. I simply added the callback function and left existing code intact to fix the bug. # Additional Changes Added `type=button` to prevent submitting the form when clicking the button. # Additional Context none --------- Co-authored-by: Livio Spring --- .../api/ui/login/static/resources/scripts/copy_to_clipboard.js | 2 +- internal/api/ui/login/static/templates/mfa_init_otp.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/api/ui/login/static/resources/scripts/copy_to_clipboard.js b/internal/api/ui/login/static/resources/scripts/copy_to_clipboard.js index 97359cdde5..848aa0742b 100644 --- a/internal/api/ui/login/static/resources/scripts/copy_to_clipboard.js +++ b/internal/api/ui/login/static/resources/scripts/copy_to_clipboard.js @@ -3,4 +3,4 @@ const copyToClipboard = str => { }; let copyButton = document.getElementById("copy"); -copyButton.addEventListener("click", copyToClipboard(copyButton.getAttribute("data-copy"))); +copyButton.addEventListener("click", () => copyToClipboard(copyButton.getAttribute("data-copy"))); diff --git a/internal/api/ui/login/static/templates/mfa_init_otp.html b/internal/api/ui/login/static/templates/mfa_init_otp.html index b84a88a5b1..a9ae31dcdd 100644 --- a/internal/api/ui/login/static/templates/mfa_init_otp.html +++ b/internal/api/ui/login/static/templates/mfa_init_otp.html @@ -28,7 +28,7 @@

From c097887bc5f680e12c998580fb56d98a15758f53 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Wed, 28 May 2025 10:12:27 +0200 Subject: [PATCH 05/35] fix: validate proto header and provide https enforcement (#9975) # Which Problems Are Solved ZITADEL uses the notification triggering requests Forwarded or X-Forwarded-Proto header to build the button link sent in emails for confirming a password reset with the emailed code. If this header is overwritten and a user clicks the link to a malicious site in the email, the secret code can be retrieved and used to reset the users password and take over his account. Accounts with MFA or Passwordless enabled can not be taken over by this attack. # How the Problems Are Solved - The `X-Forwarded-Proto` and `proto` of the Forwarded headers are validated (http / https). - Additionally, when exposing ZITADEL through https. An overwrite to http is no longer possible. # Additional Changes None # Additional Context None --- .../api/http/middleware/origin_interceptor.go | 32 +++++++------- .../middleware/origin_interceptor_test.go | 42 +++++++++---------- 2 files changed, 39 insertions(+), 35 deletions(-) diff --git a/internal/api/http/middleware/origin_interceptor.go b/internal/api/http/middleware/origin_interceptor.go index 35af8770b7..607855b80f 100644 --- a/internal/api/http/middleware/origin_interceptor.go +++ b/internal/api/http/middleware/origin_interceptor.go @@ -10,12 +10,12 @@ import ( http_util "github.com/zitadel/zitadel/internal/api/http" ) -func WithOrigin(fallBackToHttps bool, http1Header, http2Header string, instanceHostHeaders, publicDomainHeaders []string) mux.MiddlewareFunc { +func WithOrigin(enforceHttps bool, http1Header, http2Header string, instanceHostHeaders, publicDomainHeaders []string) mux.MiddlewareFunc { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := composeDomainContext( r, - fallBackToHttps, + enforceHttps, // to make sure we don't break existing configurations we append the existing checked headers as well slices.Compact(append(instanceHostHeaders, http1Header, http2Header, http_util.Forwarded, http_util.ZitadelForwarded, http_util.ForwardedFor, http_util.ForwardedHost, http_util.ForwardedProto)), publicDomainHeaders, @@ -25,28 +25,32 @@ func WithOrigin(fallBackToHttps bool, http1Header, http2Header string, instanceH } } -func composeDomainContext(r *http.Request, fallBackToHttps bool, instanceDomainHeaders, publicDomainHeaders []string) *http_util.DomainCtx { +func composeDomainContext(r *http.Request, enforceHttps bool, instanceDomainHeaders, publicDomainHeaders []string) *http_util.DomainCtx { instanceHost, instanceProto := hostFromRequest(r, instanceDomainHeaders) publicHost, publicProto := hostFromRequest(r, publicDomainHeaders) - if publicProto == "" { - publicProto = instanceProto - } - if publicProto == "" { - publicProto = "http" - if fallBackToHttps { - publicProto = "https" - } - } if instanceHost == "" { instanceHost = r.Host } return &http_util.DomainCtx{ InstanceHost: instanceHost, - Protocol: publicProto, + Protocol: protocolFromRequest(instanceProto, publicProto, enforceHttps), PublicHost: publicHost, } } +func protocolFromRequest(instanceProto, publicProto string, enforceHttps bool) string { + if enforceHttps { + return "https" + } + if publicProto != "" { + return publicProto + } + if instanceProto != "" { + return instanceProto + } + return "http" +} + func hostFromRequest(r *http.Request, headers []string) (host, proto string) { var hostFromHeader, protoFromHeader string for _, header := range headers { @@ -65,7 +69,7 @@ func hostFromRequest(r *http.Request, headers []string) (host, proto string) { if host == "" { host = hostFromHeader } - if proto == "" { + if proto == "" && (protoFromHeader == "http" || protoFromHeader == "https") { proto = protoFromHeader } } diff --git a/internal/api/http/middleware/origin_interceptor_test.go b/internal/api/http/middleware/origin_interceptor_test.go index 989e4d48b3..7419c91aba 100644 --- a/internal/api/http/middleware/origin_interceptor_test.go +++ b/internal/api/http/middleware/origin_interceptor_test.go @@ -11,8 +11,8 @@ import ( func Test_composeOrigin(t *testing.T) { type args struct { - h http.Header - fallBackToHttps bool + h http.Header + enforceHttps bool } tests := []struct { name string @@ -30,7 +30,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "Forwarded": []string{"proto=https"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "host.header", @@ -42,7 +42,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "Forwarded": []string{"host=forwarded.host"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", @@ -54,7 +54,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "Forwarded": []string{"proto=https;host=forwarded.host"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", @@ -66,7 +66,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "Forwarded": []string{"proto=https;host=forwarded.host, proto=http;host=forwarded.host2"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", @@ -78,7 +78,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "Forwarded": []string{"proto=https;host=forwarded.host, proto=http"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", @@ -90,11 +90,11 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "Forwarded": []string{"proto=http", "proto=https;host=forwarded.host", "proto=http"}, }, - fallBackToHttps: true, + enforceHttps: true, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", - Protocol: "http", + Protocol: "https", }, }, { name: "x-forwarded-proto https", @@ -102,7 +102,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "X-Forwarded-Proto": []string{"https"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "host.header", @@ -114,25 +114,25 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "X-Forwarded-Proto": []string{"http"}, }, - fallBackToHttps: true, + enforceHttps: true, }, want: &http_util.DomainCtx{ InstanceHost: "host.header", - Protocol: "http", + Protocol: "https", }, }, { name: "fallback to http", args: args{ - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "host.header", Protocol: "http", }, }, { - name: "fallback to https", + name: "enforce https", args: args{ - fallBackToHttps: true, + enforceHttps: true, }, want: &http_util.DomainCtx{ InstanceHost: "host.header", @@ -144,7 +144,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "X-Forwarded-Host": []string{"x-forwarded.host"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "x-forwarded.host", @@ -157,7 +157,7 @@ func Test_composeOrigin(t *testing.T) { "X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Host": []string{"x-forwarded.host"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "x-forwarded.host", @@ -170,7 +170,7 @@ func Test_composeOrigin(t *testing.T) { "Forwarded": []string{"host=forwarded.host"}, "X-Forwarded-Host": []string{"x-forwarded.host"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", @@ -183,7 +183,7 @@ func Test_composeOrigin(t *testing.T) { "Forwarded": []string{"host=forwarded.host"}, "X-Forwarded-Proto": []string{"https"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", @@ -198,10 +198,10 @@ func Test_composeOrigin(t *testing.T) { Host: "host.header", Header: tt.args.h, }, - tt.args.fallBackToHttps, + tt.args.enforceHttps, []string{http_util.Forwarded, http_util.ForwardedFor, http_util.ForwardedHost, http_util.ForwardedProto}, []string{"x-zitadel-public-host"}, - ), "headers: %+v, fallBackToHttps: %t", tt.args.h, tt.args.fallBackToHttps) + ), "headers: %+v, enforceHttps: %t", tt.args.h, tt.args.enforceHttps) }) } } From 046b165db85f397c4a437c4112be34695cfd5f58 Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Wed, 28 May 2025 10:47:42 +0200 Subject: [PATCH 06/35] docs(a10016): add versions for v2.66 - v3 (#9908) # Which Problems Are Solved versions were missing in https://github.com/zitadel/zitadel/pull/9882 # How the Problems Are Solved added versions for 2.66.x, 2.67.x, 2.68.x, 2.69.x, 2.70.x, 2.71.x, 3.x # Additional Context can be merged after: - https://github.com/zitadel/zitadel/pull/9901 - https://github.com/zitadel/zitadel/pull/9903 - https://github.com/zitadel/zitadel/pull/9904 - https://github.com/zitadel/zitadel/pull/9907 - https://github.com/zitadel/zitadel/pull/9905 - https://github.com/zitadel/zitadel/pull/9906 - https://github.com/zitadel/zitadel/pull/9916 --- docs/docs/support/advisory/a10016.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/docs/support/advisory/a10016.md b/docs/docs/support/advisory/a10016.md index 794c354e42..84dd1cd34c 100644 --- a/docs/docs/support/advisory/a10016.md +++ b/docs/docs/support/advisory/a10016.md @@ -4,15 +4,19 @@ title: Technical Advisory 10016 ## Date -Versions:[^1] - - v2.65.x: > v2.65.9 +- v2.66.x > v2.66.17 +- v2.67.x > v2.67.14 +- v2.68.x > v2.68.10 +- v2.69.x > v2.69.10 +- v2.70.x > v2.70.11 +- v2.71.x > v2.71.10 +- v3.x > v3.2.1 + Date: 2025-05-14 -Last updated: 2025-05-14 - -[^1]: The mentioned fix is being rolled out gradually on multiple patch releases of Zitadel. This advisory will be updated as we release these versions. +Last updated: 2025-05-19 ## Description From 131f70db3423b80da1b038a822da59c908e4ffa6 Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Wed, 28 May 2025 23:54:18 +0200 Subject: [PATCH 07/35] fix(eventstore): use decimal, correct mirror (#9914) # Eventstore fixes - `event.Position` used float64 before which can lead to [precision loss](https://github.com/golang/go/issues/47300). The type got replaced by [a type without precision loss](https://github.com/jackc/pgx-shopspring-decimal) - the handler reported the wrong error if the current state was updated and therefore took longer to retry failed events. # Mirror fixes - max age of auth requests can be configured to speed up copying data from `auth.auth_requests` table. Auth requests last updated before the set age will be ignored. Default is 1 month - notification projections are skipped because notifications should be sent by the source system. The projections are set to the latest position - ensure that mirror can be executed multiple times --------- Co-authored-by: Livio Spring --- cmd/mirror/event.go | 4 +- cmd/mirror/event_store.go | 14 ++++-- cmd/mirror/projections.go | 7 +++ go.mod | 2 + go.sum | 4 ++ .../eventsourcing/handler/handler.go | 16 +++++-- internal/api/oidc/key.go | 17 +++---- internal/api/saml/certificate.go | 17 +++---- .../eventsourcing/handler/handler.go | 16 +++++-- internal/database/database.go | 4 ++ internal/database/dialect/connections.go | 8 +++- internal/eventstore/event.go | 4 +- internal/eventstore/event_base.go | 5 +- internal/eventstore/eventstore.go | 17 +++++-- .../eventstore/eventstore_querier_test.go | 16 ++++--- internal/eventstore/eventstore_test.go | 17 +++---- .../eventstore/handler/v2/field_handler.go | 18 ++++--- internal/eventstore/handler/v2/handler.go | 32 +++++++++---- internal/eventstore/handler/v2/state.go | 8 ++-- internal/eventstore/handler/v2/state_test.go | 13 ++--- internal/eventstore/handler/v2/statement.go | 5 +- internal/eventstore/local_postgres_test.go | 19 ++++++-- internal/eventstore/read_model.go | 22 +++++---- internal/eventstore/repository/event.go | 5 +- .../repository/mock/repository.mock.go | 15 +++--- .../repository/mock/repository.mock.impl.go | 5 +- .../eventstore/repository/search_query.go | 10 ++-- .../repository/sql/local_postgres_test.go | 8 +++- .../eventstore/repository/sql/postgres.go | 14 +++--- .../repository/sql/postgres_test.go | 4 +- internal/eventstore/repository/sql/query.go | 25 +++++----- .../eventstore/repository/sql/query_test.go | 44 +++++++++-------- internal/eventstore/search_query.go | 24 +++++----- internal/eventstore/search_query_test.go | 4 +- internal/eventstore/v1/models/event.go | 6 ++- internal/eventstore/v3/event.go | 7 +-- internal/notification/projections.go | 20 ++++++++ internal/query/access_token.go | 5 +- internal/query/current_state.go | 11 +++-- internal/query/current_state_test.go | 8 ++-- internal/query/projection/projection.go | 38 +++++++++++---- internal/query/user_grant.go | 4 +- internal/query/user_membership.go | 4 +- internal/v2/database/number_filter.go | 3 +- internal/v2/eventstore/event_store.go | 6 ++- internal/v2/eventstore/postgres/push_test.go | 24 +++++----- internal/v2/eventstore/postgres/query_test.go | 48 ++++++++++--------- internal/v2/eventstore/query.go | 6 ++- internal/v2/eventstore/query_test.go | 26 +++++----- .../v2/readmodel/last_successful_mirror.go | 7 ++- internal/v2/system/mirror/succeeded.go | 6 ++- 51 files changed, 436 insertions(+), 236 deletions(-) diff --git a/cmd/mirror/event.go b/cmd/mirror/event.go index d513990e10..567fb659a2 100644 --- a/cmd/mirror/event.go +++ b/cmd/mirror/event.go @@ -3,6 +3,8 @@ package mirror import ( "context" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/eventstore" "github.com/zitadel/zitadel/internal/v2/readmodel" "github.com/zitadel/zitadel/internal/v2/system" @@ -29,7 +31,7 @@ func queryLastSuccessfulMigration(ctx context.Context, destinationES *eventstore return lastSuccess, nil } -func writeMigrationSucceeded(ctx context.Context, destinationES *eventstore.EventStore, id, source string, position float64) error { +func writeMigrationSucceeded(ctx context.Context, destinationES *eventstore.EventStore, id, source string, position decimal.Decimal) error { return destinationES.Push( ctx, eventstore.NewPushIntent( diff --git a/cmd/mirror/event_store.go b/cmd/mirror/event_store.go index 41c529c025..be14abe340 100644 --- a/cmd/mirror/event_store.go +++ b/cmd/mirror/event_store.go @@ -8,7 +8,9 @@ import ( "io" "time" + "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/stdlib" + "github.com/shopspring/decimal" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/zitadel/logging" @@ -89,7 +91,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { previousMigration, err := queryLastSuccessfulMigration(ctx, destinationES, source.DatabaseName()) logging.OnError(err).Fatal("unable to query latest successful migration") - var maxPosition float64 + var maxPosition decimal.Decimal err = source.QueryRowContext(ctx, func(row *sql.Row) error { return row.Scan(&maxPosition) @@ -101,7 +103,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { logging.WithFields("from", previousMigration.Position, "to", maxPosition).Info("start event migration") nextPos := make(chan bool, 1) - pos := make(chan float64, 1) + pos := make(chan decimal.Decimal, 1) errs := make(chan error, 3) go func() { @@ -152,7 +154,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { go func() { defer close(pos) for range nextPos { - var position float64 + var position decimal.Decimal err := dest.QueryRowContext( ctx, func(row *sql.Row) error { @@ -175,6 +177,10 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { tag, err := conn.PgConn().CopyFrom(ctx, reader, "COPY eventstore.events2 FROM STDIN") eventCount = tag.RowsAffected() if err != nil { + pgErr := new(pgconn.PgError) + errors.As(err, &pgErr) + + logging.WithError(err).WithField("pg_err_details", pgErr.Detail).Error("unable to copy events into destination") return zerrors.ThrowUnknown(err, "MIGRA-DTHi7", "unable to copy events into destination") } @@ -187,7 +193,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { logging.WithFields("took", time.Since(start), "count", eventCount).Info("events migrated") } -func writeCopyEventsDone(ctx context.Context, es *eventstore.EventStore, id, source string, position float64, errs <-chan error) { +func writeCopyEventsDone(ctx context.Context, es *eventstore.EventStore, id, source string, position decimal.Decimal, errs <-chan error) { joinedErrs := make([]error, 0, len(errs)) for err := range errs { joinedErrs = append(joinedErrs, err) diff --git a/cmd/mirror/projections.go b/cmd/mirror/projections.go index 4e12b29748..0ff4356d6f 100644 --- a/cmd/mirror/projections.go +++ b/cmd/mirror/projections.go @@ -296,6 +296,13 @@ func execProjections(ctx context.Context, instances <-chan string, failedInstanc continue } + err = projection.ProjectInstanceFields(ctx) + if err != nil { + logging.WithFields("instance", instance).WithError(err).Info("trigger fields failed") + failedInstances <- instance + continue + } + err = auth_handler.ProjectInstance(ctx) if err != nil { logging.WithFields("instance", instance).WithError(err).Info("trigger auth handler failed") diff --git a/go.mod b/go.mod index ec86708942..c1cbf2dd77 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/h2non/gock v1.2.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/improbable-eng/grpc-web v0.15.0 + github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e github.com/jackc/pgx/v5 v5.7.5 github.com/jarcoal/jpath v0.0.0-20140328210829-f76b8b2dbf52 github.com/jinzhu/gorm v1.9.16 @@ -65,6 +66,7 @@ require ( github.com/riverqueue/river/rivertype v0.22.0 github.com/rs/cors v1.11.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 + github.com/shopspring/decimal v1.3.1 github.com/sony/gobreaker/v2 v2.1.0 github.com/sony/sonyflake v1.2.1 github.com/spf13/cobra v1.9.1 diff --git a/go.sum b/go.sum index 6d54730acd..cc3bc35841 100644 --- a/go.sum +++ b/go.sum @@ -442,6 +442,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e h1:i3gQ/Zo7sk4LUVbsAjTNeC4gIjoPNIZVzs4EXstssV4= +github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e/go.mod h1:zUHglCZ4mpDUPgIwqEKoba6+tcUQzRdb1+DPTuYe9pI= github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= @@ -705,6 +707,8 @@ github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= diff --git a/internal/admin/repository/eventsourcing/handler/handler.go b/internal/admin/repository/eventsourcing/handler/handler.go index 76584b55b0..b38e890e66 100644 --- a/internal/admin/repository/eventsourcing/handler/handler.go +++ b/internal/admin/repository/eventsourcing/handler/handler.go @@ -2,9 +2,11 @@ package handler import ( "context" + "errors" "fmt" "time" + "github.com/jackc/pgx/v5/pgconn" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing/view" @@ -63,9 +65,17 @@ func Start(ctx context.Context) { func ProjectInstance(ctx context.Context) error { for i, projection := range projections { logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("starting admin projection") - _, err := projection.Trigger(ctx) - if err != nil { - return err + for { + _, err := projection.Trigger(ctx) + if err == nil { + break + } + var pgErr *pgconn.PgError + errors.As(err, &pgErr) + if pgErr.Code != database.PgUniqueConstraintErrorCode { + return err + } + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID()).WithError(err).Debug("admin projection failed because of unique constraint, retrying") } logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("admin projection done") } diff --git a/internal/api/oidc/key.go b/internal/api/oidc/key.go index 81f3b1c466..852bbc7db8 100644 --- a/internal/api/oidc/key.go +++ b/internal/api/oidc/key.go @@ -11,6 +11,7 @@ import ( "github.com/go-jose/go-jose/v4" "github.com/jonboulle/clockwork" "github.com/muhlemmer/gu" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/op" @@ -350,14 +351,14 @@ func (o *OPStorage) getSigningKey(ctx context.Context) (op.SigningKey, error) { if len(keys.Keys) > 0 { return PrivateKeyToSigningKey(SelectSigningKey(keys.Keys), o.encAlg) } - var position float64 + var position decimal.Decimal if keys.State != nil { position = keys.State.Position } return nil, o.refreshSigningKey(ctx, position) } -func (o *OPStorage) refreshSigningKey(ctx context.Context, position float64) error { +func (o *OPStorage) refreshSigningKey(ctx context.Context, position decimal.Decimal) error { ok, err := o.ensureIsLatestKey(ctx, position) if err != nil || !ok { return zerrors.ThrowInternal(err, "OIDC-ASfh3", "cannot ensure that projection is up to date") @@ -369,12 +370,12 @@ func (o *OPStorage) refreshSigningKey(ctx context.Context, position float64) err return zerrors.ThrowInternal(nil, "OIDC-Df1bh", "") } -func (o *OPStorage) ensureIsLatestKey(ctx context.Context, position float64) (bool, error) { - maxSequence, err := o.getMaxKeySequence(ctx) +func (o *OPStorage) ensureIsLatestKey(ctx context.Context, position decimal.Decimal) (bool, error) { + maxSequence, err := o.getMaxKeyPosition(ctx) if err != nil { return false, fmt.Errorf("error retrieving new events: %w", err) } - return position >= maxSequence, nil + return position.GreaterThanOrEqual(maxSequence), nil } func PrivateKeyToSigningKey(key query.PrivateKey, algorithm crypto.EncryptionAlgorithm) (_ op.SigningKey, err error) { @@ -412,9 +413,9 @@ func (o *OPStorage) lockAndGenerateSigningKeyPair(ctx context.Context) error { return o.command.GenerateSigningKeyPair(setOIDCCtx(ctx), "RS256") } -func (o *OPStorage) getMaxKeySequence(ctx context.Context) (float64, error) { - return o.eventstore.LatestSequence(ctx, - eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence). +func (o *OPStorage) getMaxKeyPosition(ctx context.Context) (decimal.Decimal, error) { + return o.eventstore.LatestPosition(ctx, + eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxPosition). ResourceOwner(authz.GetInstance(ctx).InstanceID()). AwaitOpenTransactions(). AddQuery(). diff --git a/internal/api/saml/certificate.go b/internal/api/saml/certificate.go index ff130f7709..14752cd5cd 100644 --- a/internal/api/saml/certificate.go +++ b/internal/api/saml/certificate.go @@ -6,6 +6,7 @@ import ( "time" "github.com/go-jose/go-jose/v4" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/saml/pkg/provider/key" @@ -76,7 +77,7 @@ func (p *Storage) getCertificateAndKey(ctx context.Context, usage crypto.KeyUsag return p.certificateToCertificateAndKey(selectCertificate(certs.Certificates)) } - var position float64 + var position decimal.Decimal if certs.State != nil { position = certs.State.Position } @@ -87,7 +88,7 @@ func (p *Storage) getCertificateAndKey(ctx context.Context, usage crypto.KeyUsag func (p *Storage) refreshCertificate( ctx context.Context, usage crypto.KeyUsage, - position float64, + position decimal.Decimal, ) error { ok, err := p.ensureIsLatestCertificate(ctx, position) if err != nil { @@ -103,12 +104,12 @@ func (p *Storage) refreshCertificate( return nil } -func (p *Storage) ensureIsLatestCertificate(ctx context.Context, position float64) (bool, error) { - maxSequence, err := p.getMaxKeySequence(ctx) +func (p *Storage) ensureIsLatestCertificate(ctx context.Context, position decimal.Decimal) (bool, error) { + maxSequence, err := p.getMaxKeyPosition(ctx) if err != nil { return false, fmt.Errorf("error retrieving new events: %w", err) } - return position >= maxSequence, nil + return position.GreaterThanOrEqual(maxSequence), nil } func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage crypto.KeyUsage) error { @@ -151,9 +152,9 @@ func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage cr } } -func (p *Storage) getMaxKeySequence(ctx context.Context) (float64, error) { - return p.eventstore.LatestSequence(ctx, - eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence). +func (p *Storage) getMaxKeyPosition(ctx context.Context) (decimal.Decimal, error) { + return p.eventstore.LatestPosition(ctx, + eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxPosition). ResourceOwner(authz.GetInstance(ctx).InstanceID()). AwaitOpenTransactions(). AddQuery(). diff --git a/internal/auth/repository/eventsourcing/handler/handler.go b/internal/auth/repository/eventsourcing/handler/handler.go index 74a27a8312..0c151bb412 100644 --- a/internal/auth/repository/eventsourcing/handler/handler.go +++ b/internal/auth/repository/eventsourcing/handler/handler.go @@ -2,9 +2,11 @@ package handler import ( "context" + "errors" "fmt" "time" + "github.com/jackc/pgx/v5/pgconn" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" @@ -78,9 +80,17 @@ func Projections() []*handler2.Handler { func ProjectInstance(ctx context.Context) error { for i, projection := range projections { logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("starting auth projection") - _, err := projection.Trigger(ctx) - if err != nil { - return err + for { + _, err := projection.Trigger(ctx) + if err == nil { + break + } + var pgErr *pgconn.PgError + errors.As(err, &pgErr) + if pgErr.Code != database.PgUniqueConstraintErrorCode { + return err + } + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID()).WithError(err).Debug("auth projection failed because of unique constraint, retrying") } logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("auth projection done") } diff --git a/internal/database/database.go b/internal/database/database.go index ddc26a7961..b40715d6b5 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -64,6 +64,10 @@ func CloseTransaction(tx Tx, err error) error { return commitErr } +const ( + PgUniqueConstraintErrorCode = "23505" +) + type Config struct { Dialects map[string]interface{} `mapstructure:",remain"` connector dialect.Connector diff --git a/internal/database/dialect/connections.go b/internal/database/dialect/connections.go index 11b2681fea..a5c90b4059 100644 --- a/internal/database/dialect/connections.go +++ b/internal/database/dialect/connections.go @@ -5,6 +5,7 @@ import ( "errors" "reflect" + pgxdecimal "github.com/jackc/pgx-shopspring-decimal" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) @@ -23,7 +24,12 @@ type ConnectionConfig struct { AfterRelease []func(c *pgx.Conn) error } -var afterConnectFuncs []func(ctx context.Context, c *pgx.Conn) error +var afterConnectFuncs = []func(ctx context.Context, c *pgx.Conn) error{ + func(ctx context.Context, c *pgx.Conn) error { + pgxdecimal.Register(c.TypeMap()) + return nil + }, +} func RegisterAfterConnect(f func(ctx context.Context, c *pgx.Conn) error) { afterConnectFuncs = append(afterConnectFuncs, f) diff --git a/internal/eventstore/event.go b/internal/eventstore/event.go index 3df096f069..656a02f33d 100644 --- a/internal/eventstore/event.go +++ b/internal/eventstore/event.go @@ -5,6 +5,8 @@ import ( "reflect" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/zerrors" ) @@ -44,7 +46,7 @@ type Event interface { // CreatedAt is the time the event was created at CreatedAt() time.Time // Position is the global position of the event - Position() float64 + Position() decimal.Decimal // Unmarshal parses the payload and stores the result // in the value pointed to by ptr. If ptr is nil or not a pointer, diff --git a/internal/eventstore/event_base.go b/internal/eventstore/event_base.go index ed81e95320..6a911bc0eb 100644 --- a/internal/eventstore/event_base.go +++ b/internal/eventstore/event_base.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" @@ -25,7 +26,7 @@ type BaseEvent struct { Agg *Aggregate `json:"-"` Seq uint64 - Pos float64 + Pos decimal.Decimal Creation time.Time previousAggregateSequence uint64 previousAggregateTypeSequence uint64 @@ -38,7 +39,7 @@ type BaseEvent struct { } // Position implements Event. -func (e *BaseEvent) Position() float64 { +func (e *BaseEvent) Position() decimal.Decimal { return e.Pos } diff --git a/internal/eventstore/eventstore.go b/internal/eventstore/eventstore.go index 4954df86c8..8a8d32bc43 100644 --- a/internal/eventstore/eventstore.go +++ b/internal/eventstore/eventstore.go @@ -7,6 +7,7 @@ import ( "time" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" @@ -14,6 +15,12 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) +func init() { + // this is needed to ensure that position is marshaled as a number + // otherwise it will be marshaled as a string + decimal.MarshalJSONWithoutQuotes = true +} + // Eventstore abstracts all functions needed to store valid events // and filters the stored events type Eventstore struct { @@ -229,11 +236,11 @@ func (es *Eventstore) FilterToReducer(ctx context.Context, searchQuery *SearchQu }) } -// LatestSequence filters the latest sequence for the given search query -func (es *Eventstore) LatestSequence(ctx context.Context, queryFactory *SearchQueryBuilder) (float64, error) { +// LatestPosition filters the latest position for the given search query +func (es *Eventstore) LatestPosition(ctx context.Context, queryFactory *SearchQueryBuilder) (decimal.Decimal, error) { queryFactory.InstanceID(authz.GetInstance(ctx).InstanceID()) - return es.querier.LatestSequence(ctx, queryFactory) + return es.querier.LatestPosition(ctx, queryFactory) } // InstanceIDs returns the distinct instance ids found by the search query @@ -265,8 +272,8 @@ type Querier interface { Health(ctx context.Context) error // FilterToReducer calls r for every event returned from the storage FilterToReducer(ctx context.Context, searchQuery *SearchQueryBuilder, r Reducer) error - // LatestSequence returns the latest sequence found by the search query - LatestSequence(ctx context.Context, queryFactory *SearchQueryBuilder) (float64, error) + // LatestPosition returns the latest position found by the search query + LatestPosition(ctx context.Context, queryFactory *SearchQueryBuilder) (decimal.Decimal, error) // InstanceIDs returns the instance ids found by the search query InstanceIDs(ctx context.Context, queryFactory *SearchQueryBuilder) ([]string, error) // Client returns the underlying database connection diff --git a/internal/eventstore/eventstore_querier_test.go b/internal/eventstore/eventstore_querier_test.go index 3f23c5da75..88797a835e 100644 --- a/internal/eventstore/eventstore_querier_test.go +++ b/internal/eventstore/eventstore_querier_test.go @@ -4,6 +4,8 @@ import ( "context" "testing" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/eventstore" ) @@ -131,7 +133,7 @@ func TestEventstore_Filter(t *testing.T) { } } -func TestEventstore_LatestSequence(t *testing.T) { +func TestEventstore_LatestPosition(t *testing.T) { type args struct { searchQuery *eventstore.SearchQueryBuilder } @@ -139,7 +141,7 @@ func TestEventstore_LatestSequence(t *testing.T) { existingEvents []eventstore.Command } type res struct { - sequence float64 + position decimal.Decimal } tests := []struct { name string @@ -151,7 +153,7 @@ func TestEventstore_LatestSequence(t *testing.T) { { name: "aggregate type filter no sequence", args: args{ - searchQuery: eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence). + searchQuery: eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxPosition). AddQuery(). AggregateTypes("not found"). Builder(), @@ -168,7 +170,7 @@ func TestEventstore_LatestSequence(t *testing.T) { { name: "aggregate type filter sequence", args: args{ - searchQuery: eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence). + searchQuery: eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxPosition). AddQuery(). AggregateTypes(eventstore.AggregateType(t.Name())). Builder(), @@ -202,12 +204,12 @@ func TestEventstore_LatestSequence(t *testing.T) { return } - sequence, err := db.LatestSequence(context.Background(), tt.args.searchQuery) + position, err := db.LatestPosition(context.Background(), tt.args.searchQuery) if (err != nil) != tt.wantErr { t.Errorf("eventstore.query() error = %v, wantErr %v", err, tt.wantErr) } - if tt.res.sequence > sequence { - t.Errorf("eventstore.query() expected sequence: %v got %v", tt.res.sequence, sequence) + if tt.res.position.GreaterThan(position) { + t.Errorf("eventstore.query() expected position: %v got %v", tt.res.position, position) } }) } diff --git a/internal/eventstore/eventstore_test.go b/internal/eventstore/eventstore_test.go index 9e1aa77db1..5452572faa 100644 --- a/internal/eventstore/eventstore_test.go +++ b/internal/eventstore/eventstore_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/service" @@ -397,7 +398,7 @@ func (repo *testPusher) Push(_ context.Context, _ database.ContextQueryExecuter, type testQuerier struct { events []Event - sequence float64 + sequence decimal.Decimal instances []string err error t *testing.T @@ -430,9 +431,9 @@ func (repo *testQuerier) FilterToReducer(ctx context.Context, searchQuery *Searc return nil } -func (repo *testQuerier) LatestSequence(ctx context.Context, queryFactory *SearchQueryBuilder) (float64, error) { +func (repo *testQuerier) LatestPosition(ctx context.Context, queryFactory *SearchQueryBuilder) (decimal.Decimal, error) { if repo.err != nil { - return 0, repo.err + return decimal.Decimal{}, repo.err } return repo.sequence, nil } @@ -1076,7 +1077,7 @@ func TestEventstore_FilterEvents(t *testing.T) { } } -func TestEventstore_LatestSequence(t *testing.T) { +func TestEventstore_LatestPosition(t *testing.T) { type args struct { query *SearchQueryBuilder } @@ -1096,7 +1097,7 @@ func TestEventstore_LatestSequence(t *testing.T) { name: "no events", args: args{ query: &SearchQueryBuilder{ - columns: ColumnsMaxSequence, + columns: ColumnsMaxPosition, queries: []*SearchQuery{ { builder: &SearchQueryBuilder{}, @@ -1119,7 +1120,7 @@ func TestEventstore_LatestSequence(t *testing.T) { name: "repo error", args: args{ query: &SearchQueryBuilder{ - columns: ColumnsMaxSequence, + columns: ColumnsMaxPosition, queries: []*SearchQuery{ { builder: &SearchQueryBuilder{}, @@ -1142,7 +1143,7 @@ func TestEventstore_LatestSequence(t *testing.T) { name: "found events", args: args{ query: &SearchQueryBuilder{ - columns: ColumnsMaxSequence, + columns: ColumnsMaxPosition, queries: []*SearchQuery{ { builder: &SearchQueryBuilder{}, @@ -1168,7 +1169,7 @@ func TestEventstore_LatestSequence(t *testing.T) { querier: tt.fields.repo, } - _, err := es.LatestSequence(context.Background(), tt.args.query) + _, err := es.LatestPosition(context.Background(), tt.args.query) if (err != nil) != tt.res.wantErr { t.Errorf("Eventstore.aggregatesToEvents() error = %v, wantErr %v", err, tt.res.wantErr) } diff --git a/internal/eventstore/handler/v2/field_handler.go b/internal/eventstore/handler/v2/field_handler.go index ad309ac790..3c25731c83 100644 --- a/internal/eventstore/handler/v2/field_handler.go +++ b/internal/eventstore/handler/v2/field_handler.go @@ -8,6 +8,7 @@ import ( "time" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/zitadel/internal/eventstore" ) @@ -126,10 +127,15 @@ func (h *FieldHandler) processEvents(ctx context.Context, config *triggerConfig) return additionalIteration, err } // stop execution if currentState.eventTimestamp >= config.maxCreatedAt - if config.maxPosition != 0 && currentState.position >= config.maxPosition { + if !config.maxPosition.IsZero() && currentState.position.GreaterThanOrEqual(config.maxPosition) { return false, nil } + if config.minPosition.GreaterThan(decimal.NewFromInt(0)) { + currentState.position = config.minPosition + currentState.offset = 0 + } + events, additionalIteration, err := h.fetchEvents(ctx, tx, currentState) if err != nil { return additionalIteration, err @@ -159,7 +165,7 @@ func (h *FieldHandler) fetchEvents(ctx context.Context, tx *sql.Tx, currentState idx, offset := skipPreviouslyReducedEvents(events, currentState) - if currentState.position == events[len(events)-1].Position() { + if currentState.position.Equal(events[len(events)-1].Position()) { offset += currentState.offset } currentState.position = events[len(events)-1].Position() @@ -179,7 +185,7 @@ func (h *FieldHandler) fetchEvents(ctx context.Context, tx *sql.Tx, currentState fillFieldsEvents := make([]eventstore.FillFieldsEvent, len(events)) highestPosition := events[len(events)-1].Position() for i, event := range events { - if event.Position() == highestPosition { + if event.Position().Equal(highestPosition) { offset++ } fillFieldsEvents[i] = event.(eventstore.FillFieldsEvent) @@ -189,14 +195,14 @@ func (h *FieldHandler) fetchEvents(ctx context.Context, tx *sql.Tx, currentState } func skipPreviouslyReducedEvents(events []eventstore.Event, currentState *state) (index int, offset uint32) { - var position float64 + var position decimal.Decimal for i, event := range events { - if event.Position() != position { + if !event.Position().Equal(position) { offset = 0 position = event.Position() } offset++ - if event.Position() == currentState.position && + if event.Position().Equal(currentState.position) && event.Aggregate().ID == currentState.aggregateID && event.Aggregate().Type == currentState.aggregateType && event.Sequence() == currentState.sequence { diff --git a/internal/eventstore/handler/v2/handler.go b/internal/eventstore/handler/v2/handler.go index fb696ad090..fd8b206b38 100644 --- a/internal/eventstore/handler/v2/handler.go +++ b/internal/eventstore/handler/v2/handler.go @@ -4,13 +4,13 @@ import ( "context" "database/sql" "errors" - "math" "math/rand" "slices" "sync" "time" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" @@ -395,7 +395,8 @@ func (h *Handler) existingInstances(ctx context.Context) ([]string, error) { type triggerConfig struct { awaitRunning bool - maxPosition float64 + maxPosition decimal.Decimal + minPosition decimal.Decimal } type TriggerOpt func(conf *triggerConfig) @@ -406,12 +407,18 @@ func WithAwaitRunning() TriggerOpt { } } -func WithMaxPosition(position float64) TriggerOpt { +func WithMaxPosition(position decimal.Decimal) TriggerOpt { return func(conf *triggerConfig) { conf.maxPosition = position } } +func WithMinPosition(position decimal.Decimal) TriggerOpt { + return func(conf *triggerConfig) { + conf.minPosition = position + } +} + func (h *Handler) Trigger(ctx context.Context, opts ...TriggerOpt) (_ context.Context, err error) { config := new(triggerConfig) for _, opt := range opts { @@ -520,10 +527,15 @@ func (h *Handler) processEvents(ctx context.Context, config *triggerConfig) (add return additionalIteration, err } // stop execution if currentState.position >= config.maxPosition - if config.maxPosition != 0 && currentState.position >= config.maxPosition { + if !config.maxPosition.Equal(decimal.Decimal{}) && currentState.position.GreaterThanOrEqual(config.maxPosition) { return false, nil } + if config.minPosition.GreaterThan(decimal.NewFromInt(0)) { + currentState.position = config.minPosition + currentState.offset = 0 + } + var statements []*Statement statements, additionalIteration, err = h.generateStatements(ctx, tx, currentState) if err != nil { @@ -565,7 +577,10 @@ func (h *Handler) processEvents(ctx context.Context, config *triggerConfig) (add currentState.sequence = statements[lastProcessedIndex].Sequence currentState.eventTimestamp = statements[lastProcessedIndex].CreationDate - err = h.setState(tx, currentState) + setStateErr := h.setState(tx, currentState) + if setStateErr != nil { + err = setStateErr + } return additionalIteration, err } @@ -615,7 +630,7 @@ func (h *Handler) generateStatements(ctx context.Context, tx *sql.Tx, currentSta func skipPreviouslyReducedStatements(statements []*Statement, currentState *state) int { for i, statement := range statements { - if statement.Position == currentState.position && + if statement.Position.Equal(currentState.position) && statement.Aggregate.ID == currentState.aggregateID && statement.Aggregate.Type == currentState.aggregateType && statement.Sequence == currentState.sequence { @@ -678,9 +693,8 @@ func (h *Handler) eventQuery(currentState *state) *eventstore.SearchQueryBuilder OrderAsc(). InstanceID(currentState.instanceID) - if currentState.position > 0 { - // decrease position by 10 because builder.PositionAfter filters for position > and we need position >= - builder = builder.PositionAfter(math.Float64frombits(math.Float64bits(currentState.position) - 10)) + if currentState.position.GreaterThan(decimal.Decimal{}) { + builder = builder.PositionAtLeast(currentState.position) if currentState.offset > 0 { builder = builder.Offset(currentState.offset) } diff --git a/internal/eventstore/handler/v2/state.go b/internal/eventstore/handler/v2/state.go index d3b6953488..c4afaed204 100644 --- a/internal/eventstore/handler/v2/state.go +++ b/internal/eventstore/handler/v2/state.go @@ -7,6 +7,8 @@ import ( "errors" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/zerrors" @@ -14,7 +16,7 @@ import ( type state struct { instanceID string - position float64 + position decimal.Decimal eventTimestamp time.Time aggregateType eventstore.AggregateType aggregateID string @@ -45,7 +47,7 @@ func (h *Handler) currentState(ctx context.Context, tx *sql.Tx, config *triggerC aggregateType = new(sql.NullString) sequence = new(sql.NullInt64) timestamp = new(sql.NullTime) - position = new(sql.NullFloat64) + position = new(decimal.NullDecimal) offset = new(sql.NullInt64) ) @@ -75,7 +77,7 @@ func (h *Handler) currentState(ctx context.Context, tx *sql.Tx, config *triggerC currentState.aggregateType = eventstore.AggregateType(aggregateType.String) currentState.sequence = uint64(sequence.Int64) currentState.eventTimestamp = timestamp.Time - currentState.position = position.Float64 + currentState.position = position.Decimal // psql does not provide unsigned numbers so we work around it currentState.offset = uint32(offset.Int64) return currentState, nil diff --git a/internal/eventstore/handler/v2/state_test.go b/internal/eventstore/handler/v2/state_test.go index cc5fb1fbab..ef91d78e55 100644 --- a/internal/eventstore/handler/v2/state_test.go +++ b/internal/eventstore/handler/v2/state_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/database/mock" @@ -166,7 +167,7 @@ func TestHandler_updateLastUpdated(t *testing.T) { updatedState: &state{ instanceID: "instance", eventTimestamp: time.Now(), - position: 42, + position: decimal.NewFromInt(42), }, }, isErr: func(t *testing.T, err error) { @@ -192,7 +193,7 @@ func TestHandler_updateLastUpdated(t *testing.T) { updatedState: &state{ instanceID: "instance", eventTimestamp: time.Now(), - position: 42, + position: decimal.NewFromInt(42), }, }, isErr: func(t *testing.T, err error) { @@ -217,7 +218,7 @@ func TestHandler_updateLastUpdated(t *testing.T) { eventstore.AggregateType("aggregate type"), uint64(42), mock.AnyType[time.Time]{}, - float64(42), + decimal.NewFromInt(42), uint32(0), ), mock.WithExecRowsAffected(1), @@ -228,7 +229,7 @@ func TestHandler_updateLastUpdated(t *testing.T) { updatedState: &state{ instanceID: "instance", eventTimestamp: time.Now(), - position: 42, + position: decimal.NewFromInt(42), aggregateType: "aggregate type", aggregateID: "aggregate id", sequence: 42, @@ -397,7 +398,7 @@ func TestHandler_currentState(t *testing.T) { "aggregate type", int64(42), testTime, - float64(42), + decimal.NewFromInt(42).String(), uint16(10), }, }, @@ -412,7 +413,7 @@ func TestHandler_currentState(t *testing.T) { currentState: &state{ instanceID: "instance", eventTimestamp: testTime, - position: 42, + position: decimal.NewFromInt(42), aggregateType: "aggregate type", aggregateID: "aggregate id", sequence: 42, diff --git a/internal/eventstore/handler/v2/statement.go b/internal/eventstore/handler/v2/statement.go index a02e5d3580..5024c8c945 100644 --- a/internal/eventstore/handler/v2/statement.go +++ b/internal/eventstore/handler/v2/statement.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "golang.org/x/exp/constraints" @@ -52,7 +53,7 @@ func (h *Handler) eventsToStatements(tx *sql.Tx, events []eventstore.Event, curr return statements, err } offset++ - if previousPosition != event.Position() { + if !previousPosition.Equal(event.Position()) { // offset is 1 because we want to skip this event offset = 1 } @@ -82,7 +83,7 @@ func (h *Handler) reduce(event eventstore.Event) (*Statement, error) { type Statement struct { Aggregate *eventstore.Aggregate Sequence uint64 - Position float64 + Position decimal.Decimal CreationDate time.Time offset uint32 diff --git a/internal/eventstore/local_postgres_test.go b/internal/eventstore/local_postgres_test.go index d75292b3ff..fdb8b4f516 100644 --- a/internal/eventstore/local_postgres_test.go +++ b/internal/eventstore/local_postgres_test.go @@ -2,12 +2,13 @@ package eventstore_test import ( "context" - "database/sql" "encoding/json" "os" "testing" "time" + pgxdecimal "github.com/jackc/pgx-shopspring-decimal" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/stdlib" "github.com/zitadel/logging" @@ -40,7 +41,10 @@ func TestMain(m *testing.M) { connConfig, err := pgxpool.ParseConfig(config.GetConnectionURL()) logging.OnError(err).Fatal("unable to parse db url") - connConfig.AfterConnect = new_es.RegisterEventstoreTypes + connConfig.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { + pgxdecimal.Register(conn.TypeMap()) + return new_es.RegisterEventstoreTypes(ctx, conn) + } pool, err := pgxpool.NewWithConfig(context.Background(), connConfig) logging.OnError(err).Fatal("unable to create db pool") @@ -101,10 +105,19 @@ func initDB(ctx context.Context, db *database.DB) error { } func connectLocalhost() (*database.DB, error) { - client, err := sql.Open("pgx", "postgresql://postgres@localhost:5432/postgres?sslmode=disable") + config, err := pgxpool.ParseConfig("postgresql://postgres@localhost:5432/postgres?sslmode=disable") if err != nil { return nil, err } + config.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { + pgxdecimal.Register(conn.TypeMap()) + return new_es.RegisterEventstoreTypes(ctx, conn) + } + pool, err := pgxpool.NewWithConfig(context.Background(), config) + if err != nil { + return nil, err + } + client := stdlib.OpenDBFromPool(pool) if err = client.Ping(); err != nil { return nil, err } diff --git a/internal/eventstore/read_model.go b/internal/eventstore/read_model.go index d2c755cc3a..ae77275732 100644 --- a/internal/eventstore/read_model.go +++ b/internal/eventstore/read_model.go @@ -1,19 +1,23 @@ package eventstore -import "time" +import ( + "time" + + "github.com/shopspring/decimal" +) // ReadModel is the minimum representation of a read model. // It implements a basic reducer // it might be saved in a database or in memory type ReadModel struct { - AggregateID string `json:"-"` - ProcessedSequence uint64 `json:"-"` - CreationDate time.Time `json:"-"` - ChangeDate time.Time `json:"-"` - Events []Event `json:"-"` - ResourceOwner string `json:"-"` - InstanceID string `json:"-"` - Position float64 `json:"-"` + AggregateID string `json:"-"` + ProcessedSequence uint64 `json:"-"` + CreationDate time.Time `json:"-"` + ChangeDate time.Time `json:"-"` + Events []Event `json:"-"` + ResourceOwner string `json:"-"` + InstanceID string `json:"-"` + Position decimal.Decimal `json:"-"` } // AppendEvents adds all the events to the read model. diff --git a/internal/eventstore/repository/event.go b/internal/eventstore/repository/event.go index d0d2660d79..1107649934 100644 --- a/internal/eventstore/repository/event.go +++ b/internal/eventstore/repository/event.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/eventstore" @@ -22,7 +23,7 @@ type Event struct { // Seq is the sequence of the event Seq uint64 // Pos is the global sequence of the event multiple events can have the same sequence - Pos float64 + Pos decimal.Decimal //CreationDate is the time the event is created // it's used for human readability. @@ -97,7 +98,7 @@ func (e *Event) Sequence() uint64 { } // Position implements [eventstore.Event] -func (e *Event) Position() float64 { +func (e *Event) Position() decimal.Decimal { return e.Pos } diff --git a/internal/eventstore/repository/mock/repository.mock.go b/internal/eventstore/repository/mock/repository.mock.go index 8d5c0430ad..12925bc975 100644 --- a/internal/eventstore/repository/mock/repository.mock.go +++ b/internal/eventstore/repository/mock/repository.mock.go @@ -13,6 +13,7 @@ import ( context "context" reflect "reflect" + decimal "github.com/shopspring/decimal" database "github.com/zitadel/zitadel/internal/database" eventstore "github.com/zitadel/zitadel/internal/eventstore" gomock "go.uber.org/mock/gomock" @@ -98,19 +99,19 @@ func (mr *MockQuerierMockRecorder) InstanceIDs(arg0, arg1 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceIDs", reflect.TypeOf((*MockQuerier)(nil).InstanceIDs), arg0, arg1) } -// LatestSequence mocks base method. -func (m *MockQuerier) LatestSequence(arg0 context.Context, arg1 *eventstore.SearchQueryBuilder) (float64, error) { +// LatestPosition mocks base method. +func (m *MockQuerier) LatestPosition(arg0 context.Context, arg1 *eventstore.SearchQueryBuilder) (decimal.Decimal, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LatestSequence", arg0, arg1) - ret0, _ := ret[0].(float64) + ret := m.ctrl.Call(m, "LatestPosition", arg0, arg1) + ret0, _ := ret[0].(decimal.Decimal) ret1, _ := ret[1].(error) return ret0, ret1 } -// LatestSequence indicates an expected call of LatestSequence. -func (mr *MockQuerierMockRecorder) LatestSequence(arg0, arg1 any) *gomock.Call { +// LatestPosition indicates an expected call of LatestPosition. +func (mr *MockQuerierMockRecorder) LatestPosition(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LatestSequence", reflect.TypeOf((*MockQuerier)(nil).LatestSequence), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LatestPosition", reflect.TypeOf((*MockQuerier)(nil).LatestPosition), arg0, arg1) } // MockPusher is a mock of Pusher interface. diff --git a/internal/eventstore/repository/mock/repository.mock.impl.go b/internal/eventstore/repository/mock/repository.mock.impl.go index ced76953cb..313f7ee5e8 100644 --- a/internal/eventstore/repository/mock/repository.mock.impl.go +++ b/internal/eventstore/repository/mock/repository.mock.impl.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" @@ -197,8 +198,8 @@ func (e *mockEvent) Sequence() uint64 { return e.sequence } -func (e *mockEvent) Position() float64 { - return 0 +func (e *mockEvent) Position() decimal.Decimal { + return decimal.Decimal{} } func (e *mockEvent) CreatedAt() time.Time { diff --git a/internal/eventstore/repository/search_query.go b/internal/eventstore/repository/search_query.go index 6ffba31ca8..760f7f616c 100644 --- a/internal/eventstore/repository/search_query.go +++ b/internal/eventstore/repository/search_query.go @@ -3,6 +3,8 @@ package repository import ( "database/sql" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/zerrors" @@ -57,6 +59,8 @@ const ( // OperationNotIn checks if a stored value does not match one of the passed value list OperationNotIn + OperationGreaterOrEquals + operationCount ) @@ -250,10 +254,10 @@ func instanceIDsFilter(builder *eventstore.SearchQueryBuilder, query *SearchQuer } func positionAfterFilter(builder *eventstore.SearchQueryBuilder, query *SearchQuery) *Filter { - if builder.GetPositionAfter() == 0 { + if builder.GetPositionAtLeast().IsZero() { return nil } - query.Position = NewFilter(FieldPosition, builder.GetPositionAfter(), OperationGreater) + query.Position = NewFilter(FieldPosition, builder.GetPositionAtLeast(), OperationGreaterOrEquals) return query.Position } @@ -295,7 +299,7 @@ func eventDataFilter(query *eventstore.SearchQuery) *Filter { } func eventPositionAfterFilter(query *eventstore.SearchQuery) *Filter { - if pos := query.GetPositionAfter(); pos != 0 { + if pos := query.GetPositionAfter(); !pos.Equal(decimal.Decimal{}) { return NewFilter(FieldPosition, pos, OperationGreater) } return nil diff --git a/internal/eventstore/repository/sql/local_postgres_test.go b/internal/eventstore/repository/sql/local_postgres_test.go index 765da213e3..ae1f7b4831 100644 --- a/internal/eventstore/repository/sql/local_postgres_test.go +++ b/internal/eventstore/repository/sql/local_postgres_test.go @@ -7,6 +7,8 @@ import ( "testing" "time" + pgxdecimal "github.com/jackc/pgx-shopspring-decimal" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/stdlib" "github.com/zitadel/logging" @@ -30,7 +32,11 @@ func TestMain(m *testing.M) { connConfig, err := pgxpool.ParseConfig(config.GetConnectionURL()) logging.OnError(err).Fatal("unable to parse db url") - connConfig.AfterConnect = new_es.RegisterEventstoreTypes + connConfig.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { + pgxdecimal.Register(conn.TypeMap()) + return new_es.RegisterEventstoreTypes(ctx, conn) + } + pool, err := pgxpool.NewWithConfig(context.Background(), connConfig) logging.OnError(err).Fatal("unable to create db pool") diff --git a/internal/eventstore/repository/sql/postgres.go b/internal/eventstore/repository/sql/postgres.go index bc9ad2e029..0dc2210f7b 100644 --- a/internal/eventstore/repository/sql/postgres.go +++ b/internal/eventstore/repository/sql/postgres.go @@ -2,12 +2,12 @@ package sql import ( "context" - "database/sql" "errors" "regexp" "strconv" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" @@ -55,11 +55,11 @@ func (psql *Postgres) FilterToReducer(ctx context.Context, searchQuery *eventsto return err } -// LatestSequence returns the latest sequence found by the search query -func (db *Postgres) LatestSequence(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder) (float64, error) { - var position sql.NullFloat64 +// LatestPosition returns the latest position found by the search query +func (db *Postgres) LatestPosition(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder) (decimal.Decimal, error) { + var position decimal.Decimal err := query(ctx, db, searchQuery, &position, false) - return position.Float64, err + return position, err } // InstanceIDs returns the instance ids found by the search query @@ -126,7 +126,7 @@ func (db *Postgres) eventQuery(useV1 bool) string { " FROM eventstore.events2" } -func (db *Postgres) maxSequenceQuery(useV1 bool) string { +func (db *Postgres) maxPositionQuery(useV1 bool) string { if useV1 { return `SELECT event_sequence FROM eventstore.events` } @@ -207,6 +207,8 @@ func (db *Postgres) operation(operation repository.Operation) string { return "=" case repository.OperationGreater: return ">" + case repository.OperationGreaterOrEquals: + return ">=" case repository.OperationLess: return "<" case repository.OperationJSONContains: diff --git a/internal/eventstore/repository/sql/postgres_test.go b/internal/eventstore/repository/sql/postgres_test.go index 151fdd1b6a..8a9b7bc049 100644 --- a/internal/eventstore/repository/sql/postgres_test.go +++ b/internal/eventstore/repository/sql/postgres_test.go @@ -4,6 +4,8 @@ import ( "database/sql" "testing" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/repository" ) @@ -312,7 +314,7 @@ func generateEvent(t *testing.T, aggregateID string, opts ...func(*repository.Ev ResourceOwner: sql.NullString{String: "ro", Valid: true}, Typ: "test.created", Version: "v1", - Pos: 42, + Pos: decimal.NewFromInt(42), } for _, opt := range opts { diff --git a/internal/eventstore/repository/sql/query.go b/internal/eventstore/repository/sql/query.go index a545225d9e..8584a82fa0 100644 --- a/internal/eventstore/repository/sql/query.go +++ b/internal/eventstore/repository/sql/query.go @@ -9,6 +9,7 @@ import ( "strconv" "strings" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/database" @@ -24,7 +25,7 @@ type querier interface { conditionFormat(repository.Operation) string placeholder(query string) string eventQuery(useV1 bool) string - maxSequenceQuery(useV1 bool) string + maxPositionQuery(useV1 bool) string instanceIDsQuery(useV1 bool) string Client() *database.DB orderByEventSequence(desc, shouldOrderBySequence, useV1 bool) string @@ -68,7 +69,7 @@ func query(ctx context.Context, criteria querier, searchQuery *eventstore.Search // instead of using the max function of the database (which doesn't work for postgres) // we select the most recent row - if q.Columns == eventstore.ColumnsMaxSequence { + if q.Columns == eventstore.ColumnsMaxPosition { q.Limit = 1 q.Desc = true } @@ -85,7 +86,7 @@ func query(ctx context.Context, criteria querier, searchQuery *eventstore.Search switch q.Columns { case eventstore.ColumnsEvent, - eventstore.ColumnsMaxSequence: + eventstore.ColumnsMaxPosition: query += criteria.orderByEventSequence(q.Desc, shouldOrderBySequence, useV1) } @@ -141,8 +142,8 @@ func query(ctx context.Context, criteria querier, searchQuery *eventstore.Search func prepareColumns(criteria querier, columns eventstore.Columns, useV1 bool) (string, func(s scan, dest interface{}) error) { switch columns { - case eventstore.ColumnsMaxSequence: - return criteria.maxSequenceQuery(useV1), maxSequenceScanner + case eventstore.ColumnsMaxPosition: + return criteria.maxPositionQuery(useV1), maxPositionScanner case eventstore.ColumnsInstanceIDs: return criteria.instanceIDsQuery(useV1), instanceIDsScanner case eventstore.ColumnsEvent: @@ -152,13 +153,15 @@ func prepareColumns(criteria querier, columns eventstore.Columns, useV1 bool) (s } } -func maxSequenceScanner(row scan, dest any) (err error) { - position, ok := dest.(*sql.NullFloat64) +func maxPositionScanner(row scan, dest interface{}) (err error) { + position, ok := dest.(*decimal.Decimal) if !ok { - return zerrors.ThrowInvalidArgumentf(nil, "SQL-NBjA9", "type must be sql.NullInt64 got: %T", dest) + return zerrors.ThrowInvalidArgumentf(nil, "SQL-NBjA9", "type must be pointer to decimal.Decimal got: %T", dest) } - err = row(position) + var res decimal.NullDecimal + err = row(&res) if err == nil || errors.Is(err, sql.ErrNoRows) { + *position = res.Decimal return nil } return zerrors.ThrowInternal(err, "SQL-bN5xg", "something went wrong") @@ -187,7 +190,7 @@ func eventsScanner(useV1 bool) func(scanner scan, dest interface{}) (err error) return zerrors.ThrowInvalidArgumentf(nil, "SQL-4GP6F", "events scanner: invalid type %T", dest) } event := new(repository.Event) - position := new(sql.NullFloat64) + position := new(decimal.NullDecimal) if useV1 { err = scanner( @@ -224,7 +227,7 @@ func eventsScanner(useV1 bool) func(scanner scan, dest interface{}) (err error) logging.New().WithError(err).Warn("unable to scan row") return zerrors.ThrowInternal(err, "SQL-M0dsf", "unable to scan row") } - event.Pos = position.Float64 + event.Pos = position.Decimal return reduce(event) } } diff --git a/internal/eventstore/repository/sql/query_test.go b/internal/eventstore/repository/sql/query_test.go index 3df819be64..0e2425dd07 100644 --- a/internal/eventstore/repository/sql/query_test.go +++ b/internal/eventstore/repository/sql/query_test.go @@ -7,10 +7,12 @@ import ( "reflect" "regexp" "strconv" + "strings" "testing" "time" "github.com/DATA-DOG/go-sqlmock" + "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/zitadel/zitadel/internal/database" @@ -111,36 +113,36 @@ func Test_prepareColumns(t *testing.T) { { name: "max column", args: args{ - columns: eventstore.ColumnsMaxSequence, - dest: new(sql.NullFloat64), + columns: eventstore.ColumnsMaxPosition, + dest: new(decimal.Decimal), useV1: true, }, res: res{ query: `SELECT event_sequence FROM eventstore.events`, - expected: sql.NullFloat64{Float64: 43, Valid: true}, + expected: decimal.NewFromInt(42), }, fields: fields{ - dbRow: []interface{}{sql.NullFloat64{Float64: 43, Valid: true}}, + dbRow: []interface{}{decimal.NewNullDecimal(decimal.NewFromInt(42))}, }, }, { name: "max column v2", args: args{ - columns: eventstore.ColumnsMaxSequence, - dest: new(sql.NullFloat64), + columns: eventstore.ColumnsMaxPosition, + dest: new(decimal.Decimal), }, res: res{ query: `SELECT "position" FROM eventstore.events2`, - expected: sql.NullFloat64{Float64: 43, Valid: true}, + expected: decimal.NewFromInt(42), }, fields: fields{ - dbRow: []interface{}{sql.NullFloat64{Float64: 43, Valid: true}}, + dbRow: []interface{}{decimal.NewNullDecimal(decimal.NewFromInt(42))}, }, }, { name: "max sequence wrong dest type", args: args{ - columns: eventstore.ColumnsMaxSequence, + columns: eventstore.ColumnsMaxPosition, dest: new(uint64), }, res: res{ @@ -180,11 +182,11 @@ func Test_prepareColumns(t *testing.T) { res: res{ query: `SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2`, expected: []eventstore.Event{ - &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Pos: 42, Data: nil, Version: "v1"}, + &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Pos: decimal.NewFromInt(42), Data: nil, Version: "v1"}, }, }, fields: fields{ - dbRow: []interface{}{time.Time{}, eventstore.EventType(""), uint64(5), sql.NullFloat64{Float64: 42, Valid: true}, sql.RawBytes(nil), "", sql.NullString{}, "", eventstore.AggregateType("user"), "hodor", uint8(1)}, + dbRow: []interface{}{time.Time{}, eventstore.EventType(""), uint64(5), decimal.NewNullDecimal(decimal.NewFromInt(42)), sql.RawBytes(nil), "", sql.NullString{}, "", eventstore.AggregateType("user"), "hodor", uint8(1)}, }, }, { @@ -199,11 +201,11 @@ func Test_prepareColumns(t *testing.T) { res: res{ query: `SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2`, expected: []eventstore.Event{ - &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Pos: 0, Data: nil, Version: "v1"}, + &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Pos: decimal.Decimal{}, Data: nil, Version: "v1"}, }, }, fields: fields{ - dbRow: []interface{}{time.Time{}, eventstore.EventType(""), uint64(5), sql.NullFloat64{Float64: 0, Valid: false}, sql.RawBytes(nil), "", sql.NullString{}, "", eventstore.AggregateType("user"), "hodor", uint8(1)}, + dbRow: []interface{}{time.Time{}, eventstore.EventType(""), uint64(5), decimal.NullDecimal{}, sql.RawBytes(nil), "", sql.NullString{}, "", eventstore.AggregateType("user"), "hodor", uint8(1)}, }, }, { @@ -901,7 +903,7 @@ func Test_query_events_mocked(t *testing.T) { InstanceID("instanceID"). OrderDesc(). Limit(5). - PositionAfter(123.456). + PositionAtLeast(decimal.NewFromFloat(123.456)). AddQuery(). AggregateTypes("notify"). EventTypes("notify.foo.bar"). @@ -914,8 +916,8 @@ func Test_query_events_mocked(t *testing.T) { }, fields: fields{ mock: newMockClient(t).expectQuery( - regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND "position" > $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND "position" > $8) ORDER BY event_sequence DESC LIMIT $9`), - []driver.Value{"instanceID", eventstore.AggregateType("notify"), eventstore.EventType("notify.foo.bar"), 123.456, eventstore.AggregateType("notify"), []eventstore.EventType{"notification.failed", "notification.success"}, "instanceID", 123.456, uint64(5)}, + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND "position" >= $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND "position" >= $8) ORDER BY event_sequence DESC LIMIT $9`), + []driver.Value{"instanceID", eventstore.AggregateType("notify"), eventstore.EventType("notify.foo.bar"), decimal.NewFromFloat(123.456), eventstore.AggregateType("notify"), []eventstore.EventType{"notification.failed", "notification.success"}, "instanceID", decimal.NewFromFloat(123.456), uint64(5)}, ), }, res: res{ @@ -930,7 +932,7 @@ func Test_query_events_mocked(t *testing.T) { InstanceID("instanceID"). OrderDesc(). Limit(5). - PositionAfter(123.456). + PositionAtLeast(decimal.NewFromFloat(123.456)). AddQuery(). AggregateTypes("notify"). EventTypes("notify.foo.bar"). @@ -943,8 +945,8 @@ func Test_query_events_mocked(t *testing.T) { }, fields: fields{ mock: newMockClient(t).expectQuery( - regexp.QuoteMeta(`SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2 WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND "position" > $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events2 WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND "position" > $8) ORDER BY "position" DESC, in_tx_order DESC LIMIT $9`), - []driver.Value{"instanceID", eventstore.AggregateType("notify"), eventstore.EventType("notify.foo.bar"), 123.456, eventstore.AggregateType("notify"), []eventstore.EventType{"notification.failed", "notification.success"}, "instanceID", 123.456, uint64(5)}, + regexp.QuoteMeta(`SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2 WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND "position" >= $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events2 WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND "position" >= $8) ORDER BY "position" DESC, in_tx_order DESC LIMIT $9`), + []driver.Value{"instanceID", eventstore.AggregateType("notify"), eventstore.EventType("notify.foo.bar"), decimal.NewFromFloat(123.456), eventstore.AggregateType("notify"), []eventstore.EventType{"notification.failed", "notification.success"}, "instanceID", decimal.NewFromFloat(123.456), uint64(5)}, ), }, res: res{ @@ -988,6 +990,10 @@ func Test_query_events_mocked(t *testing.T) { client.DB.DB = tt.fields.mock.client } + if strings.HasPrefix(tt.name, "aggregate / event type, position and exclusion") { + t.Log("hodor") + } + err := query(context.Background(), client, tt.args.query, tt.args.dest, tt.args.useV1) if (err != nil) != tt.res.wantErr { t.Errorf("query() error = %v, wantErr %v", err, tt.res.wantErr) diff --git a/internal/eventstore/search_query.go b/internal/eventstore/search_query.go index 1596936a36..dc92f5a4de 100644 --- a/internal/eventstore/search_query.go +++ b/internal/eventstore/search_query.go @@ -5,6 +5,8 @@ import ( "database/sql" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -25,7 +27,7 @@ type SearchQueryBuilder struct { tx *sql.Tx lockRows bool lockOption LockOption - positionAfter float64 + positionAtLeast decimal.Decimal awaitOpenTransactions bool creationDateAfter time.Time creationDateBefore time.Time @@ -76,8 +78,8 @@ func (b *SearchQueryBuilder) GetTx() *sql.Tx { return b.tx } -func (b SearchQueryBuilder) GetPositionAfter() float64 { - return b.positionAfter +func (b SearchQueryBuilder) GetPositionAtLeast() decimal.Decimal { + return b.positionAtLeast } func (b SearchQueryBuilder) GetAwaitOpenTransactions() bool { @@ -113,7 +115,7 @@ type SearchQuery struct { aggregateIDs []string eventTypes []EventType eventData map[string]interface{} - positionAfter float64 + positionAfter decimal.Decimal } func (q SearchQuery) GetAggregateTypes() []AggregateType { @@ -132,7 +134,7 @@ func (q SearchQuery) GetEventData() map[string]interface{} { return q.eventData } -func (q SearchQuery) GetPositionAfter() float64 { +func (q SearchQuery) GetPositionAfter() decimal.Decimal { return q.positionAfter } @@ -156,8 +158,8 @@ type Columns int8 const ( //ColumnsEvent represents all fields of an event ColumnsEvent = iota + 1 - // ColumnsMaxSequence represents the latest sequence of the filtered events - ColumnsMaxSequence + // ColumnsMaxPosition represents the latest sequence of the filtered events + ColumnsMaxPosition // ColumnsInstanceIDs represents the instance ids of the filtered events ColumnsInstanceIDs @@ -284,9 +286,9 @@ func (builder *SearchQueryBuilder) EditorUser(id string) *SearchQueryBuilder { return builder } -// PositionAfter filters for events which happened after the specified time -func (builder *SearchQueryBuilder) PositionAfter(position float64) *SearchQueryBuilder { - builder.positionAfter = position +// PositionAtLeast filters for events which happened after the specified time +func (builder *SearchQueryBuilder) PositionAtLeast(position decimal.Decimal) *SearchQueryBuilder { + builder.positionAtLeast = position return builder } @@ -393,7 +395,7 @@ func (query *SearchQuery) EventData(data map[string]interface{}) *SearchQuery { return query } -func (query *SearchQuery) PositionAfter(position float64) *SearchQuery { +func (query *SearchQuery) PositionAfter(position decimal.Decimal) *SearchQuery { query.positionAfter = position return query } diff --git a/internal/eventstore/search_query_test.go b/internal/eventstore/search_query_test.go index b8f570dc0d..3325ee0c4b 100644 --- a/internal/eventstore/search_query_test.go +++ b/internal/eventstore/search_query_test.go @@ -106,10 +106,10 @@ func TestSearchQuerybuilderSetters(t *testing.T) { { name: "set columns", args: args{ - setters: []func(*SearchQueryBuilder) *SearchQueryBuilder{testSetColumns(ColumnsMaxSequence)}, + setters: []func(*SearchQueryBuilder) *SearchQueryBuilder{testSetColumns(ColumnsMaxPosition)}, }, res: &SearchQueryBuilder{ - columns: ColumnsMaxSequence, + columns: ColumnsMaxPosition, }, }, { diff --git a/internal/eventstore/v1/models/event.go b/internal/eventstore/v1/models/event.go index 8c50d64da0..ab2b608872 100644 --- a/internal/eventstore/v1/models/event.go +++ b/internal/eventstore/v1/models/event.go @@ -5,6 +5,8 @@ import ( "reflect" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -20,7 +22,7 @@ var _ eventstore.Event = (*Event)(nil) type Event struct { ID string Seq uint64 - Pos float64 + Pos decimal.Decimal CreationDate time.Time Typ eventstore.EventType PreviousSequence uint64 @@ -80,7 +82,7 @@ func (e *Event) Sequence() uint64 { } // Position implements [eventstore.Event] -func (e *Event) Position() float64 { +func (e *Event) Position() decimal.Decimal { return e.Pos } diff --git a/internal/eventstore/v3/event.go b/internal/eventstore/v3/event.go index 1141a9eacf..c9ea4d2c62 100644 --- a/internal/eventstore/v3/event.go +++ b/internal/eventstore/v3/event.go @@ -6,6 +6,7 @@ import ( "strconv" "time" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" @@ -42,7 +43,7 @@ type event struct { command *command createdAt time.Time sequence uint64 - position float64 + position decimal.Decimal } // TODO: remove on v3 @@ -152,8 +153,8 @@ func (e *event) Sequence() uint64 { return e.sequence } -// Sequence implements [eventstore.Event] -func (e *event) Position() float64 { +// Position implements [eventstore.Event] +func (e *event) Position() decimal.Decimal { return e.position } diff --git a/internal/notification/projections.go b/internal/notification/projections.go index 9b6b975fa1..7fda08135c 100644 --- a/internal/notification/projections.go +++ b/internal/notification/projections.go @@ -71,6 +71,26 @@ func Start(ctx context.Context) { } } +func SetCurrentState(ctx context.Context, es *eventstore.Eventstore) error { + if len(projections) == 0 { + return nil + } + position, err := es.LatestPosition(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxPosition).InstanceID(authz.GetInstance(ctx).InstanceID()).OrderDesc().Limit(1)) + if err != nil { + return err + } + + for i, projection := range projections { + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("set current state of notification projection") + _, err = projection.Trigger(ctx, handler.WithMinPosition(position)) + if err != nil { + return err + } + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("current state of notification projection set") + } + return nil +} + func ProjectInstance(ctx context.Context) error { for i, projection := range projections { logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("starting notification projection") diff --git a/internal/query/access_token.go b/internal/query/access_token.go index 0fc1bbb369..030ddda473 100644 --- a/internal/query/access_token.go +++ b/internal/query/access_token.go @@ -5,6 +5,7 @@ import ( "strings" "time" + "github.com/shopspring/decimal" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/domain" @@ -140,7 +141,7 @@ func (q *Queries) accessTokenByOIDCSessionAndTokenID(ctx context.Context, oidcSe // checkSessionNotTerminatedAfter checks if a [session.TerminateType] event (or user events leading to a session termination) // occurred after a certain time and will return an error if so. -func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID, userID string, position float64, fingerprintID string) (err error) { +func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID, userID string, position decimal.Decimal, fingerprintID string) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -165,7 +166,7 @@ func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID, } type sessionTerminatedModel struct { - position float64 + position decimal.Decimal sessionID string userID string fingerPrintID string diff --git a/internal/query/current_state.go b/internal/query/current_state.go index 6fae52713f..d0a5b369bf 100644 --- a/internal/query/current_state.go +++ b/internal/query/current_state.go @@ -10,6 +10,7 @@ import ( "time" sq "github.com/Masterminds/squirrel" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/eventstore" @@ -25,7 +26,7 @@ type Stateful interface { type State struct { LastRun time.Time - Position float64 + Position decimal.Decimal EventCreatedAt time.Time AggregateID string AggregateType eventstore.AggregateType @@ -220,7 +221,7 @@ func prepareLatestState() (sq.SelectBuilder, func(*sql.Row) (*State, error)) { var ( creationDate sql.NullTime lastUpdated sql.NullTime - position sql.NullFloat64 + position decimal.NullDecimal ) err := row.Scan( &creationDate, @@ -233,7 +234,7 @@ func prepareLatestState() (sq.SelectBuilder, func(*sql.Row) (*State, error)) { return &State{ EventCreatedAt: creationDate.Time, LastRun: lastUpdated.Time, - Position: position.Float64, + Position: position.Decimal, }, nil } } @@ -258,7 +259,7 @@ func prepareCurrentStateQuery() (sq.SelectBuilder, func(*sql.Rows) (*CurrentStat var ( lastRun sql.NullTime eventDate sql.NullTime - currentPosition sql.NullFloat64 + currentPosition decimal.NullDecimal aggregateType sql.NullString aggregateID sql.NullString sequence sql.NullInt64 @@ -279,7 +280,7 @@ func prepareCurrentStateQuery() (sq.SelectBuilder, func(*sql.Rows) (*CurrentStat } currentState.State.EventCreatedAt = eventDate.Time currentState.State.LastRun = lastRun.Time - currentState.Position = currentPosition.Float64 + currentState.Position = currentPosition.Decimal currentState.AggregateType = eventstore.AggregateType(aggregateType.String) currentState.AggregateID = aggregateID.String currentState.Sequence = uint64(sequence.Int64) diff --git a/internal/query/current_state_test.go b/internal/query/current_state_test.go index c0895dc439..29761b8cb3 100644 --- a/internal/query/current_state_test.go +++ b/internal/query/current_state_test.go @@ -7,6 +7,8 @@ import ( "fmt" "regexp" "testing" + + "github.com/shopspring/decimal" ) var ( @@ -86,7 +88,7 @@ func Test_CurrentSequencesPrepares(t *testing.T) { State: State{ EventCreatedAt: testNow, LastRun: testNow, - Position: 20211108, + Position: decimal.NewFromInt(20211108), AggregateID: "agg-id", AggregateType: "agg-type", Sequence: 20211108, @@ -133,7 +135,7 @@ func Test_CurrentSequencesPrepares(t *testing.T) { ProjectionName: "projection-name", State: State{ EventCreatedAt: testNow, - Position: 20211108, + Position: decimal.NewFromInt(20211108), LastRun: testNow, AggregateID: "agg-id", AggregateType: "agg-type", @@ -144,7 +146,7 @@ func Test_CurrentSequencesPrepares(t *testing.T) { ProjectionName: "projection-name2", State: State{ EventCreatedAt: testNow, - Position: 20211108, + Position: decimal.NewFromInt(20211108), LastRun: testNow, AggregateID: "agg-id", AggregateType: "agg-type", diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index 07953a27e8..77a28ac79a 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -2,8 +2,10 @@ package projection import ( "context" + "errors" "fmt" + "github.com/jackc/pgx/v5/pgconn" "github.com/zitadel/logging" internal_authz "github.com/zitadel/zitadel/internal/api/authz" @@ -212,11 +214,19 @@ func Start(ctx context.Context) { func ProjectInstance(ctx context.Context) error { for i, projection := range projections { logging.WithFields("name", projection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("starting projection") - _, err := projection.Trigger(ctx) - if err != nil { - return err + for { + _, err := projection.Trigger(ctx) + if err == nil { + break + } + var pgErr *pgconn.PgError + errors.As(err, &pgErr) + if pgErr.Code != database.PgUniqueConstraintErrorCode { + return err + } + logging.WithFields("name", projection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID()).WithError(err).Debug("projection failed because of unique constraint, retrying") } - logging.WithFields("name", projection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID()).Info("projection done") + logging.WithFields("name", projection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("projection done") } return nil } @@ -224,11 +234,19 @@ func ProjectInstance(ctx context.Context) error { func ProjectInstanceFields(ctx context.Context) error { for i, fieldProjection := range fields { logging.WithFields("name", fieldProjection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(fields))).Info("starting fields projection") - err := fieldProjection.Trigger(ctx) - if err != nil { - return err + for { + err := fieldProjection.Trigger(ctx) + if err == nil { + break + } + var pgErr *pgconn.PgError + errors.As(err, &pgErr) + if pgErr.Code != database.PgUniqueConstraintErrorCode { + return err + } + logging.WithFields("name", fieldProjection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID()).WithError(err).Debug("fields projection failed because of unique constraint, retrying") } - logging.WithFields("name", fieldProjection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID()).Info("fields projection done") + logging.WithFields("name", fieldProjection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(fields))).Info("fields projection done") } return nil } @@ -257,6 +275,10 @@ func applyCustomConfig(config handler.Config, customConfig CustomConfig) handler return config } +// we know this is ugly, but we need to have a singleton slice of all projections +// and are only able to initialize it after all projections are created +// as setup and start currently create them individually, we make sure we get the right one +// will be refactored when changing to new id based projections func newFieldsList() { fields = []*handler.FieldHandler{ ProjectGrantFields, diff --git a/internal/query/user_grant.go b/internal/query/user_grant.go index c3f24c066e..ebd4ab7c0c 100644 --- a/internal/query/user_grant.go +++ b/internal/query/user_grant.go @@ -280,7 +280,7 @@ func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, sh return nil, zerrors.ThrowInternal(err, "QUERY-wXnQR", "Errors.Query.SQLStatement") } - latestSequence, err := q.latestState(ctx, userGrantTable) + latestState, err := q.latestState(ctx, userGrantTable) if err != nil { return nil, err } @@ -293,7 +293,7 @@ func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, sh return nil, err } - grants.State = latestSequence + grants.State = latestState return grants, nil } diff --git a/internal/query/user_membership.go b/internal/query/user_membership.go index cae2b4dae3..cb7588624f 100644 --- a/internal/query/user_membership.go +++ b/internal/query/user_membership.go @@ -143,7 +143,7 @@ func (q *Queries) Memberships(ctx context.Context, queries *MembershipSearchQuer if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-T84X9", "Errors.Query.InvalidRequest") } - latestSequence, err := q.latestState(ctx, orgMemberTable, instanceMemberTable, projectMemberTable, projectGrantMemberTable) + latestState, err := q.latestState(ctx, orgMemberTable, instanceMemberTable, projectMemberTable, projectGrantMemberTable) if err != nil { return nil, err } @@ -156,7 +156,7 @@ func (q *Queries) Memberships(ctx context.Context, queries *MembershipSearchQuer if err != nil { return nil, err } - memberships.State = latestSequence + memberships.State = latestState return memberships, nil } diff --git a/internal/v2/database/number_filter.go b/internal/v2/database/number_filter.go index ce263ceeee..4853806457 100644 --- a/internal/v2/database/number_filter.go +++ b/internal/v2/database/number_filter.go @@ -3,6 +3,7 @@ package database import ( "time" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "golang.org/x/exp/constraints" ) @@ -94,7 +95,7 @@ func (c numberCompare) String() string { } type number interface { - constraints.Integer | constraints.Float | time.Time + constraints.Integer | constraints.Float | time.Time | decimal.Decimal // TODO: condition must know if it's args are named parameters or not // constraints.Integer | constraints.Float | time.Time | placeholder } diff --git a/internal/v2/eventstore/event_store.go b/internal/v2/eventstore/event_store.go index cc447c5e15..e89786c657 100644 --- a/internal/v2/eventstore/event_store.go +++ b/internal/v2/eventstore/event_store.go @@ -2,6 +2,8 @@ package eventstore import ( "context" + + "github.com/shopspring/decimal" ) func NewEventstore(querier Querier, pusher Pusher) *EventStore { @@ -30,12 +32,12 @@ type healthier interface { } type GlobalPosition struct { - Position float64 + Position decimal.Decimal InPositionOrder uint32 } func (gp GlobalPosition) IsLess(other GlobalPosition) bool { - return gp.Position < other.Position || (gp.Position == other.Position && gp.InPositionOrder < other.InPositionOrder) + return gp.Position.LessThan(other.Position) || (gp.Position.Equal(other.Position) && gp.InPositionOrder < other.InPositionOrder) } type Reducer interface { diff --git a/internal/v2/eventstore/postgres/push_test.go b/internal/v2/eventstore/postgres/push_test.go index bb3254427c..afd5fe8b8e 100644 --- a/internal/v2/eventstore/postgres/push_test.go +++ b/internal/v2/eventstore/postgres/push_test.go @@ -8,6 +8,8 @@ import ( "testing" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/database/mock" "github.com/zitadel/zitadel/internal/v2/eventstore" "github.com/zitadel/zitadel/internal/zerrors" @@ -818,7 +820,7 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, }, ), @@ -899,11 +901,11 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, { time.Now(), - float64(123.1), + decimal.NewFromFloat(123.1).String(), }, }, ), @@ -984,11 +986,11 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, { time.Now(), - float64(123.1), + decimal.NewFromFloat(123.1).String(), }, }, ), @@ -1044,7 +1046,7 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, }, ), @@ -1099,7 +1101,7 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, }, ), @@ -1181,11 +1183,11 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, { time.Now(), - float64(123.1), + decimal.NewFromFloat(123.1).String(), }, }, ), @@ -1272,11 +1274,11 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, { time.Now(), - float64(123.1), + decimal.NewFromFloat(123.1).String(), }, }, ), diff --git a/internal/v2/eventstore/postgres/query_test.go b/internal/v2/eventstore/postgres/query_test.go index 56f506ac50..34b73bd820 100644 --- a/internal/v2/eventstore/postgres/query_test.go +++ b/internal/v2/eventstore/postgres/query_test.go @@ -8,6 +8,8 @@ import ( "testing" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/database" "github.com/zitadel/zitadel/internal/v2/database/mock" "github.com/zitadel/zitadel/internal/v2/eventstore" @@ -541,13 +543,13 @@ func Test_writeFilter(t *testing.T) { args: args{ filter: eventstore.NewFilter( eventstore.FilterPagination( - eventstore.PositionGreater(123.4, 0), + eventstore.PositionGreater(decimal.NewFromFloat(123.4), 0), ), ), }, want: wantQuery{ query: " WHERE instance_id = $1 AND position > $2 ORDER BY position, in_tx_order", - args: []any{"i1", 123.4}, + args: []any{"i1", decimal.NewFromFloat(123.4)}, }, }, { @@ -555,18 +557,18 @@ func Test_writeFilter(t *testing.T) { args: args{ filter: eventstore.NewFilter( eventstore.FilterPagination( - // eventstore.PositionGreater(123.4, 0), + // eventstore.PositionGreater(decimal.NewFromFloat(123.4), 0), // eventstore.PositionLess(125.4, 10), eventstore.PositionBetween( - &eventstore.GlobalPosition{Position: 123.4}, - &eventstore.GlobalPosition{Position: 125.4, InPositionOrder: 10}, + &eventstore.GlobalPosition{Position: decimal.NewFromFloat(123.4)}, + &eventstore.GlobalPosition{Position: decimal.NewFromFloat(125.4), InPositionOrder: 10}, ), ), ), }, want: wantQuery{ query: " WHERE instance_id = $1 AND ((position = $2 AND in_tx_order < $3) OR position < $4) AND position > $5 ORDER BY position, in_tx_order", - args: []any{"i1", 125.4, uint32(10), 125.4, 123.4}, + args: []any{"i1", decimal.NewFromFloat(125.4), uint32(10), decimal.NewFromFloat(125.4), decimal.NewFromFloat(123.4)}, // TODO: (adlerhurst) would require some refactoring to reuse existing args // query: " WHERE instance_id = $1 AND position > $2 AND ((position = $3 AND in_tx_order < $4) OR position < $3) ORDER BY position, in_tx_order", // args: []any{"i1", 123.4, 125.4, uint32(10)}, @@ -577,13 +579,13 @@ func Test_writeFilter(t *testing.T) { args: args{ filter: eventstore.NewFilter( eventstore.FilterPagination( - eventstore.PositionGreater(123.4, 12), + eventstore.PositionGreater(decimal.NewFromFloat(123.4), 12), ), ), }, want: wantQuery{ query: " WHERE instance_id = $1 AND ((position = $2 AND in_tx_order > $3) OR position > $4) ORDER BY position, in_tx_order", - args: []any{"i1", 123.4, uint32(12), 123.4}, + args: []any{"i1", decimal.NewFromFloat(123.4), uint32(12), decimal.NewFromFloat(123.4)}, }, }, { @@ -593,13 +595,13 @@ func Test_writeFilter(t *testing.T) { eventstore.FilterPagination( eventstore.Limit(10), eventstore.Offset(3), - eventstore.PositionGreater(123.4, 12), + eventstore.PositionGreater(decimal.NewFromFloat(123.4), 12), ), ), }, want: wantQuery{ query: " WHERE instance_id = $1 AND ((position = $2 AND in_tx_order > $3) OR position > $4) ORDER BY position, in_tx_order LIMIT $5 OFFSET $6", - args: []any{"i1", 123.4, uint32(12), 123.4, uint32(10), uint32(3)}, + args: []any{"i1", decimal.NewFromFloat(123.4), uint32(12), decimal.NewFromFloat(123.4), uint32(10), uint32(3)}, }, }, { @@ -609,14 +611,14 @@ func Test_writeFilter(t *testing.T) { eventstore.FilterPagination( eventstore.Limit(10), eventstore.Offset(3), - eventstore.PositionGreater(123.4, 12), + eventstore.PositionGreater(decimal.NewFromFloat(123.4), 12), ), eventstore.AppendAggregateFilter("user"), ), }, want: wantQuery{ query: " WHERE instance_id = $1 AND aggregate_type = $2 AND ((position = $3 AND in_tx_order > $4) OR position > $5) ORDER BY position, in_tx_order LIMIT $6 OFFSET $7", - args: []any{"i1", "user", 123.4, uint32(12), 123.4, uint32(10), uint32(3)}, + args: []any{"i1", "user", decimal.NewFromFloat(123.4), uint32(12), decimal.NewFromFloat(123.4), uint32(10), uint32(3)}, }, }, { @@ -626,7 +628,7 @@ func Test_writeFilter(t *testing.T) { eventstore.FilterPagination( eventstore.Limit(10), eventstore.Offset(3), - eventstore.PositionGreater(123.4, 12), + eventstore.PositionGreater(decimal.NewFromFloat(123.4), 12), ), eventstore.AppendAggregateFilter("user"), eventstore.AppendAggregateFilter( @@ -637,7 +639,7 @@ func Test_writeFilter(t *testing.T) { }, want: wantQuery{ query: " WHERE instance_id = $1 AND (aggregate_type = $2 OR (aggregate_type = $3 AND aggregate_id = $4)) AND ((position = $5 AND in_tx_order > $6) OR position > $7) ORDER BY position, in_tx_order LIMIT $8 OFFSET $9", - args: []any{"i1", "user", "org", "o1", 123.4, uint32(12), 123.4, uint32(10), uint32(3)}, + args: []any{"i1", "user", "org", "o1", decimal.NewFromFloat(123.4), uint32(12), decimal.NewFromFloat(123.4), uint32(10), uint32(3)}, }, }, } @@ -956,7 +958,7 @@ func Test_writeQueryUse_examples(t *testing.T) { ), eventstore.FilterPagination( // used because we need to check for first login and an app which is not console - eventstore.PositionGreater(12, 4), + eventstore.PositionGreater(decimal.NewFromInt(12), 4), ), ), eventstore.NewFilter( @@ -1065,9 +1067,9 @@ func Test_writeQueryUse_examples(t *testing.T) { "instance", "user", "user.token.added", - float64(12), + decimal.NewFromInt(12), uint32(4), - float64(12), + decimal.NewFromInt(12), "instance", "instance", []string{"instance.idp.config.added", "instance.idp.oauth.added", "instance.idp.oidc.added", "instance.idp.jwt.added", "instance.idp.azure.added", "instance.idp.github.added", "instance.idp.github.enterprise.added", "instance.idp.gitlab.added", "instance.idp.gitlab.selfhosted.added", "instance.idp.google.added", "instance.idp.ldap.added", "instance.idp.config.apple.added", "instance.idp.saml.added"}, @@ -1201,7 +1203,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(23), - float64(123), + decimal.NewFromInt(123).String(), uint32(0), nil, "gigi", @@ -1235,7 +1237,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(23), - float64(123), + decimal.NewFromInt(123).String(), uint32(0), []byte(`{"name": "gigi"}`), "gigi", @@ -1269,7 +1271,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(23), - float64(123), + decimal.NewFromInt(123).String(), uint32(0), nil, "gigi", @@ -1283,7 +1285,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(24), - float64(124), + decimal.NewFromInt(124).String(), uint32(0), []byte(`{"name": "gigi"}`), "gigi", @@ -1317,7 +1319,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(23), - float64(123), + decimal.NewFromInt(123).String(), uint32(0), nil, "gigi", @@ -1331,7 +1333,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(24), - float64(124), + decimal.NewFromInt(124).String(), uint32(0), []byte(`{"name": "gigi"}`), "gigi", diff --git a/internal/v2/eventstore/query.go b/internal/v2/eventstore/query.go index c9b3cecd37..f7a30a2139 100644 --- a/internal/v2/eventstore/query.go +++ b/internal/v2/eventstore/query.go @@ -7,6 +7,8 @@ import ( "slices" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/database" ) @@ -723,7 +725,7 @@ func (pc *PositionCondition) Min() *GlobalPosition { // PositionGreater prepares the condition as follows // if inPositionOrder is set: position = AND in_tx_order > OR or position > // if inPositionOrder is NOT set: position > -func PositionGreater(position float64, inPositionOrder uint32) paginationOpt { +func PositionGreater(position decimal.Decimal, inPositionOrder uint32) paginationOpt { return func(p *Pagination) { p.ensurePosition() p.position.min = &GlobalPosition{ @@ -743,7 +745,7 @@ func GlobalPositionGreater(position *GlobalPosition) paginationOpt { // PositionLess prepares the condition as follows // if inPositionOrder is set: position = AND in_tx_order > OR or position > // if inPositionOrder is NOT set: position > -func PositionLess(position float64, inPositionOrder uint32) paginationOpt { +func PositionLess(position decimal.Decimal, inPositionOrder uint32) paginationOpt { return func(p *Pagination) { p.ensurePosition() p.position.max = &GlobalPosition{ diff --git a/internal/v2/eventstore/query_test.go b/internal/v2/eventstore/query_test.go index 00c08914c1..0f313e9560 100644 --- a/internal/v2/eventstore/query_test.go +++ b/internal/v2/eventstore/query_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/database" ) @@ -74,13 +76,13 @@ func TestPaginationOpt(t *testing.T) { name: "global position greater", args: args{ opts: []paginationOpt{ - GlobalPositionGreater(&GlobalPosition{Position: 10}), + GlobalPositionGreater(&GlobalPosition{Position: decimal.NewFromInt(10)}), }, }, want: &Pagination{ position: &PositionCondition{ min: &GlobalPosition{ - Position: 10, + Position: decimal.NewFromInt(10), InPositionOrder: 0, }, }, @@ -90,13 +92,13 @@ func TestPaginationOpt(t *testing.T) { name: "position greater", args: args{ opts: []paginationOpt{ - PositionGreater(10, 0), + PositionGreater(decimal.NewFromInt(10), 0), }, }, want: &Pagination{ position: &PositionCondition{ min: &GlobalPosition{ - Position: 10, + Position: decimal.NewFromInt(10), InPositionOrder: 0, }, }, @@ -107,13 +109,13 @@ func TestPaginationOpt(t *testing.T) { name: "position less", args: args{ opts: []paginationOpt{ - PositionLess(10, 12), + PositionLess(decimal.NewFromInt(10), 12), }, }, want: &Pagination{ position: &PositionCondition{ max: &GlobalPosition{ - Position: 10, + Position: decimal.NewFromInt(10), InPositionOrder: 12, }, }, @@ -123,13 +125,13 @@ func TestPaginationOpt(t *testing.T) { name: "global position less", args: args{ opts: []paginationOpt{ - GlobalPositionLess(&GlobalPosition{Position: 12, InPositionOrder: 24}), + GlobalPositionLess(&GlobalPosition{Position: decimal.NewFromInt(12), InPositionOrder: 24}), }, }, want: &Pagination{ position: &PositionCondition{ max: &GlobalPosition{ - Position: 12, + Position: decimal.NewFromInt(12), InPositionOrder: 24, }, }, @@ -140,19 +142,19 @@ func TestPaginationOpt(t *testing.T) { args: args{ opts: []paginationOpt{ PositionBetween( - &GlobalPosition{10, 12}, - &GlobalPosition{20, 0}, + &GlobalPosition{decimal.NewFromInt(10), 12}, + &GlobalPosition{decimal.NewFromInt(20), 0}, ), }, }, want: &Pagination{ position: &PositionCondition{ min: &GlobalPosition{ - Position: 10, + Position: decimal.NewFromInt(10), InPositionOrder: 12, }, max: &GlobalPosition{ - Position: 20, + Position: decimal.NewFromInt(20), InPositionOrder: 0, }, }, diff --git a/internal/v2/readmodel/last_successful_mirror.go b/internal/v2/readmodel/last_successful_mirror.go index 80b436b896..ca7815b2a8 100644 --- a/internal/v2/readmodel/last_successful_mirror.go +++ b/internal/v2/readmodel/last_successful_mirror.go @@ -1,6 +1,8 @@ package readmodel import ( + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/eventstore" "github.com/zitadel/zitadel/internal/v2/system" "github.com/zitadel/zitadel/internal/v2/system/mirror" @@ -8,7 +10,7 @@ import ( type LastSuccessfulMirror struct { ID string - Position float64 + Position decimal.Decimal source string } @@ -34,6 +36,7 @@ func (p *LastSuccessfulMirror) Filter() *eventstore.Filter { ), eventstore.FilterPagination( eventstore.Descending(), + eventstore.Limit(1), ), ) } @@ -53,7 +56,7 @@ func (h *LastSuccessfulMirror) Reduce(events ...*eventstore.StorageEvent) (err e func (h *LastSuccessfulMirror) reduceSucceeded(event *eventstore.StorageEvent) error { // if position is set we skip all older events - if h.Position > 0 { + if h.Position.GreaterThan(decimal.NewFromInt(0)) { return nil } diff --git a/internal/v2/system/mirror/succeeded.go b/internal/v2/system/mirror/succeeded.go index 6d0fba2c25..34d74f184f 100644 --- a/internal/v2/system/mirror/succeeded.go +++ b/internal/v2/system/mirror/succeeded.go @@ -1,6 +1,8 @@ package mirror import ( + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/eventstore" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -9,7 +11,7 @@ type succeededPayload struct { // Source is the name of the database data are mirrored from Source string `json:"source"` // Position until data will be mirrored - Position float64 `json:"position"` + Position decimal.Decimal `json:"position"` } const SucceededType = eventTypePrefix + "succeeded" @@ -38,7 +40,7 @@ func SucceededEventFromStorage(event *eventstore.StorageEvent) (e *SucceededEven }, nil } -func NewSucceededCommand(source string, position float64) *eventstore.Command { +func NewSucceededCommand(source string, position decimal.Decimal) *eventstore.Command { return &eventstore.Command{ Action: eventstore.Action[any]{ Creator: Creator, From 5e87fafadf1ecafdefc934e45eaa94055773fbb2 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Thu, 29 May 2025 20:23:43 +0200 Subject: [PATCH 08/35] docs: fix broken link (#9988) # Which Problems Are Solved Broken links on the default settings page. # How the Problems Are Solved Fixed the reference # Additional Changes # Additional Context --- docs/docs/guides/manage/console/default-settings.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/guides/manage/console/default-settings.mdx b/docs/docs/guides/manage/console/default-settings.mdx index e8e36956a1..f255d15d93 100644 --- a/docs/docs/guides/manage/console/default-settings.mdx +++ b/docs/docs/guides/manage/console/default-settings.mdx @@ -17,7 +17,7 @@ When you configure your default settings, you can set the following: - **Organizations**: A list of your organizations - [**Features**](#features): Feature Settings let you try out new features before they become generally available. You can also disable features you are not interested in. -- [**Notification settings**](#notification-providers-and-smtp): Setup Notification and Email Server settings for initialization-, verification- and other mails. Setup Twilio as SMS notification provider. +- [**Notification settings**](#notification-settings): Setup Notification and Email Server settings for initialization-, verification- and other mails. Setup Twilio as SMS notification provider. - [**Login Behavior and Access**](#login-behavior-and-access): Multifactor Authentication Options and Enforcement, Define whether Passwordless authentication methods are allowed or not, Set Login Lifetimes and advanced behavour for the login interface. - [**Identity Providers**](#identity-providers): Define IDPs which are available for all organizations - [**Password Complexity**](#password-complexity): Requirements for Passwords ex. Symbols, Numbers, min length and more. From 93a92446bfa7e7a8b38aa32125e1020ae08c64f1 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Fri, 30 May 2025 01:15:28 -0700 Subject: [PATCH 09/35] chore: update docusaurus to 3.8.0 (#9974) > [!IMPORTANT] > We need to change the ENV `VERCEL_FORCE_NO_BUILD_CACHE` to `0` which is currently `1` to enable the cache on all deployments This pull request includes several updates to the documentation and benchmarking components, focusing on improving performance, error handling, and compatibility with newer versions of Docusaurus. The key changes include the removal of outdated configurations, updates to dependencies, and enhancements to the `BenchmarkChart` component for better error handling and data validation. ### Documentation and Configuration Updates: * **Removed outdated Babel and Webpack configurations**: The `babel.config.js` file was deleted, and the Webpack configuration was removed from `docusaurus.config.js` to align with the latest Docusaurus setup. [[1]](diffhunk://#diff-2ed4f5b03d34a87ef641e9e36af4a98a1c0ddaf74d07ce93665957be69b7b09aL1-L4) [[2]](diffhunk://#diff-28742c737e523f302e6de471b7fc27284dc8cf720be639e6afe4c17a550cd654L204-L225) * **Added experimental features in Docusaurus**: Introduced a `future` section in `docusaurus.config.js` to enable experimental features like `swcJsLoader`, `rspackBundler`, and `lightningCssMinimizer`, while disabling problematic settings due to known issues. ### Dependency Updates: * **Upgraded Docusaurus and related packages**: Updated dependencies in `package.json` to use Docusaurus version `^3.8.0` and newer versions of associated plugins and themes for improved performance and compatibility. [[1]](diffhunk://#diff-adfa337ce44dc2902621da20152a048dac41878cf3716dfc4cc56d03aa212a56L25-R39) [[2]](diffhunk://#diff-adfa337ce44dc2902621da20152a048dac41878cf3716dfc4cc56d03aa212a56L66-R67) ### Component Enhancements: * **Improved `BenchmarkChart` error handling**: Refactored the `BenchmarkChart` component to validate input data, handle errors gracefully, and provide meaningful fallback messages when data is missing or invalid. [[1]](diffhunk://#diff-ce9fccf51f6b863dd58a39f361a9cf980b10357bccc7381f928788483b30cb0eL4-R21) [[2]](diffhunk://#diff-ce9fccf51f6b863dd58a39f361a9cf980b10357bccc7381f928788483b30cb0eR72-R76) * **Fixed edge cases in chart rendering**: Addressed issues like invalid timestamps, undefined `p99` values, and empty data sets to ensure robust chart generation. [[1]](diffhunk://#diff-ce9fccf51f6b863dd58a39f361a9cf980b10357bccc7381f928788483b30cb0eL19-L29) [[2]](diffhunk://#diff-ce9fccf51f6b863dd58a39f361a9cf980b10357bccc7381f928788483b30cb0eL38-R61) ### Documentation Benchmark Updates: * **Simplified imports in benchmark files**: Replaced the use of `raw-loader` with direct imports for benchmark data in multiple `.mdx` files to streamline the documentation setup. [[1]](diffhunk://#diff-a9710709396e5ff6756aedf89dfcbd62aeea15368ba33bf3932ebf33046a29e8L66-R66) [[2]](diffhunk://#diff-0a9b6103c97c58792450bfd2d337bbb8a6b72df2ae326cc56ebc96e01c0acd6bL35-R35) [[3]](diffhunk://#diff-38f45388e065c57f1282a43bb319354da3c218e96d95ca20f4d11709f48491b8L36-R36) [[4]](diffhunk://#diff-b8e792ebe42fcb16a493e35d23b58a91c2117d949953487e70f379c64e5cb7c0L36-R36) [[5]](diffhunk://#diff-3778acfa893504004008b162fa95f21f1c7c40dcf1868bbbaaa504ac5d51901aL38-R38) --- docs/babel.config.js | 4 - docs/docs/apis/benchmarks/_template.mdx | 2 +- .../machine_jwt_profile_grant/index.mdx | 2 +- .../machine_jwt_profile_grant/index.mdx | 2 +- .../machine_jwt_profile_grant/index.mdx | 2 +- .../benchmarks/v2.70.0/oidc_session/index.mdx | 2 +- docs/docusaurus.config.js | 35 +- docs/package.json | 30 +- docs/src/components/benchmark_chart.jsx | 32 +- docs/yarn.lock | 5297 ++++++++++++----- 10 files changed, 3794 insertions(+), 1614 deletions(-) delete mode 100644 docs/babel.config.js diff --git a/docs/babel.config.js b/docs/babel.config.js deleted file mode 100644 index 279a0ff91c..0000000000 --- a/docs/babel.config.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - presets: [require.resolve("@docusaurus/core/lib/babel/preset")], - compact: auto -}; diff --git a/docs/docs/apis/benchmarks/_template.mdx b/docs/docs/apis/benchmarks/_template.mdx index f015d20768..578ebcd842 100644 --- a/docs/docs/apis/benchmarks/_template.mdx +++ b/docs/docs/apis/benchmarks/_template.mdx @@ -63,7 +63,7 @@ TODO: describe the outcome of the test? ## Endpoint latencies -import OutputSource from "!!raw-loader!./output.json"; +import OutputSource from "./output.json"; import { BenchmarkChart } from '/src/components/benchmark_chart'; diff --git a/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index.mdx b/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index.mdx index 4c2809feb4..a8c10780ad 100644 --- a/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index.mdx +++ b/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index.mdx @@ -32,7 +32,7 @@ Tests are halted after this test run because of too many [client read events](ht ## /token endpoint latencies -import OutputSource from "!!raw-loader!./output.json"; +import OutputSource from "./output.json"; import { BenchmarkChart } from '/src/components/benchmark_chart'; diff --git a/docs/docs/apis/benchmarks/v2.66.0/machine_jwt_profile_grant/index.mdx b/docs/docs/apis/benchmarks/v2.66.0/machine_jwt_profile_grant/index.mdx index 881e7a38ee..6ab40eb4d4 100644 --- a/docs/docs/apis/benchmarks/v2.66.0/machine_jwt_profile_grant/index.mdx +++ b/docs/docs/apis/benchmarks/v2.66.0/machine_jwt_profile_grant/index.mdx @@ -33,7 +33,7 @@ The tests showed heavy database load by time by the first two database queries. ## Endpoint latencies -import OutputSource from "!!raw-loader!./output.json"; +import OutputSource from "./output.json"; import { BenchmarkChart } from '/src/components/benchmark_chart'; diff --git a/docs/docs/apis/benchmarks/v2.70.0/machine_jwt_profile_grant/index.mdx b/docs/docs/apis/benchmarks/v2.70.0/machine_jwt_profile_grant/index.mdx index fa8c84bc7e..d4fd2708a8 100644 --- a/docs/docs/apis/benchmarks/v2.70.0/machine_jwt_profile_grant/index.mdx +++ b/docs/docs/apis/benchmarks/v2.70.0/machine_jwt_profile_grant/index.mdx @@ -33,7 +33,7 @@ The performance goals of [this issue](https://github.com/zitadel/zitadel/issues/ ## Endpoint latencies -import OutputSource from "!!raw-loader!./output.json"; +import OutputSource from "./output.json"; import { BenchmarkChart } from '/src/components/benchmark_chart'; diff --git a/docs/docs/apis/benchmarks/v2.70.0/oidc_session/index.mdx b/docs/docs/apis/benchmarks/v2.70.0/oidc_session/index.mdx index 4615413d2b..94fd83f119 100644 --- a/docs/docs/apis/benchmarks/v2.70.0/oidc_session/index.mdx +++ b/docs/docs/apis/benchmarks/v2.70.0/oidc_session/index.mdx @@ -35,7 +35,7 @@ The tests showed that querying the user takes too much time because Zitadel ensu ## Endpoint latencies -import OutputSource from "!!raw-loader!./output.json"; +import OutputSource from "./output.json"; import { BenchmarkChart } from '/src/components/benchmark_chart'; diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 22df468475..c161d38d9f 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -201,28 +201,6 @@ module.exports = { runmeLinkLabel: 'Checkout via Runme' }, }, - webpack: { - jsLoader: (isServer) => ({ - loader: require.resolve('swc-loader'), - options: { - jsc: { - parser: { - syntax: 'typescript', - tsx: true, - }, - transform: { - react: { - runtime: 'automatic', - }, - }, - target: 'es2017', - }, - module: { - type: isServer ? 'commonjs' : 'es6', - }, - }, - }), - }, presets: [ [ "classic", @@ -397,4 +375,17 @@ module.exports = { }, ], themes: [ "docusaurus-theme-github-codeblock", "docusaurus-theme-openapi-docs"], + future: { + v4: false, // Disabled because of some problems related to https://github.com/facebook/docusaurus/issues/11040 + experimental_faster: { + swcJsLoader: false, // Disabled because of memory usage > 8GB which is a problem on vercel default runners + swcJsMinimizer: true, + swcHtmlMinimizer : true, + lightningCssMinimizer: true, + mdxCrossCompilerCache: true, + ssgWorkerThreads: false, // Disabled because of some problems related to https://github.com/facebook/docusaurus/issues/11040 + rspackBundler: true, + rspackPersistentCache: true, + }, + }, }; diff --git a/docs/package.json b/docs/package.json index f9636418dd..014a8ec0ca 100644 --- a/docs/package.json +++ b/docs/package.json @@ -6,7 +6,7 @@ "docusaurus": "docusaurus", "start": "docusaurus start", "start:api": "yarn run generate && docusaurus start", - "build": "yarn run generate && NODE_OPTIONS=--max-old-space-size=8192 docusaurus build", + "build": "yarn run generate && docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", @@ -22,33 +22,27 @@ }, "dependencies": { "@bufbuild/buf": "^1.14.0", - "@docusaurus/core": "3.4.0", - "@docusaurus/preset-classic": "3.4.0", - "@docusaurus/theme-mermaid": "3.4.0", - "@docusaurus/theme-search-algolia": "3.4.0", + "@docusaurus/core": "^3.8.0", + "@docusaurus/faster": "^3.8.0", + "@docusaurus/preset-classic": "^3.8.0", + "@docusaurus/theme-mermaid": "^3.8.0", + "@docusaurus/theme-search-algolia": "^3.8.0", "@headlessui/react": "^1.7.4", "@heroicons/react": "^2.0.13", - "@mdx-js/react": "^3.0.0", - "@swc/core": "^1.3.74", "autoprefixer": "^10.4.13", "clsx": "^1.2.1", - "docusaurus-plugin-image-zoom": "^1.0.1", - "docusaurus-plugin-openapi-docs": "3.0.1", + "docusaurus-plugin-image-zoom": "^3.0.1", + "docusaurus-plugin-openapi-docs": "4.4.0", "docusaurus-theme-github-codeblock": "^2.0.2", - "docusaurus-theme-openapi-docs": "3.0.1", + "docusaurus-theme-openapi-docs": "4.4.0", "mdx-mermaid": "^2.0.0", - "mermaid": "^10.9.1", "postcss": "^8.4.31", - "prism-react-renderer": "^2.1.0", "raw-loader": "^4.0.2", "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.2.0", "react-google-charts": "^5.2.1", - "react-player": "^2.15.1", - "sitemap": "7.1.1", - "swc-loader": "^0.2.3", - "wait-on": "6.0.1" + "react-player": "^2.15.1" }, "browserslist": { "production": [ @@ -63,8 +57,8 @@ ] }, "devDependencies": { - "@docusaurus/module-type-aliases": "3.4.0", - "@docusaurus/types": "3.4.0", + "@docusaurus/module-type-aliases": "^3.8.0", + "@docusaurus/types": "^3.8.0", "tailwindcss": "^3.2.4" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" diff --git a/docs/src/components/benchmark_chart.jsx b/docs/src/components/benchmark_chart.jsx index 4f0d4bc61c..cf93a842ef 100644 --- a/docs/src/components/benchmark_chart.jsx +++ b/docs/src/components/benchmark_chart.jsx @@ -1,11 +1,24 @@ import React from "react"; import Chart from "react-google-charts"; -export function BenchmarkChart(testResults=[], height='500px') { +export function BenchmarkChart({ testResults = [], height = '500px' } = {}) { + if (!Array.isArray(testResults)) { + console.error("BenchmarkChart: testResults is not an array. Received:", testResults); + return

Error: Benchmark data is not available or in the wrong format.

; + } + + if (testResults.length === 0) { + return

No benchmark data to display.

; + } + const dataPerMetric = new Map(); let maxVValue = 0; - JSON.parse(testResults.testResults).forEach((result) => { + testResults.forEach((result) => { + if (!result || typeof result.metric_name === 'undefined') { + console.warn("BenchmarkChart: Skipping invalid result item:", result); + return; + } if (!dataPerMetric.has(result.metric_name)) { dataPerMetric.set(result.metric_name, [ [ @@ -16,17 +29,16 @@ export function BenchmarkChart(testResults=[], height='500px') { ], ]); } - if (result.p99 > maxVValue) { + if (result.p99 !== undefined && result.p99 > maxVValue) { maxVValue = result.p99; } dataPerMetric.get(result.metric_name).push([ - new Date(result.timestamp), + result.timestamp ? new Date(result.timestamp) : null, result.p50, result.p95, result.p99, ]); }); - const options = { legend: { position: 'bottom' }, focusTarget: 'category', @@ -35,17 +47,18 @@ export function BenchmarkChart(testResults=[], height='500px') { }, vAxis: { title: 'latency (ms)', - maxValue: maxVValue, + maxValue: maxVValue > 0 ? maxVValue : undefined, }, title: '' }; const charts = []; dataPerMetric.forEach((data, metric) => { - const opt = Object.create(options); + const opt = { ...options }; opt.title = metric; charts.push( No chart data could be generated.

; + } - return (charts); + return <>{charts}; } \ No newline at end of file diff --git a/docs/yarn.lock b/docs/yarn.lock index ad31e03b5e..70f2de1f05 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -2,158 +2,153 @@ # yarn lockfile v1 -"@algolia/autocomplete-core@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz#1d56482a768c33aae0868c8533049e02e8961be7" - integrity sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw== +"@algolia/autocomplete-core@1.17.9": + version "1.17.9" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.17.9.tgz#83374c47dc72482aa45d6b953e89377047f0dcdc" + integrity sha512-O7BxrpLDPJWWHv/DLA9DRFWs+iY1uOJZkqUwjS5HSZAGcl0hIVCQ97LTLewiZmZ402JYUrun+8NqFP+hCknlbQ== dependencies: - "@algolia/autocomplete-plugin-algolia-insights" "1.9.3" - "@algolia/autocomplete-shared" "1.9.3" + "@algolia/autocomplete-plugin-algolia-insights" "1.17.9" + "@algolia/autocomplete-shared" "1.17.9" -"@algolia/autocomplete-plugin-algolia-insights@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.9.3.tgz#9b7f8641052c8ead6d66c1623d444cbe19dde587" - integrity sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg== +"@algolia/autocomplete-plugin-algolia-insights@1.17.9": + version "1.17.9" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.9.tgz#74c86024d09d09e8bfa3dd90b844b77d9f9947b6" + integrity sha512-u1fEHkCbWF92DBeB/KHeMacsjsoI0wFhjZtlCq2ddZbAehshbZST6Hs0Avkc0s+4UyBGbMDnSuXHLuvRWK5iDQ== dependencies: - "@algolia/autocomplete-shared" "1.9.3" + "@algolia/autocomplete-shared" "1.17.9" -"@algolia/autocomplete-preset-algolia@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.9.3.tgz#64cca4a4304cfcad2cf730e83067e0c1b2f485da" - integrity sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA== +"@algolia/autocomplete-preset-algolia@1.17.9": + version "1.17.9" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.9.tgz#911f3250544eb8ea4096fcfb268f156b085321b5" + integrity sha512-Na1OuceSJeg8j7ZWn5ssMu/Ax3amtOwk76u4h5J4eK2Nx2KB5qt0Z4cOapCsxot9VcEN11ADV5aUSlQF4RhGjQ== dependencies: - "@algolia/autocomplete-shared" "1.9.3" + "@algolia/autocomplete-shared" "1.17.9" -"@algolia/autocomplete-shared@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz#2e22e830d36f0a9cf2c0ccd3c7f6d59435b77dfa" - integrity sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ== +"@algolia/autocomplete-shared@1.17.9": + version "1.17.9" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.9.tgz#5f38868f7cb1d54b014b17a10fc4f7e79d427fa8" + integrity sha512-iDf05JDQ7I0b7JEA/9IektxN/80a2MZ1ToohfmNS3rfeuQnIKI3IJlIafD0xu4StbtQTghx9T3Maa97ytkXenQ== -"@algolia/cache-browser-local-storage@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.23.3.tgz#0cc26b96085e1115dac5fcb9d826651ba57faabc" - integrity sha512-vRHXYCpPlTDE7i6UOy2xE03zHF2C8MEFjPN2v7fRbqVpcOvAUQK81x3Kc21xyb5aSIpYCjWCZbYZuz8Glyzyyg== +"@algolia/client-abtesting@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-abtesting/-/client-abtesting-5.25.0.tgz#012204f1614e1a71366fb1e117c8f195186ff081" + integrity sha512-1pfQulNUYNf1Tk/svbfjfkLBS36zsuph6m+B6gDkPEivFmso/XnRgwDvjAx80WNtiHnmeNjIXdF7Gos8+OLHqQ== dependencies: - "@algolia/cache-common" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/cache-common@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.23.3.tgz#3bec79092d512a96c9bfbdeec7cff4ad36367166" - integrity sha512-h9XcNI6lxYStaw32pHpB1TMm0RuxphF+Ik4o7tcQiodEdpKK+wKufY6QXtba7t3k8eseirEMVB83uFFF3Nu54A== - -"@algolia/cache-in-memory@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.23.3.tgz#3945f87cd21ffa2bec23890c85305b6b11192423" - integrity sha512-yvpbuUXg/+0rbcagxNT7un0eo3czx2Uf0y4eiR4z4SD7SiptwYTpbuS0IHxcLHG3lq22ukx1T6Kjtk/rT+mqNg== +"@algolia/client-analytics@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-5.25.0.tgz#eba015bfafb3dbb82712c9160a00717a5974ff71" + integrity sha512-AFbG6VDJX/o2vDd9hqncj1B6B4Tulk61mY0pzTtzKClyTDlNP0xaUiEKhl6E7KO9I/x0FJF5tDCm0Hn6v5x18A== dependencies: - "@algolia/cache-common" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/client-account@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.23.3.tgz#8751bbf636e6741c95e7c778488dee3ee430ac6f" - integrity sha512-hpa6S5d7iQmretHHF40QGq6hz0anWEHGlULcTIT9tbUssWUriN9AUXIFQ8Ei4w9azD0hc1rUok9/DeQQobhQMA== - dependencies: - "@algolia/client-common" "4.23.3" - "@algolia/client-search" "4.23.3" - "@algolia/transporter" "4.23.3" +"@algolia/client-common@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-5.25.0.tgz#2def8947efe849266057d92f67d1b8d83de0c005" + integrity sha512-il1zS/+Rc6la6RaCdSZ2YbJnkQC6W1wiBO8+SH+DE6CPMWBU6iDVzH0sCKSAtMWl9WBxoN6MhNjGBnCv9Yy2bA== -"@algolia/client-analytics@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.23.3.tgz#f88710885278fe6fb6964384af59004a5a6f161d" - integrity sha512-LBsEARGS9cj8VkTAVEZphjxTjMVCci+zIIiRhpFun9jGDUlS1XmhCW7CTrnaWeIuCQS/2iPyRqSy1nXPjcBLRA== +"@algolia/client-insights@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-insights/-/client-insights-5.25.0.tgz#b87df8614b96c4cc9c9aa7765cce07fa70864fa8" + integrity sha512-blbjrUH1siZNfyCGeq0iLQu00w3a4fBXm0WRIM0V8alcAPo7rWjLbMJMrfBtzL9X5ic6wgxVpDADXduGtdrnkw== dependencies: - "@algolia/client-common" "4.23.3" - "@algolia/client-search" "4.23.3" - "@algolia/requester-common" "4.23.3" - "@algolia/transporter" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/client-common@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.23.3.tgz#891116aa0db75055a7ecc107649f7f0965774704" - integrity sha512-l6EiPxdAlg8CYhroqS5ybfIczsGUIAC47slLPOMDeKSVXYG1n0qGiz4RjAHLw2aD0xzh2EXZ7aRguPfz7UKDKw== +"@algolia/client-personalization@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-5.25.0.tgz#74b041f0e7d91e1009c131c8d716c34e4d45c30f" + integrity sha512-aywoEuu1NxChBcHZ1pWaat0Plw7A8jDMwjgRJ00Mcl7wGlwuPt5dJ/LTNcg3McsEUbs2MBNmw0ignXBw9Tbgow== dependencies: - "@algolia/requester-common" "4.23.3" - "@algolia/transporter" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/client-personalization@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-4.23.3.tgz#35fa8e5699b0295fbc400a8eb211dc711e5909db" - integrity sha512-3E3yF3Ocr1tB/xOZiuC3doHQBQ2zu2MPTYZ0d4lpfWads2WTKG7ZzmGnsHmm63RflvDeLK/UVx7j2b3QuwKQ2g== +"@algolia/client-query-suggestions@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-query-suggestions/-/client-query-suggestions-5.25.0.tgz#e92d935d9e2994f790d43c64d3518d81070a3888" + integrity sha512-a/W2z6XWKjKjIW1QQQV8PTTj1TXtaKx79uR3NGBdBdGvVdt24KzGAaN7sCr5oP8DW4D3cJt44wp2OY/fZcPAVA== dependencies: - "@algolia/client-common" "4.23.3" - "@algolia/requester-common" "4.23.3" - "@algolia/transporter" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/client-search@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.23.3.tgz#a3486e6af13a231ec4ab43a915a1f318787b937f" - integrity sha512-P4VAKFHqU0wx9O+q29Q8YVuaowaZ5EM77rxfmGnkHUJggh28useXQdopokgwMeYw2XUht49WX5RcTQ40rZIabw== +"@algolia/client-search@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-5.25.0.tgz#dc38ca1015f2f4c9f5053a4517f96fb28a2117f8" + integrity sha512-9rUYcMIBOrCtYiLX49djyzxqdK9Dya/6Z/8sebPn94BekT+KLOpaZCuc6s0Fpfq7nx5J6YY5LIVFQrtioK9u0g== dependencies: - "@algolia/client-common" "4.23.3" - "@algolia/requester-common" "4.23.3" - "@algolia/transporter" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" "@algolia/events@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@algolia/events/-/events-4.0.1.tgz#fd39e7477e7bc703d7f893b556f676c032af3950" integrity sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ== -"@algolia/logger-common@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.23.3.tgz#35c6d833cbf41e853a4f36ba37c6e5864920bfe9" - integrity sha512-y9kBtmJwiZ9ZZ+1Ek66P0M68mHQzKRxkW5kAAXYN/rdzgDN0d2COsViEFufxJ0pb45K4FRcfC7+33YB4BLrZ+g== - -"@algolia/logger-console@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.23.3.tgz#30f916781826c4db5f51fcd9a8a264a06e136985" - integrity sha512-8xoiseoWDKuCVnWP8jHthgaeobDLolh00KJAdMe9XPrWPuf1by732jSpgy2BlsLTaT9m32pHI8CRfrOqQzHv3A== +"@algolia/ingestion@1.25.0": + version "1.25.0" + resolved "https://registry.yarnpkg.com/@algolia/ingestion/-/ingestion-1.25.0.tgz#4d13c56dda0a05c7bacb0e3ef5866292dfd86ed5" + integrity sha512-jJeH/Hk+k17Vkokf02lkfYE4A+EJX+UgnMhTLR/Mb+d1ya5WhE+po8p5a/Nxb6lo9OLCRl6w3Hmk1TX1e9gVbQ== dependencies: - "@algolia/logger-common" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/recommend@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-4.23.3.tgz#53d4f194d22d9c72dc05f3f7514c5878f87c5890" - integrity sha512-9fK4nXZF0bFkdcLBRDexsnGzVmu4TSYZqxdpgBW2tEyfuSSY54D4qSRkLmNkrrz4YFvdh2GM1gA8vSsnZPR73w== +"@algolia/monitoring@1.25.0": + version "1.25.0" + resolved "https://registry.yarnpkg.com/@algolia/monitoring/-/monitoring-1.25.0.tgz#d59360cfe556338519d05a9d8107147e9dbcb020" + integrity sha512-Ls3i1AehJ0C6xaHe7kK9vPmzImOn5zBg7Kzj8tRYIcmCWVyuuFwCIsbuIIz/qzUf1FPSWmw0TZrGeTumk2fqXg== dependencies: - "@algolia/cache-browser-local-storage" "4.23.3" - "@algolia/cache-common" "4.23.3" - "@algolia/cache-in-memory" "4.23.3" - "@algolia/client-common" "4.23.3" - "@algolia/client-search" "4.23.3" - "@algolia/logger-common" "4.23.3" - "@algolia/logger-console" "4.23.3" - "@algolia/requester-browser-xhr" "4.23.3" - "@algolia/requester-common" "4.23.3" - "@algolia/requester-node-http" "4.23.3" - "@algolia/transporter" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/requester-browser-xhr@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.23.3.tgz#9e47e76f60d540acc8b27b4ebc7a80d1b41938b9" - integrity sha512-jDWGIQ96BhXbmONAQsasIpTYWslyjkiGu0Quydjlowe+ciqySpiDUrJHERIRfELE5+wFc7hc1Q5hqjGoV7yghw== +"@algolia/recommend@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-5.25.0.tgz#b96f12c85aa74a0326982c7801fcd4a610b420f4" + integrity sha512-79sMdHpiRLXVxSjgw7Pt4R1aNUHxFLHiaTDnN2MQjHwJ1+o3wSseb55T9VXU4kqy3m7TUme3pyRhLk5ip/S4Mw== dependencies: - "@algolia/requester-common" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/requester-common@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.23.3.tgz#7dbae896e41adfaaf1d1fa5f317f83a99afb04b3" - integrity sha512-xloIdr/bedtYEGcXCiF2muajyvRhwop4cMZo+K2qzNht0CMzlRkm8YsDdj5IaBhshqfgmBb3rTg4sL4/PpvLYw== - -"@algolia/requester-node-http@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.23.3.tgz#c9f94a5cb96a15f48cea338ab6ef16bbd0ff989f" - integrity sha512-zgu++8Uj03IWDEJM3fuNl34s746JnZOWn1Uz5taV1dFyJhVM/kTNw9Ik7YJWiUNHJQXcaD8IXD1eCb0nq/aByA== +"@algolia/requester-browser-xhr@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.25.0.tgz#c194fa5f49206b9343e6646c41bfbca2a3f2ac54" + integrity sha512-JLaF23p1SOPBmfEqozUAgKHQrGl3z/Z5RHbggBu6s07QqXXcazEsub5VLonCxGVqTv6a61AAPr8J1G5HgGGjEw== dependencies: - "@algolia/requester-common" "4.23.3" + "@algolia/client-common" "5.25.0" -"@algolia/transporter@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.23.3.tgz#545b045b67db3850ddf0bbecbc6c84ff1f3398b7" - integrity sha512-Wjl5gttqnf/gQKJA+dafnD0Y6Yw97yvfY8R9h0dQltX1GXTgNs1zWgvtWW0tHl1EgMdhAyw189uWiZMnL3QebQ== +"@algolia/requester-fetch@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-fetch/-/requester-fetch-5.25.0.tgz#231a2d0da2397d141f80b8f28e2cb6e3d219d38d" + integrity sha512-rtzXwqzFi1edkOF6sXxq+HhmRKDy7tz84u0o5t1fXwz0cwx+cjpmxu/6OQKTdOJFS92JUYHsG51Iunie7xbqfQ== dependencies: - "@algolia/cache-common" "4.23.3" - "@algolia/logger-common" "4.23.3" - "@algolia/requester-common" "4.23.3" + "@algolia/client-common" "5.25.0" + +"@algolia/requester-node-http@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-5.25.0.tgz#0ce13c550890de21c558b04381535d2d245a3725" + integrity sha512-ZO0UKvDyEFvyeJQX0gmZDQEvhLZ2X10K+ps6hViMo1HgE2V8em00SwNsQ+7E/52a+YiBkVWX61pJJJE44juDMQ== + dependencies: + "@algolia/client-common" "5.25.0" "@alloc/quick-lru@^5.2.0": version "5.2.0" @@ -168,6 +163,19 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" +"@antfu/install-pkg@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@antfu/install-pkg/-/install-pkg-1.1.0.tgz#78fa036be1a6081b5a77a5cf59f50c7752b6ba26" + integrity sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ== + dependencies: + package-manager-detector "^1.3.0" + tinyexec "^1.0.1" + +"@antfu/utils@^8.1.0": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@antfu/utils/-/utils-8.1.1.tgz#95b1947d292a9a2efffba2081796dcaa05ecedfb" + integrity sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ== + "@apidevtools/json-schema-ref-parser@^11.5.4": version "11.6.4" resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.6.4.tgz#0f3e02302f646471d621a8850e6a346d63c8ebd4" @@ -177,7 +185,7 @@ "@types/json-schema" "^7.0.15" js-yaml "^4.1.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.24.7", "@babel/code-frame@^7.8.3": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== @@ -194,12 +202,26 @@ js-tokens "^4.0.0" picocolors "^1.0.0" +"@babel/code-frame@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" + integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== + dependencies: + "@babel/helper-validator-identifier" "^7.27.1" + js-tokens "^4.0.0" + picocolors "^1.1.1" + "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.7.tgz#d23bbea508c3883ba8251fb4164982c36ea577ed" integrity sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw== -"@babel/core@^7.21.3", "@babel/core@^7.23.3": +"@babel/compat-data@^7.27.2": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.27.3.tgz#cc49c2ac222d69b889bf34c795f537c0c6311111" + integrity sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw== + +"@babel/core@^7.21.3": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.7.tgz#b676450141e0b52a3d43bc91da86aa608f950ac4" integrity sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g== @@ -220,7 +242,28 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.23.3", "@babel/generator@^7.24.7": +"@babel/core@^7.25.9": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.27.3.tgz#d7d05502bccede3cab36373ed142e6a1df554c2f" + integrity sha512-hyrN8ivxfvJ4i0fIJuV4EOlV0WDMz5Ui4StRTgVaAvWeiRCilXgwVvxJKtFQ3TKtHgJscB2YiXKGNJuVwhQMtA== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.27.3" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-module-transforms" "^7.27.3" + "@babel/helpers" "^7.27.3" + "@babel/parser" "^7.27.3" + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.27.3" + "@babel/types" "^7.27.3" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.7.tgz#1654d01de20ad66b4b4d99c135471bc654c55e6d" integrity sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA== @@ -230,6 +273,17 @@ "@jridgewell/trace-mapping" "^0.3.25" jsesc "^2.5.1" +"@babel/generator@^7.25.9", "@babel/generator@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.27.3.tgz#ef1c0f7cfe3b5fc8cbb9f6cc69f93441a68edefc" + integrity sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q== + dependencies: + "@babel/parser" "^7.27.3" + "@babel/types" "^7.27.3" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + "@babel/helper-annotate-as-pure@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz#5373c7bc8366b12a033b4be1ac13a206c6656aab" @@ -237,6 +291,13 @@ dependencies: "@babel/types" "^7.24.7" +"@babel/helper-annotate-as-pure@^7.27.1": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" + integrity sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg== + dependencies: + "@babel/types" "^7.27.3" + "@babel/helper-builder-binary-assignment-operator-visitor@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz#37d66feb012024f2422b762b9b2a7cfe27c7fba3" @@ -256,6 +317,17 @@ lru-cache "^5.1.1" semver "^6.3.1" +"@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d" + integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ== + dependencies: + "@babel/compat-data" "^7.27.2" + "@babel/helper-validator-option" "^7.27.1" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + "@babel/helper-create-class-features-plugin@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz#2eaed36b3a1c11c53bdf80d53838b293c52f5b3b" @@ -271,6 +343,19 @@ "@babel/helper-split-export-declaration" "^7.24.7" semver "^6.3.1" +"@babel/helper-create-class-features-plugin@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz#5bee4262a6ea5ddc852d0806199eb17ca3de9281" + integrity sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-member-expression-to-functions" "^7.27.1" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/traverse" "^7.27.1" + semver "^6.3.1" + "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz#be4f435a80dc2b053c76eeb4b7d16dd22cfc89da" @@ -280,6 +365,15 @@ regexpu-core "^5.3.1" semver "^6.3.1" +"@babel/helper-create-regexp-features-plugin@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz#05b0882d97ba1d4d03519e4bce615d70afa18c53" + integrity sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + regexpu-core "^6.2.0" + semver "^6.3.1" + "@babel/helper-define-polyfill-provider@^0.6.1", "@babel/helper-define-polyfill-provider@^0.6.2": version "0.6.2" resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz#18594f789c3594acb24cfdb4a7f7b7d2e8bd912d" @@ -291,6 +385,17 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" +"@babel/helper-define-polyfill-provider@^0.6.3": + version "0.6.4" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz#15e8746368bfa671785f5926ff74b3064c291fab" + integrity sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw== + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + "@babel/helper-environment-visitor@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz#4b31ba9551d1f90781ba83491dd59cf9b269f7d9" @@ -321,6 +426,14 @@ "@babel/traverse" "^7.24.7" "@babel/types" "^7.24.7" +"@babel/helper-member-expression-to-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz#ea1211276be93e798ce19037da6f06fbb994fa44" + integrity sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + "@babel/helper-module-imports@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b" @@ -329,6 +442,14 @@ "@babel/traverse" "^7.24.7" "@babel/types" "^7.24.7" +"@babel/helper-module-imports@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" + integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + "@babel/helper-module-transforms@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz#31b6c9a2930679498db65b685b1698bfd6c7daf8" @@ -340,6 +461,15 @@ "@babel/helper-split-export-declaration" "^7.24.7" "@babel/helper-validator-identifier" "^7.24.7" +"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz#db0bbcfba5802f9ef7870705a7ef8788508ede02" + integrity sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.27.3" + "@babel/helper-optimise-call-expression@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz#8b0a0456c92f6b323d27cfd00d1d664e76692a0f" @@ -347,11 +477,23 @@ dependencies: "@babel/types" "^7.24.7" +"@babel/helper-optimise-call-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz#c65221b61a643f3e62705e5dd2b5f115e35f9200" + integrity sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw== + dependencies: + "@babel/types" "^7.27.1" + "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz#98c84fe6fe3d0d3ae7bfc3a5e166a46844feb2a0" integrity sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg== +"@babel/helper-plugin-utils@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c" + integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== + "@babel/helper-remap-async-to-generator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz#b3f0f203628522713849d49403f1a414468be4c7" @@ -361,6 +503,15 @@ "@babel/helper-environment-visitor" "^7.24.7" "@babel/helper-wrap-function" "^7.24.7" +"@babel/helper-remap-async-to-generator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz#4601d5c7ce2eb2aea58328d43725523fcd362ce6" + integrity sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-wrap-function" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/helper-replace-supers@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz#f933b7eed81a1c0265740edc91491ce51250f765" @@ -370,6 +521,15 @@ "@babel/helper-member-expression-to-functions" "^7.24.7" "@babel/helper-optimise-call-expression" "^7.24.7" +"@babel/helper-replace-supers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz#b1ed2d634ce3bdb730e4b52de30f8cccfd692bc0" + integrity sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.27.1" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/helper-simple-access@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3" @@ -386,6 +546,14 @@ "@babel/traverse" "^7.24.7" "@babel/types" "^7.24.7" +"@babel/helper-skip-transparent-expression-wrappers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz#62bb91b3abba8c7f1fec0252d9dbea11b3ee7a56" + integrity sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + "@babel/helper-split-export-declaration@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz#83949436890e07fa3d6873c61a96e3bbf692d856" @@ -403,6 +571,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + "@babel/helper-validator-identifier@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" @@ -413,11 +586,21 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== +"@babel/helper-validator-identifier@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" + integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== + "@babel/helper-validator-option@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz#24c3bb77c7a425d1742eec8fb433b5a1b38e62f6" integrity sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw== +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== + "@babel/helper-wrap-function@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz#52d893af7e42edca7c6d2c6764549826336aae1f" @@ -428,6 +611,15 @@ "@babel/traverse" "^7.24.7" "@babel/types" "^7.24.7" +"@babel/helper-wrap-function@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz#b88285009c31427af318d4fe37651cd62a142409" + integrity sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ== + dependencies: + "@babel/template" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + "@babel/helpers@^7.24.7": version "7.27.0" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.0.tgz#53d156098defa8243eab0f32fa17589075a1b808" @@ -436,6 +628,14 @@ "@babel/template" "^7.27.0" "@babel/types" "^7.27.0" +"@babel/helpers@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.3.tgz#387d65d279290e22fe7a47a8ffcd2d0c0184edd0" + integrity sha512-h/eKy9agOya1IGuLaZ9tEUgz+uIRXcbtOhRtUyyMf8JFmn1iT13vnl/IGVWSkdOCG/pC57U4S1jnAabAavTMwg== + dependencies: + "@babel/template" "^7.27.2" + "@babel/types" "^7.27.3" + "@babel/highlight@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" @@ -458,6 +658,13 @@ dependencies: "@babel/types" "^7.27.0" +"@babel/parser@^7.27.2", "@babel/parser@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.3.tgz#1b7533f0d908ad2ac545c4d05cbe2fb6dc8cfaaf" + integrity sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw== + dependencies: + "@babel/types" "^7.27.3" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz#fd059fd27b184ea2b4c7e646868a9a381bbc3055" @@ -466,6 +673,21 @@ "@babel/helper-environment-visitor" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz#61dd8a8e61f7eb568268d1b5f129da3eee364bf9" + integrity sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz#43f70a6d7efd52370eefbdf55ae03d91b293856d" + integrity sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz#468096ca44bbcbe8fcc570574e12eb1950e18107" @@ -473,6 +695,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz#beb623bd573b8b6f3047bd04c32506adc3e58a72" + integrity sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz#e4eabdd5109acc399b38d7999b2ef66fc2022f89" @@ -482,6 +711,15 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" "@babel/plugin-transform-optional-chaining" "^7.24.7" +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz#e134a5479eb2ba9c02714e8c1ebf1ec9076124fd" + integrity sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-transform-optional-chaining" "^7.27.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.7.tgz#71b21bb0286d5810e63a1538aa901c58e87375ec" @@ -490,6 +728,14 @@ "@babel/helper-environment-visitor" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz#bb1c25af34d75115ce229a1de7fa44bf8f955670" + integrity sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": version "7.21.0-placeholder-for-preset-env.2" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" @@ -537,6 +783,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-syntax-import-assertions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz#88894aefd2b03b5ee6ad1562a7c8e1587496aecd" + integrity sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-import-attributes@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz#b4f9ea95a79e6912480c4b626739f86a076624ca" @@ -544,6 +797,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-syntax-import-attributes@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz#34c017d54496f9b11b61474e7ea3dfd5563ffe07" + integrity sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-import-meta@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" @@ -565,6 +825,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-syntax-jsx@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz#2f9beb5eff30fa507c5532d107daac7b888fa34c" + integrity sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" @@ -628,6 +895,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-syntax-typescript@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz#5147d29066a793450f220c63fa3a9431b7e6dd18" + integrity sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" @@ -643,6 +917,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-arrow-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz#6e2061067ba3ab0266d834a9f94811196f2aba9a" + integrity sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-async-generator-functions@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.7.tgz#7330a5c50e05181ca52351b8fd01642000c96cfd" @@ -653,6 +934,15 @@ "@babel/helper-remap-async-to-generator" "^7.24.7" "@babel/plugin-syntax-async-generators" "^7.8.4" +"@babel/plugin-transform-async-generator-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz#ca433df983d68e1375398e7ca71bf2a4f6fd89d7" + integrity sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-remap-async-to-generator" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/plugin-transform-async-to-generator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz#72a3af6c451d575842a7e9b5a02863414355bdcc" @@ -662,6 +952,15 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-remap-async-to-generator" "^7.24.7" +"@babel/plugin-transform-async-to-generator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz#9a93893b9379b39466c74474f55af03de78c66e7" + integrity sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-remap-async-to-generator" "^7.27.1" + "@babel/plugin-transform-block-scoped-functions@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz#a4251d98ea0c0f399dafe1a35801eaba455bbf1f" @@ -669,6 +968,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-block-scoped-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz#558a9d6e24cf72802dd3b62a4b51e0d62c0f57f9" + integrity sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-block-scoping@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz#42063e4deb850c7bd7c55e626bf4e7ab48e6ce02" @@ -676,6 +982,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-block-scoping@^7.27.1": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.3.tgz#a21f37e222dc0a7b91c3784fa3bd4edf8d7a6dc1" + integrity sha512-+F8CnfhuLhwUACIJMLWnjz6zvzYM2r0yeIHKlbgfw7ml8rOMJsXNXV/hyRcb3nb493gRs4WvYpQAndWj/qQmkQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-class-properties@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz#256879467b57b0b68c7ddfc5b76584f398cd6834" @@ -684,6 +997,14 @@ "@babel/helper-create-class-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-class-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz#dd40a6a370dfd49d32362ae206ddaf2bb082a925" + integrity sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-class-static-block@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz#c82027ebb7010bc33c116d4b5044fbbf8c05484d" @@ -693,6 +1014,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-class-static-block" "^7.14.5" +"@babel/plugin-transform-class-static-block@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz#7e920d5625b25bbccd3061aefbcc05805ed56ce4" + integrity sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-classes@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz#4ae6ef43a12492134138c1e45913f7c46c41b4bf" @@ -707,6 +1036,18 @@ "@babel/helper-split-export-declaration" "^7.24.7" globals "^11.1.0" +"@babel/plugin-transform-classes@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz#03bb04bea2c7b2f711f0db7304a8da46a85cced4" + integrity sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-compilation-targets" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/traverse" "^7.27.1" + globals "^11.1.0" + "@babel/plugin-transform-computed-properties@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz#4cab3214e80bc71fae3853238d13d097b004c707" @@ -715,6 +1056,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/template" "^7.24.7" +"@babel/plugin-transform-computed-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz#81662e78bf5e734a97982c2b7f0a793288ef3caa" + integrity sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/template" "^7.27.1" + "@babel/plugin-transform-destructuring@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz#a097f25292defb6e6cc16d6333a4cfc1e3c72d9e" @@ -722,6 +1071,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-destructuring@^7.27.1", "@babel/plugin-transform-destructuring@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.3.tgz#3cc8299ed798d9a909f8d66ddeb40849ec32e3b0" + integrity sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-dotall-regex@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz#5f8bf8a680f2116a7207e16288a5f974ad47a7a0" @@ -730,6 +1086,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-dotall-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz#aa6821de864c528b1fecf286f0a174e38e826f4d" + integrity sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-duplicate-keys@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz#dd20102897c9a2324e5adfffb67ff3610359a8ee" @@ -737,6 +1101,21 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-duplicate-keys@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz#f1fbf628ece18e12e7b32b175940e68358f546d1" + integrity sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz#5043854ca620a94149372e69030ff8cb6a9eb0ec" + integrity sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-dynamic-import@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz#4d8b95e3bae2b037673091aa09cd33fecd6419f4" @@ -745,6 +1124,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-dynamic-import" "^7.8.3" +"@babel/plugin-transform-dynamic-import@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz#4c78f35552ac0e06aa1f6e3c573d67695e8af5a4" + integrity sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-exponentiation-operator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz#b629ee22645f412024297d5245bce425c31f9b0d" @@ -753,6 +1139,13 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-exponentiation-operator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz#fc497b12d8277e559747f5a3ed868dd8064f83e1" + integrity sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-export-namespace-from@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz#176d52d8d8ed516aeae7013ee9556d540c53f197" @@ -761,6 +1154,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" +"@babel/plugin-transform-export-namespace-from@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz#71ca69d3471edd6daa711cf4dfc3400415df9c23" + integrity sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-for-of@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz#f25b33f72df1d8be76399e1b8f3f9d366eb5bc70" @@ -769,6 +1169,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" +"@babel/plugin-transform-for-of@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz#bc24f7080e9ff721b63a70ac7b2564ca15b6c40a" + integrity sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-transform-function-name@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz#6d8601fbffe665c894440ab4470bc721dd9131d6" @@ -778,6 +1186,15 @@ "@babel/helper-function-name" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-function-name@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz#4d0bf307720e4dce6d7c30fcb1fd6ca77bdeb3a7" + integrity sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ== + dependencies: + "@babel/helper-compilation-targets" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/plugin-transform-json-strings@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz#f3e9c37c0a373fee86e36880d45b3664cedaf73a" @@ -786,6 +1203,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-json-strings" "^7.8.3" +"@babel/plugin-transform-json-strings@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz#a2e0ce6ef256376bd527f290da023983527a4f4c" + integrity sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-literals@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz#36b505c1e655151a9d7607799a9988fc5467d06c" @@ -793,6 +1217,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz#baaefa4d10a1d4206f9dcdda50d7d5827bb70b24" + integrity sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-logical-assignment-operators@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz#a58fb6eda16c9dc8f9ff1c7b1ba6deb7f4694cb0" @@ -801,6 +1232,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" +"@babel/plugin-transform-logical-assignment-operators@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz#890cb20e0270e0e5bebe3f025b434841c32d5baa" + integrity sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-member-expression-literals@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz#3b4454fb0e302e18ba4945ba3246acb1248315df" @@ -808,6 +1246,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-member-expression-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz#37b88ba594d852418e99536f5612f795f23aeaf9" + integrity sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-modules-amd@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz#65090ed493c4a834976a3ca1cde776e6ccff32d7" @@ -816,6 +1261,14 @@ "@babel/helper-module-transforms" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-modules-amd@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz#a4145f9d87c2291fe2d05f994b65dba4e3e7196f" + integrity sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-modules-commonjs@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz#9fd5f7fdadee9085886b183f1ad13d1ab260f4ab" @@ -825,6 +1278,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-simple-access" "^7.24.7" +"@babel/plugin-transform-modules-commonjs@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz#8e44ed37c2787ecc23bdc367f49977476614e832" + integrity sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-modules-systemjs@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz#f8012316c5098f6e8dee6ecd58e2bc6f003d0ce7" @@ -835,6 +1296,16 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-validator-identifier" "^7.24.7" +"@babel/plugin-transform-modules-systemjs@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz#00e05b61863070d0f3292a00126c16c0e024c4ed" + integrity sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/plugin-transform-modules-umd@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz#edd9f43ec549099620df7df24e7ba13b5c76efc8" @@ -843,6 +1314,14 @@ "@babel/helper-module-transforms" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-modules-umd@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz#63f2cf4f6dc15debc12f694e44714863d34cd334" + integrity sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-named-capturing-groups-regex@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz#9042e9b856bc6b3688c0c2e4060e9e10b1460923" @@ -851,6 +1330,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-named-capturing-groups-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz#f32b8f7818d8fc0cc46ee20a8ef75f071af976e1" + integrity sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-new-target@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz#31ff54c4e0555cc549d5816e4ab39241dfb6ab00" @@ -858,6 +1345,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-new-target@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz#259c43939728cad1706ac17351b7e6a7bea1abeb" + integrity sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-nullish-coalescing-operator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz#1de4534c590af9596f53d67f52a92f12db984120" @@ -866,6 +1360,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" +"@babel/plugin-transform-nullish-coalescing-operator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz#4f9d3153bf6782d73dd42785a9d22d03197bc91d" + integrity sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-numeric-separator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz#bea62b538c80605d8a0fac9b40f48e97efa7de63" @@ -874,6 +1375,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-numeric-separator" "^7.10.4" +"@babel/plugin-transform-numeric-separator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz#614e0b15cc800e5997dadd9bd6ea524ed6c819c6" + integrity sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-object-rest-spread@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz#d13a2b93435aeb8a197e115221cab266ba6e55d6" @@ -884,6 +1392,16 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.3" "@babel/plugin-transform-parameters" "^7.24.7" +"@babel/plugin-transform-object-rest-spread@^7.27.2": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.3.tgz#ce130aa73fef828bc3e3e835f9bc6144be3eb1c0" + integrity sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q== + dependencies: + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-destructuring" "^7.27.3" + "@babel/plugin-transform-parameters" "^7.27.1" + "@babel/plugin-transform-object-super@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz#66eeaff7830bba945dd8989b632a40c04ed625be" @@ -892,6 +1410,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-replace-supers" "^7.24.7" +"@babel/plugin-transform-object-super@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz#1c932cd27bf3874c43a5cac4f43ebf970c9871b5" + integrity sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/plugin-transform-optional-catch-binding@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz#00eabd883d0dd6a60c1c557548785919b6e717b4" @@ -900,6 +1426,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" +"@babel/plugin-transform-optional-catch-binding@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz#84c7341ebde35ccd36b137e9e45866825072a30c" + integrity sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-optional-chaining@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz#b8f6848a80cf2da98a8a204429bec04756c6d454" @@ -909,6 +1442,14 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" "@babel/plugin-syntax-optional-chaining" "^7.8.3" +"@babel/plugin-transform-optional-chaining@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz#874ce3c4f06b7780592e946026eb76a32830454f" + integrity sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-transform-parameters@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz#5881f0ae21018400e320fc7eb817e529d1254b68" @@ -916,6 +1457,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-parameters@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz#80334b54b9b1ac5244155a0c8304a187a618d5a7" + integrity sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-private-methods@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz#e6318746b2ae70a59d023d5cc1344a2ba7a75f5e" @@ -924,6 +1472,14 @@ "@babel/helper-create-class-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-private-methods@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz#fdacbab1c5ed81ec70dfdbb8b213d65da148b6af" + integrity sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-private-property-in-object@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz#4eec6bc701288c1fab5f72e6a4bbc9d67faca061" @@ -934,6 +1490,15 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" +"@babel/plugin-transform-private-property-in-object@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz#4dbbef283b5b2f01a21e81e299f76e35f900fb11" + integrity sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-property-literals@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz#f0d2ed8380dfbed949c42d4d790266525d63bbdc" @@ -941,6 +1506,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-property-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz#07eafd618800591e88073a0af1b940d9a42c6424" + integrity sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-react-constant-elements@^7.21.3": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.24.7.tgz#b85e8f240b14400277f106c9c9b585d9acf608a1" @@ -955,6 +1527,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-react-display-name@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.27.1.tgz#43af31362d71f7848cfac0cbc212882b1a16e80f" + integrity sha512-p9+Vl3yuHPmkirRrg021XiP+EETmPMQTLr6Ayjj85RLNEbb3Eya/4VI0vAdzQG9SEAl2Lnt7fy5lZyMzjYoZQQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-react-jsx-development@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz#eaee12f15a93f6496d852509a850085e6361470b" @@ -962,6 +1541,13 @@ dependencies: "@babel/plugin-transform-react-jsx" "^7.24.7" +"@babel/plugin-transform-react-jsx-development@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz#47ff95940e20a3a70e68ad3d4fcb657b647f6c98" + integrity sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.27.1" + "@babel/plugin-transform-react-jsx@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz#17cd06b75a9f0e2bd076503400e7c4b99beedac4" @@ -973,6 +1559,17 @@ "@babel/plugin-syntax-jsx" "^7.24.7" "@babel/types" "^7.24.7" +"@babel/plugin-transform-react-jsx@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz#1023bc94b78b0a2d68c82b5e96aed573bcfb9db0" + integrity sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-jsx" "^7.27.1" + "@babel/types" "^7.27.1" + "@babel/plugin-transform-react-pure-annotations@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz#bdd9d140d1c318b4f28b29a00fb94f97ecab1595" @@ -981,6 +1578,14 @@ "@babel/helper-annotate-as-pure" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-react-pure-annotations@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz#339f1ce355eae242e0649f232b1c68907c02e879" + integrity sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-regenerator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz#021562de4534d8b4b1851759fd7af4e05d2c47f8" @@ -989,6 +1594,21 @@ "@babel/helper-plugin-utils" "^7.24.7" regenerator-transform "^0.15.2" +"@babel/plugin-transform-regenerator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz#0a471df9213416e44cd66bf67176b66f65768401" + integrity sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-regexp-modifiers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz#df9ba5577c974e3f1449888b70b76169998a6d09" + integrity sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-reserved-words@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz#80037fe4fbf031fc1125022178ff3938bb3743a4" @@ -996,15 +1616,22 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-runtime@^7.22.9": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.7.tgz#00a5bfaf8c43cf5c8703a8a6e82b59d9c58f38ca" - integrity sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw== +"@babel/plugin-transform-reserved-words@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz#40fba4878ccbd1c56605a4479a3a891ac0274bb4" + integrity sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw== dependencies: - "@babel/helper-module-imports" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-runtime@^7.25.9": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.27.3.tgz#ad35f1eff5ba18a5e23f7270e939fb5a59d3ec0b" + integrity sha512-bA9ZL5PW90YwNgGfjg6U+7Qh/k3zCEQJ06BFgAGRp/yMjw9hP9UGbGPtx3KSOkHGljEPCCxaE+PH4fUR2h1sDw== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" babel-plugin-polyfill-corejs2 "^0.4.10" - babel-plugin-polyfill-corejs3 "^0.10.1" + babel-plugin-polyfill-corejs3 "^0.11.0" babel-plugin-polyfill-regenerator "^0.6.1" semver "^6.3.1" @@ -1015,6 +1642,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-shorthand-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz#532abdacdec87bfee1e0ef8e2fcdee543fe32b90" + integrity sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-spread@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz#e8a38c0fde7882e0fb8f160378f74bd885cc7bb3" @@ -1023,6 +1657,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" +"@babel/plugin-transform-spread@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz#1a264d5fc12750918f50e3fe3e24e437178abb08" + integrity sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-transform-sticky-regex@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz#96ae80d7a7e5251f657b5cf18f1ea6bf926f5feb" @@ -1030,6 +1672,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-sticky-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz#18984935d9d2296843a491d78a014939f7dcd280" + integrity sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-template-literals@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz#a05debb4a9072ae8f985bcf77f3f215434c8f8c8" @@ -1037,6 +1686,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-template-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz#1a0eb35d8bb3e6efc06c9fd40eb0bcef548328b8" + integrity sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-typeof-symbol@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz#f074be466580d47d6e6b27473a840c9f9ca08fb0" @@ -1044,6 +1700,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-typeof-symbol@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz#70e966bb492e03509cf37eafa6dcc3051f844369" + integrity sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-typescript@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.7.tgz#b006b3e0094bf0813d505e0c5485679eeaf4a881" @@ -1054,6 +1717,17 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-typescript" "^7.24.7" +"@babel/plugin-transform-typescript@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.1.tgz#d3bb65598bece03f773111e88cc4e8e5070f1140" + integrity sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-syntax-typescript" "^7.27.1" + "@babel/plugin-transform-unicode-escapes@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz#2023a82ced1fb4971630a2e079764502c4148e0e" @@ -1061,6 +1735,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-unicode-escapes@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz#3e3143f8438aef842de28816ece58780190cf806" + integrity sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-unicode-property-regex@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz#9073a4cd13b86ea71c3264659590ac086605bbcd" @@ -1069,6 +1750,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-unicode-property-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz#bdfe2d3170c78c5691a3c3be934c8c0087525956" + integrity sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-unicode-regex@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz#dfc3d4a51127108099b19817c0963be6a2adf19f" @@ -1077,6 +1766,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-unicode-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz#25948f5c395db15f609028e370667ed8bae9af97" + integrity sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-unicode-sets-regex@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz#d40705d67523803a576e29c63cef6e516b858ed9" @@ -1085,7 +1782,15 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" -"@babel/preset-env@^7.20.2", "@babel/preset-env@^7.22.9": +"@babel/plugin-transform-unicode-sets-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz#6ab706d10f801b5c72da8bb2548561fa04193cd1" + integrity sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/preset-env@^7.20.2": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.7.tgz#ff067b4e30ba4a72f225f12f123173e77b987f37" integrity sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ== @@ -1172,6 +1877,81 @@ core-js-compat "^3.31.0" semver "^6.3.1" +"@babel/preset-env@^7.25.9": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.27.2.tgz#106e6bfad92b591b1f6f76fd4cf13b7725a7bf9a" + integrity sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ== + dependencies: + "@babel/compat-data" "^7.27.2" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.27.1" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.27.1" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.27.1" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.27.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.27.1" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-import-assertions" "^7.27.1" + "@babel/plugin-syntax-import-attributes" "^7.27.1" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.27.1" + "@babel/plugin-transform-async-generator-functions" "^7.27.1" + "@babel/plugin-transform-async-to-generator" "^7.27.1" + "@babel/plugin-transform-block-scoped-functions" "^7.27.1" + "@babel/plugin-transform-block-scoping" "^7.27.1" + "@babel/plugin-transform-class-properties" "^7.27.1" + "@babel/plugin-transform-class-static-block" "^7.27.1" + "@babel/plugin-transform-classes" "^7.27.1" + "@babel/plugin-transform-computed-properties" "^7.27.1" + "@babel/plugin-transform-destructuring" "^7.27.1" + "@babel/plugin-transform-dotall-regex" "^7.27.1" + "@babel/plugin-transform-duplicate-keys" "^7.27.1" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.27.1" + "@babel/plugin-transform-dynamic-import" "^7.27.1" + "@babel/plugin-transform-exponentiation-operator" "^7.27.1" + "@babel/plugin-transform-export-namespace-from" "^7.27.1" + "@babel/plugin-transform-for-of" "^7.27.1" + "@babel/plugin-transform-function-name" "^7.27.1" + "@babel/plugin-transform-json-strings" "^7.27.1" + "@babel/plugin-transform-literals" "^7.27.1" + "@babel/plugin-transform-logical-assignment-operators" "^7.27.1" + "@babel/plugin-transform-member-expression-literals" "^7.27.1" + "@babel/plugin-transform-modules-amd" "^7.27.1" + "@babel/plugin-transform-modules-commonjs" "^7.27.1" + "@babel/plugin-transform-modules-systemjs" "^7.27.1" + "@babel/plugin-transform-modules-umd" "^7.27.1" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.27.1" + "@babel/plugin-transform-new-target" "^7.27.1" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.27.1" + "@babel/plugin-transform-numeric-separator" "^7.27.1" + "@babel/plugin-transform-object-rest-spread" "^7.27.2" + "@babel/plugin-transform-object-super" "^7.27.1" + "@babel/plugin-transform-optional-catch-binding" "^7.27.1" + "@babel/plugin-transform-optional-chaining" "^7.27.1" + "@babel/plugin-transform-parameters" "^7.27.1" + "@babel/plugin-transform-private-methods" "^7.27.1" + "@babel/plugin-transform-private-property-in-object" "^7.27.1" + "@babel/plugin-transform-property-literals" "^7.27.1" + "@babel/plugin-transform-regenerator" "^7.27.1" + "@babel/plugin-transform-regexp-modifiers" "^7.27.1" + "@babel/plugin-transform-reserved-words" "^7.27.1" + "@babel/plugin-transform-shorthand-properties" "^7.27.1" + "@babel/plugin-transform-spread" "^7.27.1" + "@babel/plugin-transform-sticky-regex" "^7.27.1" + "@babel/plugin-transform-template-literals" "^7.27.1" + "@babel/plugin-transform-typeof-symbol" "^7.27.1" + "@babel/plugin-transform-unicode-escapes" "^7.27.1" + "@babel/plugin-transform-unicode-property-regex" "^7.27.1" + "@babel/plugin-transform-unicode-regex" "^7.27.1" + "@babel/plugin-transform-unicode-sets-regex" "^7.27.1" + "@babel/preset-modules" "0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2 "^0.4.10" + babel-plugin-polyfill-corejs3 "^0.11.0" + babel-plugin-polyfill-regenerator "^0.6.1" + core-js-compat "^3.40.0" + semver "^6.3.1" + "@babel/preset-modules@0.1.6-no-external-plugins": version "0.1.6-no-external-plugins" resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" @@ -1181,7 +1961,7 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/preset-react@^7.18.6", "@babel/preset-react@^7.22.5": +"@babel/preset-react@^7.18.6": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.24.7.tgz#480aeb389b2a798880bf1f889199e3641cbb22dc" integrity sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag== @@ -1193,7 +1973,19 @@ "@babel/plugin-transform-react-jsx-development" "^7.24.7" "@babel/plugin-transform-react-pure-annotations" "^7.24.7" -"@babel/preset-typescript@^7.21.0", "@babel/preset-typescript@^7.22.5": +"@babel/preset-react@^7.25.9": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.27.1.tgz#86ea0a5ca3984663f744be2fd26cb6747c3fd0ec" + integrity sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-transform-react-display-name" "^7.27.1" + "@babel/plugin-transform-react-jsx" "^7.27.1" + "@babel/plugin-transform-react-jsx-development" "^7.27.1" + "@babel/plugin-transform-react-pure-annotations" "^7.27.1" + +"@babel/preset-typescript@^7.21.0": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz#66cd86ea8f8c014855671d5ea9a737139cbbfef1" integrity sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ== @@ -1204,26 +1996,41 @@ "@babel/plugin-transform-modules-commonjs" "^7.24.7" "@babel/plugin-transform-typescript" "^7.24.7" +"@babel/preset-typescript@^7.25.9": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz#190742a6428d282306648a55b0529b561484f912" + integrity sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-syntax-jsx" "^7.27.1" + "@babel/plugin-transform-modules-commonjs" "^7.27.1" + "@babel/plugin-transform-typescript" "^7.27.1" + "@babel/regjsgen@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime-corejs3@^7.22.6": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.24.7.tgz#65a99097e4c28e6c3a174825591700cc5abd710e" - integrity sha512-eytSX6JLBY6PVAeQa2bFlDx/7Mmln/gaEpsit5a3WEvjGfiIytEsgAwuIXCPM0xvw0v0cJn3ilq0/TvXrW0kgA== +"@babel/runtime-corejs3@^7.25.9": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.27.3.tgz#b971a4a0a376171e266629152e74ef50e9931f79" + integrity sha512-ZYcgrwb+dkWNcDlsTe4fH1CMdqMDSJ5lWFd1by8Si2pI54XcQjte/+ViIPqAk7EAWisaUxvQ89grv+bNX2x8zg== dependencies: core-js-pure "^3.30.2" - regenerator-runtime "^0.14.0" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.22.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.27.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762" integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw== dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.25.9": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.3.tgz#10491113799fb8d77e1d9273384d5d68deeea8f6" + integrity sha512-7EYtGezsdiDMyY80+65EzwiGmcJqpmcZCojSXaRgdrBaGtWTgDZKq69cPIVped6MkIM78cTQ2GOiEYjwOlG4xw== + "@babel/template@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.7.tgz#02efcee317d0609d2c07117cb70ef8fb17ab7315" @@ -1242,7 +2049,16 @@ "@babel/parser" "^7.27.0" "@babel/types" "^7.27.0" -"@babel/traverse@^7.22.8", "@babel/traverse@^7.24.7": +"@babel/template@^7.27.1", "@babel/template@^7.27.2": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" + integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/parser" "^7.27.2" + "@babel/types" "^7.27.1" + +"@babel/traverse@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.7.tgz#de2b900163fa741721ba382163fe46a936c40cf5" integrity sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA== @@ -1258,6 +2074,19 @@ debug "^4.3.1" globals "^11.1.0" +"@babel/traverse@^7.25.9", "@babel/traverse@^7.27.1", "@babel/traverse@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.27.3.tgz#8b62a6c2d10f9d921ba7339c90074708509cffae" + integrity sha512-lId/IfN/Ye1CIu8xG7oKBHXd2iNb2aW1ilPszzGcJug6M8RCKfVNcYhpI5+bMvFYjK7lXIM0R+a+6r8xhHp2FQ== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.27.3" + "@babel/parser" "^7.27.3" + "@babel/template" "^7.27.2" + "@babel/types" "^7.27.3" + debug "^4.3.1" + globals "^11.1.0" + "@babel/types@^7.21.3", "@babel/types@^7.24.7", "@babel/types@^7.4.4": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.7.tgz#6027fe12bc1aa724cd32ab113fb7f1988f1f66f2" @@ -1275,10 +2104,18 @@ "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" -"@braintree/sanitize-url@^6.0.1": - version "6.0.4" - resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783" - integrity sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A== +"@babel/types@^7.27.1", "@babel/types@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.3.tgz#c0257bedf33aad6aad1f406d35c44758321eb3ec" + integrity sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + +"@braintree/sanitize-url@^7.0.4": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz#15e19737d946559289b915e5dad3b4c28407735e" + integrity sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw== "@bufbuild/buf-darwin-arm64@1.33.0": version "1.33.0" @@ -1322,138 +2159,543 @@ "@bufbuild/buf-win32-arm64" "1.33.0" "@bufbuild/buf-win32-x64" "1.33.0" +"@chevrotain/cst-dts-gen@11.0.3": + version "11.0.3" + resolved "https://registry.yarnpkg.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz#5e0863cc57dc45e204ccfee6303225d15d9d4783" + integrity sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ== + dependencies: + "@chevrotain/gast" "11.0.3" + "@chevrotain/types" "11.0.3" + lodash-es "4.17.21" + +"@chevrotain/gast@11.0.3": + version "11.0.3" + resolved "https://registry.yarnpkg.com/@chevrotain/gast/-/gast-11.0.3.tgz#e84d8880323fe8cbe792ef69ce3ffd43a936e818" + integrity sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q== + dependencies: + "@chevrotain/types" "11.0.3" + lodash-es "4.17.21" + +"@chevrotain/regexp-to-ast@11.0.3": + version "11.0.3" + resolved "https://registry.yarnpkg.com/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz#11429a81c74a8e6a829271ce02fc66166d56dcdb" + integrity sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA== + +"@chevrotain/types@11.0.3": + version "11.0.3" + resolved "https://registry.yarnpkg.com/@chevrotain/types/-/types-11.0.3.tgz#f8a03914f7b937f594f56eb89312b3b8f1c91848" + integrity sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ== + +"@chevrotain/utils@11.0.3": + version "11.0.3" + resolved "https://registry.yarnpkg.com/@chevrotain/utils/-/utils-11.0.3.tgz#e39999307b102cff3645ec4f5b3665f5297a2224" + integrity sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ== + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== +"@csstools/cascade-layer-name-parser@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz#43f962bebead0052a9fed1a2deeb11f85efcbc72" + integrity sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A== + +"@csstools/color-helpers@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-5.0.2.tgz#82592c9a7c2b83c293d9161894e2a6471feb97b8" + integrity sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA== + +"@csstools/css-calc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-2.1.4.tgz#8473f63e2fcd6e459838dd412401d5948f224c65" + integrity sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ== + +"@csstools/css-color-parser@^3.0.10": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz#79fc68864dd43c3b6782d2b3828bc0fa9d085c10" + integrity sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg== + dependencies: + "@csstools/color-helpers" "^5.0.2" + "@csstools/css-calc" "^2.1.4" + +"@csstools/css-parser-algorithms@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz#5755370a9a29abaec5515b43c8b3f2cf9c2e3076" + integrity sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ== + +"@csstools/css-tokenizer@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz#333fedabc3fd1a8e5d0100013731cf19e6a8c5d3" + integrity sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw== + +"@csstools/media-query-list-parser@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz#7aec77bcb89c2da80ef207e73f474ef9e1b3cdf1" + integrity sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ== + +"@csstools/postcss-cascade-layers@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.1.tgz#9640313e64b5e39133de7e38a5aa7f40dc259597" + integrity sha512-XOfhI7GShVcKiKwmPAnWSqd2tBR0uxt+runAxttbSp/LY2U16yAVPmAf7e9q4JJ0d+xMNmpwNDLBXnmRCl3HMQ== + dependencies: + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + +"@csstools/postcss-color-function@^4.0.10": + version "4.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-4.0.10.tgz#11ad43a66ef2cc794ab826a07df8b5fa9fb47a3a" + integrity sha512-4dY0NBu7NVIpzxZRgh/Q/0GPSz/jLSw0i/u3LTUor0BkQcz/fNhN10mSWBDsL0p9nDb0Ky1PD6/dcGbhACuFTQ== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-color-mix-function@^3.0.10": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.10.tgz#8c9d0ccfae5c45a9870dd84807ea2995c7a3a514" + integrity sha512-P0lIbQW9I4ShE7uBgZRib/lMTf9XMjJkFl/d6w4EMNHu2qvQ6zljJGEcBkw/NsBtq/6q3WrmgxSS8kHtPMkK4Q== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-color-mix-variadic-function-arguments@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-1.0.0.tgz#0b29cb9b4630d7ed68549db265662d41554a17ed" + integrity sha512-Z5WhouTyD74dPFPrVE7KydgNS9VvnjB8qcdes9ARpCOItb4jTnm7cHp4FhxCRUoyhabD0WVv43wbkJ4p8hLAlQ== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-content-alt-text@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.6.tgz#548862226eac54bab0ee5f1bf3a9981393ab204b" + integrity sha512-eRjLbOjblXq+byyaedQRSrAejKGNAFued+LcbzT+LCL78fabxHkxYjBbxkroONxHHYu2qxhFK2dBStTLPG3jpQ== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-exponential-functions@^2.0.9": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.9.tgz#fc03d1272888cb77e64cc1a7d8a33016e4f05c69" + integrity sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-font-format-keywords@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-4.0.0.tgz#6730836eb0153ff4f3840416cc2322f129c086e6" + integrity sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-gamut-mapping@^2.0.10": + version "2.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.10.tgz#f518d941231d721dbecf5b41e3c441885ff2f28b" + integrity sha512-QDGqhJlvFnDlaPAfCYPsnwVA6ze+8hhrwevYWlnUeSjkkZfBpcCO42SaUD8jiLlq7niouyLgvup5lh+f1qessg== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-gradients-interpolation-method@^5.0.10": + version "5.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.10.tgz#3146da352c31142a721fdba062ac3a6d11dbbec3" + integrity sha512-HHPauB2k7Oits02tKFUeVFEU2ox/H3OQVrP3fSOKDxvloOikSal+3dzlyTZmYsb9FlY9p5EUpBtz0//XBmy+aw== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-hwb-function@^4.0.10": + version "4.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.10.tgz#f93f3c457e6440ac37ef9b908feb5d901b417d50" + integrity sha512-nOKKfp14SWcdEQ++S9/4TgRKchooLZL0TUFdun3nI4KPwCjETmhjta1QT4ICQcGVWQTvrsgMM/aLB5We+kMHhQ== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-ic-unit@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.2.tgz#7561e09db65fac8304ceeab9dd3e5c6e43414587" + integrity sha512-lrK2jjyZwh7DbxaNnIUjkeDmU8Y6KyzRBk91ZkI5h8nb1ykEfZrtIVArdIjX4DHMIBGpdHrgP0n4qXDr7OHaKA== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-initial@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-initial/-/postcss-initial-2.0.1.tgz#c385bd9d8ad31ad159edd7992069e97ceea4d09a" + integrity sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg== + +"@csstools/postcss-is-pseudo-class@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.1.tgz#12041448fedf01090dd4626022c28b7f7623f58e" + integrity sha512-JLp3POui4S1auhDR0n8wHd/zTOWmMsmK3nQd3hhL6FhWPaox5W7j1se6zXOG/aP07wV2ww0lxbKYGwbBszOtfQ== + dependencies: + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + +"@csstools/postcss-light-dark-function@^2.0.9": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.9.tgz#9fb080188907539734a9d5311d2a1cb82531ef38" + integrity sha512-1tCZH5bla0EAkFAI2r0H33CDnIBeLUaJh1p+hvvsylJ4svsv2wOmJjJn+OXwUZLXef37GYbRIVKX+X+g6m+3CQ== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-logical-float-and-clear@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-3.0.0.tgz#62617564182cf86ab5d4e7485433ad91e4c58571" + integrity sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ== + +"@csstools/postcss-logical-overflow@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-2.0.0.tgz#c6de7c5f04e3d4233731a847f6c62819bcbcfa1d" + integrity sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA== + +"@csstools/postcss-logical-overscroll-behavior@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-2.0.0.tgz#43c03eaecdf34055ef53bfab691db6dc97a53d37" + integrity sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w== + +"@csstools/postcss-logical-resize@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-resize/-/postcss-logical-resize-3.0.0.tgz#4df0eeb1a61d7bd85395e56a5cce350b5dbfdca6" + integrity sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-logical-viewport-units@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.4.tgz#016d98a8b7b5f969e58eb8413447eb801add16fc" + integrity sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ== + dependencies: + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-media-minmax@^2.0.9": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.9.tgz#184252d5b93155ae526689328af6bdf3fc113987" + integrity sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/media-query-list-parser" "^4.0.3" + +"@csstools/postcss-media-queries-aspect-ratio-number-values@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.5.tgz#f485c31ec13d6b0fb5c528a3474334a40eff5f11" + integrity sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/media-query-list-parser" "^4.0.3" + +"@csstools/postcss-nested-calc@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-nested-calc/-/postcss-nested-calc-4.0.0.tgz#754e10edc6958d664c11cde917f44ba144141c62" + integrity sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-normalize-display-values@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.0.tgz#ecdde2daf4e192e5da0c6fd933b6d8aff32f2a36" + integrity sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-oklab-function@^4.0.10": + version "4.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.10.tgz#d4c23c51dd0be45e6dedde22432d7d0003710780" + integrity sha512-ZzZUTDd0fgNdhv8UUjGCtObPD8LYxMH+MJsW9xlZaWTV8Ppr4PtxlHYNMmF4vVWGl0T6f8tyWAKjoI6vePSgAg== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-progressive-custom-properties@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.1.0.tgz#70c8d41b577f4023633b7e3791604e0b7f3775bc" + integrity sha512-YrkI9dx8U4R8Sz2EJaoeD9fI7s7kmeEBfmO+UURNeL6lQI7VxF6sBE+rSqdCBn4onwqmxFdBU3lTwyYb/lCmxA== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-random-function@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-random-function/-/postcss-random-function-2.0.1.tgz#3191f32fe72936e361dadf7dbfb55a0209e2691e" + integrity sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-relative-color-syntax@^3.0.10": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.10.tgz#daa840583969461e1e06b12e9c591e52a790ec86" + integrity sha512-8+0kQbQGg9yYG8hv0dtEpOMLwB9M+P7PhacgIzVzJpixxV4Eq9AUQtQw8adMmAJU1RBBmIlpmtmm3XTRd/T00g== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-scope-pseudo-class@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-4.0.1.tgz#9fe60e9d6d91d58fb5fc6c768a40f6e47e89a235" + integrity sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q== + dependencies: + postcss-selector-parser "^7.0.0" + +"@csstools/postcss-sign-functions@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.4.tgz#a9ac56954014ae4c513475b3f1b3e3424a1e0c12" + integrity sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-stepped-value-functions@^4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.9.tgz#36036f1a0e5e5ee2308e72f3c9cb433567c387b9" + integrity sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-text-decoration-shorthand@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.2.tgz#a3bcf80492e6dda36477538ab8e8943908c9f80a" + integrity sha512-8XvCRrFNseBSAGxeaVTaNijAu+FzUvjwFXtcrynmazGb/9WUdsPCpBX+mHEHShVRq47Gy4peYAoxYs8ltUnmzA== + dependencies: + "@csstools/color-helpers" "^5.0.2" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-trigonometric-functions@^4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.9.tgz#3f94ed2e319b57f2c59720b64e4d0a8a6fb8c3b2" + integrity sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-unset-value@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-unset-value/-/postcss-unset-value-4.0.0.tgz#7caa981a34196d06a737754864baf77d64de4bba" + integrity sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA== + +"@csstools/selector-resolve-nested@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.0.0.tgz#704a9b637975680e025e069a4c58b3beb3e2752a" + integrity sha512-ZoK24Yku6VJU1gS79a5PFmC8yn3wIapiKmPgun0hZgEI5AOqgH2kiPRsPz1qkGv4HL+wuDLH83yQyk6inMYrJQ== + +"@csstools/selector-specificity@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz#037817b574262134cabd68fc4ec1a454f168407b" + integrity sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw== + +"@csstools/utilities@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/utilities/-/utilities-2.0.0.tgz#f7ff0fee38c9ffb5646d47b6906e0bc8868bde60" + integrity sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ== + "@discoveryjs/json-ext@0.5.7": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== -"@docsearch/css@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.6.0.tgz#0e9f56f704b3a34d044d15fd9962ebc1536ba4fb" - integrity sha512-+sbxb71sWre+PwDK7X2T8+bhS6clcVMLwBPznX45Qu6opJcgRjAp7gYSDzVFp187J+feSj5dNBN1mJoi6ckkUQ== +"@docsearch/css@3.9.0": + version "3.9.0" + resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.9.0.tgz#3bc29c96bf024350d73b0cfb7c2a7b71bf251cd5" + integrity sha512-cQbnVbq0rrBwNAKegIac/t6a8nWoUAn8frnkLFW6YARaRmAQr5/Eoe6Ln2fqkUCZ40KpdrKbpSAmgrkviOxuWA== -"@docsearch/react@^3.5.2": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.6.0.tgz#b4f25228ecb7fc473741aefac592121e86dd2958" - integrity sha512-HUFut4ztcVNmqy9gp/wxNbC7pTOHhgVVkHVGCACTuLhUKUhKAF9KYHJtMiLUJxEqiFLQiuri1fWF8zqwM/cu1w== +"@docsearch/react@^3.9.0": + version "3.9.0" + resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.9.0.tgz#d0842b700c3ee26696786f3c8ae9f10c1a3f0db3" + integrity sha512-mb5FOZYZIkRQ6s/NWnM98k879vu5pscWqTLubLFBO87igYYT4VzVazh4h5o/zCvTIZgEt3PvsCOMOswOUo9yHQ== dependencies: - "@algolia/autocomplete-core" "1.9.3" - "@algolia/autocomplete-preset-algolia" "1.9.3" - "@docsearch/css" "3.6.0" - algoliasearch "^4.19.1" + "@algolia/autocomplete-core" "1.17.9" + "@algolia/autocomplete-preset-algolia" "1.17.9" + "@docsearch/css" "3.9.0" + algoliasearch "^5.14.2" -"@docusaurus/core@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.4.0.tgz#bdbf1af4b2f25d1bf4a5b62ec6137d84c821cb3c" - integrity sha512-g+0wwmN2UJsBqy2fQRQ6fhXruoEa62JDeEa5d8IdTJlMoaDaEDfHh7WjwGRn4opuTQWpjAwP/fbcgyHKlE+64w== +"@docusaurus/babel@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/babel/-/babel-3.8.0.tgz#2f390cc4e588a96ec496d87921e44890899738a6" + integrity sha512-9EJwSgS6TgB8IzGk1L8XddJLhZod8fXT4ULYMx6SKqyCBqCFpVCEjR/hNXXhnmtVM2irDuzYoVLGWv7srG/VOA== dependencies: - "@babel/core" "^7.23.3" - "@babel/generator" "^7.23.3" + "@babel/core" "^7.25.9" + "@babel/generator" "^7.25.9" "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-transform-runtime" "^7.22.9" - "@babel/preset-env" "^7.22.9" - "@babel/preset-react" "^7.22.5" - "@babel/preset-typescript" "^7.22.5" - "@babel/runtime" "^7.22.6" - "@babel/runtime-corejs3" "^7.22.6" - "@babel/traverse" "^7.22.8" - "@docusaurus/cssnano-preset" "3.4.0" - "@docusaurus/logger" "3.4.0" - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - autoprefixer "^10.4.14" - babel-loader "^9.1.3" + "@babel/plugin-transform-runtime" "^7.25.9" + "@babel/preset-env" "^7.25.9" + "@babel/preset-react" "^7.25.9" + "@babel/preset-typescript" "^7.25.9" + "@babel/runtime" "^7.25.9" + "@babel/runtime-corejs3" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@docusaurus/logger" "3.8.0" + "@docusaurus/utils" "3.8.0" babel-plugin-dynamic-import-node "^2.3.3" - boxen "^6.2.1" - chalk "^4.1.2" - chokidar "^3.5.3" + fs-extra "^11.1.1" + tslib "^2.6.0" + +"@docusaurus/bundler@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/bundler/-/bundler-3.8.0.tgz#386f54dca594d81bac6b617c71822e0808d6e2f6" + integrity sha512-Rq4Z/MSeAHjVzBLirLeMcjLIAQy92pF1OI+2rmt18fSlMARfTGLWRE8Vb+ljQPTOSfJxwDYSzsK6i7XloD2rNA== + dependencies: + "@babel/core" "^7.25.9" + "@docusaurus/babel" "3.8.0" + "@docusaurus/cssnano-preset" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + babel-loader "^9.2.1" clean-css "^5.3.2" - cli-table3 "^0.6.3" - combine-promises "^1.1.0" - commander "^5.1.0" copy-webpack-plugin "^11.0.0" - core-js "^3.31.1" css-loader "^6.8.1" css-minimizer-webpack-plugin "^5.0.1" cssnano "^6.1.2" - del "^6.1.1" + file-loader "^6.2.0" + html-minifier-terser "^7.2.0" + mini-css-extract-plugin "^2.9.1" + null-loader "^4.0.1" + postcss "^8.4.26" + postcss-loader "^7.3.3" + postcss-preset-env "^10.1.0" + terser-webpack-plugin "^5.3.9" + tslib "^2.6.0" + url-loader "^4.1.1" + webpack "^5.95.0" + webpackbar "^6.0.1" + +"@docusaurus/core@3.8.0", "@docusaurus/core@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.8.0.tgz#79d5e1084415c8834a8a5cb87162ca13f52fe147" + integrity sha512-c7u6zFELmSGPEP9WSubhVDjgnpiHgDqMh1qVdCB7rTflh4Jx0msTYmMiO91Ez0KtHj4sIsDsASnjwfJ2IZp3Vw== + dependencies: + "@docusaurus/babel" "3.8.0" + "@docusaurus/bundler" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/mdx-loader" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" + boxen "^6.2.1" + chalk "^4.1.2" + chokidar "^3.5.3" + cli-table3 "^0.6.3" + combine-promises "^1.1.0" + commander "^5.1.0" + core-js "^3.31.1" detect-port "^1.5.1" escape-html "^1.0.3" eta "^2.2.0" eval "^0.1.8" - file-loader "^6.2.0" + execa "5.1.1" fs-extra "^11.1.1" - html-minifier-terser "^7.2.0" html-tags "^3.3.1" - html-webpack-plugin "^5.5.3" + html-webpack-plugin "^5.6.0" leven "^3.1.0" lodash "^4.17.21" - mini-css-extract-plugin "^2.7.6" + open "^8.4.0" p-map "^4.0.0" - postcss "^8.4.26" - postcss-loader "^7.3.3" prompts "^2.4.2" - react-dev-utils "^12.0.1" - react-helmet-async "^1.3.0" + react-helmet-async "npm:@slorber/react-helmet-async@1.3.0" react-loadable "npm:@docusaurus/react-loadable@6.0.0" react-loadable-ssr-addon-v5-slorber "^1.0.1" react-router "^5.3.4" react-router-config "^5.1.1" react-router-dom "^5.3.4" - rtl-detect "^1.0.4" semver "^7.5.4" - serve-handler "^6.1.5" - shelljs "^0.8.5" - terser-webpack-plugin "^5.3.9" + serve-handler "^6.1.6" + tinypool "^1.0.2" tslib "^2.6.0" update-notifier "^6.0.2" - url-loader "^4.1.1" - webpack "^5.88.1" - webpack-bundle-analyzer "^4.9.0" - webpack-dev-server "^4.15.1" - webpack-merge "^5.9.0" - webpackbar "^5.0.2" + webpack "^5.95.0" + webpack-bundle-analyzer "^4.10.2" + webpack-dev-server "^4.15.2" + webpack-merge "^6.0.1" -"@docusaurus/cssnano-preset@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.4.0.tgz#dc7922b3bbeabcefc9b60d0161680d81cf72c368" - integrity sha512-qwLFSz6v/pZHy/UP32IrprmH5ORce86BGtN0eBtG75PpzQJAzp9gefspox+s8IEOr0oZKuQ/nhzZ3xwyc3jYJQ== +"@docusaurus/cssnano-preset@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.8.0.tgz#a70f19e2995be2299f5ef9c3da3e5d4d5c14bff2" + integrity sha512-UJ4hAS2T0R4WNy+phwVff2Q0L5+RXW9cwlH6AEphHR5qw3m/yacfWcSK7ort2pMMbDn8uGrD38BTm4oLkuuNoQ== dependencies: cssnano-preset-advanced "^6.1.2" postcss "^8.4.38" postcss-sort-media-queries "^5.2.0" tslib "^2.6.0" -"@docusaurus/logger@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.4.0.tgz#8b0ac05c7f3dac2009066e2f964dee8209a77403" - integrity sha512-bZwkX+9SJ8lB9kVRkXw+xvHYSMGG4bpYHKGXeXFvyVc79NMeeBSGgzd4TQLHH+DYeOJoCdl8flrFJVxlZ0wo/Q== +"@docusaurus/faster@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/faster/-/faster-3.8.0.tgz#2814c5ea4f19e10a6cebf9296b6f15f8a621bf61" + integrity sha512-v9+8rT2gw/4zIRBwc4fIVhrTH/yFVDQgJgyYZjqr3fgojOypdQCOwkN6Z8dOwTei4/zo+b/zDPB4x1UvghJZRg== + dependencies: + "@docusaurus/types" "3.8.0" + "@rspack/core" "^1.3.10" + "@swc/core" "^1.7.39" + "@swc/html" "^1.7.39" + browserslist "^4.24.2" + lightningcss "^1.27.0" + swc-loader "^0.2.6" + tslib "^2.6.0" + webpack "^5.95.0" + +"@docusaurus/logger@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.8.0.tgz#c1abbb084a8058dc0047d57070fb9cd0241a679d" + integrity sha512-7eEMaFIam5Q+v8XwGqF/n0ZoCld4hV4eCCgQkfcN9Mq5inoZa6PHHW9Wu6lmgzoK5Kx3keEeABcO2SxwraoPDQ== dependencies: chalk "^4.1.2" tslib "^2.6.0" -"@docusaurus/mdx-loader@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.4.0.tgz#483d7ab57928fdbb5c8bd1678098721a930fc5f6" - integrity sha512-kSSbrrk4nTjf4d+wtBA9H+FGauf2gCax89kV8SUSJu3qaTdSIKdWERlngsiHaCFgZ7laTJ8a67UFf+xlFPtuTw== +"@docusaurus/mdx-loader@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.8.0.tgz#2b225cd2b1159cc49b10b1cac63a927a8368274b" + integrity sha512-mDPSzssRnpjSdCGuv7z2EIAnPS1MHuZGTaRLwPn4oQwszu4afjWZ/60sfKjTnjBjI8Vl4OgJl2vMmfmiNDX4Ng== dependencies: - "@docusaurus/logger" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" "@mdx-js/mdx" "^3.0.0" "@slorber/remark-comment" "^1.0.0" escape-html "^1.0.3" estree-util-value-to-estree "^3.0.1" file-loader "^6.2.0" fs-extra "^11.1.1" - image-size "^1.0.2" + image-size "^2.0.2" mdast-util-mdx "^3.0.0" mdast-util-to-string "^4.0.0" rehype-raw "^7.0.0" @@ -1469,176 +2711,206 @@ vfile "^6.0.1" webpack "^5.88.1" -"@docusaurus/module-type-aliases@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.4.0.tgz#2653bde58fc1aa3dbc626a6c08cfb63a37ae1bb8" - integrity sha512-A1AyS8WF5Bkjnb8s+guTDuYmUiwJzNrtchebBHpc0gz0PyHJNMaybUlSrmJjHVcGrya0LKI4YcR3lBDQfXRYLw== +"@docusaurus/module-type-aliases@3.8.0", "@docusaurus/module-type-aliases@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.8.0.tgz#e487052c372538c5dcf2200999e13f328fa5ffaa" + integrity sha512-/uMb4Ipt5J/QnD13MpnoC/A4EYAe6DKNWqTWLlGrqsPJwJv73vSwkA25xnYunwfqWk0FlUQfGv/Swdh5eCCg7g== dependencies: - "@docusaurus/types" "3.4.0" + "@docusaurus/types" "3.8.0" "@types/history" "^4.7.11" "@types/react" "*" "@types/react-router-config" "*" "@types/react-router-dom" "*" - react-helmet-async "*" + react-helmet-async "npm:@slorber/react-helmet-async@1.3.0" react-loadable "npm:@docusaurus/react-loadable@6.0.0" -"@docusaurus/plugin-content-blog@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.4.0.tgz#6373632fdbababbda73a13c4a08f907d7de8f007" - integrity sha512-vv6ZAj78ibR5Jh7XBUT4ndIjmlAxkijM3Sx5MAAzC1gyv0vupDQNhzuFg1USQmQVj3P5I6bquk12etPV3LJ+Xw== +"@docusaurus/plugin-content-blog@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.8.0.tgz#0c200b1fb821e09e9e975c45255e5ddfab06c392" + integrity sha512-0SlOTd9R55WEr1GgIXu+hhTT0hzARYx3zIScA5IzpdekZQesI/hKEa5LPHBd415fLkWMjdD59TaW/3qQKpJ0Lg== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/logger" "3.4.0" - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - cheerio "^1.0.0-rc.12" + "@docusaurus/core" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/mdx-loader" "3.8.0" + "@docusaurus/theme-common" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" + cheerio "1.0.0-rc.12" feed "^4.2.2" fs-extra "^11.1.1" lodash "^4.17.21" - reading-time "^1.5.0" + schema-dts "^1.1.2" srcset "^4.0.0" tslib "^2.6.0" unist-util-visit "^5.0.0" utility-types "^3.10.0" webpack "^5.88.1" -"@docusaurus/plugin-content-docs@3.4.0", "@docusaurus/plugin-content-docs@^3.0.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.4.0.tgz#3088973f72169a2a6d533afccec7153c8720d332" - integrity sha512-HkUCZffhBo7ocYheD9oZvMcDloRnGhBMOZRyVcAQRFmZPmNqSyISlXA1tQCIxW+r478fty97XXAGjNYzBjpCsg== +"@docusaurus/plugin-content-docs@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.8.0.tgz#6aedb1261da1f0c8c2fa11cfaa6df4577a9b7826" + integrity sha512-fRDMFLbUN6eVRXcjP8s3Y7HpAt9pzPYh1F/7KKXOCxvJhjjCtbon4VJW0WndEPInVz4t8QUXn5QZkU2tGVCE2g== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/logger" "3.4.0" - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/module-type-aliases" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/mdx-loader" "3.8.0" + "@docusaurus/module-type-aliases" "3.8.0" + "@docusaurus/theme-common" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" "@types/react-router-config" "^5.0.7" combine-promises "^1.1.0" fs-extra "^11.1.1" js-yaml "^4.1.0" lodash "^4.17.21" + schema-dts "^1.1.2" tslib "^2.6.0" utility-types "^3.10.0" webpack "^5.88.1" -"@docusaurus/plugin-content-pages@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.4.0.tgz#1846172ca0355c7d32a67ef8377750ce02bbb8ad" - integrity sha512-h2+VN/0JjpR8fIkDEAoadNjfR3oLzB+v1qSXbIAKjQ46JAHx3X22n9nqS+BWSQnTnp1AjkjSvZyJMekmcwxzxg== +"@docusaurus/plugin-content-pages@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.8.0.tgz#2db5f990872684c621665d0d0d8d9b5831fd2999" + integrity sha512-39EDx2y1GA0Pxfion5tQZLNJxL4gq6susd1xzetVBjVIQtwpCdyloOfQBAgX0FylqQxfJrYqL0DIUuq7rd7uBw== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/mdx-loader" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" fs-extra "^11.1.1" tslib "^2.6.0" webpack "^5.88.1" -"@docusaurus/plugin-debug@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.4.0.tgz#74e4ec5686fa314c26f3ac150bacadbba7f06948" - integrity sha512-uV7FDUNXGyDSD3PwUaf5YijX91T5/H9SX4ErEcshzwgzWwBtK37nUWPU3ZLJfeTavX3fycTOqk9TglpOLaWkCg== +"@docusaurus/plugin-css-cascade-layers@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.8.0.tgz#a0741ae32917a88ce7ce76b6f472495fa4bf576d" + integrity sha512-/VBTNymPIxQB8oA3ZQ4GFFRYdH4ZxDRRBECxyjRyv486mfUPXfcdk+im4S5mKWa6EK2JzBz95IH/Wu0qQgJ5yQ== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" + tslib "^2.6.0" + +"@docusaurus/plugin-debug@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.8.0.tgz#297c159ae99924e60042426d2ad6ee0d5e9126b3" + integrity sha512-teonJvJsDB9o2OnG6ifbhblg/PXzZvpUKHFgD8dOL1UJ58u0lk8o0ZOkvaYEBa9nDgqzoWrRk9w+e3qaG2mOhQ== + dependencies: + "@docusaurus/core" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" fs-extra "^11.1.1" - react-json-view-lite "^1.2.0" + react-json-view-lite "^2.3.0" tslib "^2.6.0" -"@docusaurus/plugin-google-analytics@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.4.0.tgz#5f59fc25329a59decc231936f6f9fb5663da3c55" - integrity sha512-mCArluxEGi3cmYHqsgpGGt3IyLCrFBxPsxNZ56Mpur0xSlInnIHoeLDH7FvVVcPJRPSQ9/MfRqLsainRw+BojA== +"@docusaurus/plugin-google-analytics@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.8.0.tgz#fb97097af331beb13553a384081dc83607539b31" + integrity sha512-aKKa7Q8+3xRSRESipNvlFgNp3FNPELKhuo48Cg/svQbGNwidSHbZT03JqbW4cBaQnyyVchO1ttk+kJ5VC9Gx0w== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" tslib "^2.6.0" -"@docusaurus/plugin-google-gtag@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.4.0.tgz#42489ac5fe1c83b5523ceedd5ef74f9aa8bc251b" - integrity sha512-Dsgg6PLAqzZw5wZ4QjUYc8Z2KqJqXxHxq3vIoyoBWiLEEfigIs7wHR+oiWUQy3Zk9MIk6JTYj7tMoQU0Jm3nqA== +"@docusaurus/plugin-google-gtag@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.8.0.tgz#b5a60006c28ac582859a469fb92e53d383b0a055" + integrity sha512-ugQYMGF4BjbAW/JIBtVcp+9eZEgT9HRdvdcDudl5rywNPBA0lct+lXMG3r17s02rrhInMpjMahN3Yc9Cb3H5/g== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" "@types/gtag.js" "^0.0.12" tslib "^2.6.0" -"@docusaurus/plugin-google-tag-manager@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.4.0.tgz#cebb03a5ffa1e70b37d95601442babea251329ff" - integrity sha512-O9tX1BTwxIhgXpOLpFDueYA9DWk69WCbDRrjYoMQtFHSkTyE7RhNgyjSPREUWJb9i+YUg3OrsvrBYRl64FCPCQ== +"@docusaurus/plugin-google-tag-manager@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.8.0.tgz#612aa63e161fb273bf7db2591034c0142951727d" + integrity sha512-9juRWxbwZD3SV02Jd9QB6yeN7eu+7T4zB0bvJLcVQwi+am51wAxn2CwbdL0YCCX+9OfiXbADE8D8Q65Hbopu/w== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" tslib "^2.6.0" -"@docusaurus/plugin-sitemap@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.4.0.tgz#b091d64d1e3c6c872050189999580187537bcbc6" - integrity sha512-+0VDvx9SmNrFNgwPoeoCha+tRoAjopwT0+pYO1xAbyLcewXSemq+eLxEa46Q1/aoOaJQ0qqHELuQM7iS2gp33Q== +"@docusaurus/plugin-sitemap@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.8.0.tgz#a39e3b5aa2f059aba0052ed11a6b4fbf78ac0dad" + integrity sha512-fGpOIyJvNiuAb90nSJ2Gfy/hUOaDu6826e5w5UxPmbpCIc7KlBHNAZ5g4L4ZuHhc4hdfq4mzVBsQSnne+8Ze1g== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/logger" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" fs-extra "^11.1.1" sitemap "^7.1.1" tslib "^2.6.0" -"@docusaurus/preset-classic@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.4.0.tgz#6082a32fbb465b0cb2c2a50ebfc277cff2c0f139" - integrity sha512-Ohj6KB7siKqZaQhNJVMBBUzT3Nnp6eTKqO+FXO3qu/n1hJl3YLwVKTWBg28LF7MWrKu46UuYavwMRxud0VyqHg== +"@docusaurus/plugin-svgr@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-svgr/-/plugin-svgr-3.8.0.tgz#6d2d43f14b32b4bb2dd8dc87a70c6e78754c1e85" + integrity sha512-kEDyry+4OMz6BWLG/lEqrNsL/w818bywK70N1gytViw4m9iAmoxCUT7Ri9Dgs7xUdzCHJ3OujolEmD88Wy44OA== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/plugin-content-blog" "3.4.0" - "@docusaurus/plugin-content-docs" "3.4.0" - "@docusaurus/plugin-content-pages" "3.4.0" - "@docusaurus/plugin-debug" "3.4.0" - "@docusaurus/plugin-google-analytics" "3.4.0" - "@docusaurus/plugin-google-gtag" "3.4.0" - "@docusaurus/plugin-google-tag-manager" "3.4.0" - "@docusaurus/plugin-sitemap" "3.4.0" - "@docusaurus/theme-classic" "3.4.0" - "@docusaurus/theme-common" "3.4.0" - "@docusaurus/theme-search-algolia" "3.4.0" - "@docusaurus/types" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" + "@svgr/core" "8.1.0" + "@svgr/webpack" "^8.1.0" + tslib "^2.6.0" + webpack "^5.88.1" -"@docusaurus/theme-classic@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.4.0.tgz#1b0f48edec3e3ec8927843554b9f11e5927b0e52" - integrity sha512-0IPtmxsBYv2adr1GnZRdMkEQt1YW6tpzrUPj02YxNpvJ5+ju4E13J5tB4nfdaen/tfR1hmpSPlTFPvTf4kwy8Q== +"@docusaurus/preset-classic@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.8.0.tgz#ac8bc17e3b7b443d8a24f2f1da0c0be396950fef" + integrity sha512-qOu6tQDOWv+rpTlKu+eJATCJVGnABpRCPuqf7LbEaQ1mNY//N/P8cHQwkpAU+aweQfarcZ0XfwCqRHJfjeSV/g== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/module-type-aliases" "3.4.0" - "@docusaurus/plugin-content-blog" "3.4.0" - "@docusaurus/plugin-content-docs" "3.4.0" - "@docusaurus/plugin-content-pages" "3.4.0" - "@docusaurus/theme-common" "3.4.0" - "@docusaurus/theme-translations" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/plugin-content-blog" "3.8.0" + "@docusaurus/plugin-content-docs" "3.8.0" + "@docusaurus/plugin-content-pages" "3.8.0" + "@docusaurus/plugin-css-cascade-layers" "3.8.0" + "@docusaurus/plugin-debug" "3.8.0" + "@docusaurus/plugin-google-analytics" "3.8.0" + "@docusaurus/plugin-google-gtag" "3.8.0" + "@docusaurus/plugin-google-tag-manager" "3.8.0" + "@docusaurus/plugin-sitemap" "3.8.0" + "@docusaurus/plugin-svgr" "3.8.0" + "@docusaurus/theme-classic" "3.8.0" + "@docusaurus/theme-common" "3.8.0" + "@docusaurus/theme-search-algolia" "3.8.0" + "@docusaurus/types" "3.8.0" + +"@docusaurus/theme-classic@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.8.0.tgz#6d44fb801b86a7c7af01cda0325af1a3300b3ac2" + integrity sha512-nQWFiD5ZjoT76OaELt2n33P3WVuuCz8Dt5KFRP2fCBo2r9JCLsp2GJjZpnaG24LZ5/arRjv4VqWKgpK0/YLt7g== + dependencies: + "@docusaurus/core" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/mdx-loader" "3.8.0" + "@docusaurus/module-type-aliases" "3.8.0" + "@docusaurus/plugin-content-blog" "3.8.0" + "@docusaurus/plugin-content-docs" "3.8.0" + "@docusaurus/plugin-content-pages" "3.8.0" + "@docusaurus/theme-common" "3.8.0" + "@docusaurus/theme-translations" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" "@mdx-js/react" "^3.0.0" clsx "^2.0.0" copy-text-to-clipboard "^3.2.0" - infima "0.2.0-alpha.43" + infima "0.2.0-alpha.45" lodash "^4.17.21" nprogress "^0.2.0" postcss "^8.4.26" @@ -1649,18 +2921,15 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-common@3.4.0", "@docusaurus/theme-common@^3.0.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.4.0.tgz#01f2b728de6cb57f6443f52fc30675cf12a5d49f" - integrity sha512-0A27alXuv7ZdCg28oPE8nH/Iz73/IUejVaCazqu9elS4ypjiLhK3KfzdSQBnL/g7YfHSlymZKdiOHEo8fJ0qMA== +"@docusaurus/theme-common@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.8.0.tgz#102c385c3d1d3b7a6b52d1911c7e88c38d9a977e" + integrity sha512-YqV2vAWpXGLA+A3PMLrOMtqgTHJLDcT+1Caa6RF7N4/IWgrevy5diY8oIHFkXR/eybjcrFFjUPrHif8gSGs3Tw== dependencies: - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/module-type-aliases" "3.4.0" - "@docusaurus/plugin-content-blog" "3.4.0" - "@docusaurus/plugin-content-docs" "3.4.0" - "@docusaurus/plugin-content-pages" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" + "@docusaurus/mdx-loader" "3.8.0" + "@docusaurus/module-type-aliases" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" "@types/history" "^4.7.11" "@types/react" "*" "@types/react-router-config" "*" @@ -1670,34 +2939,34 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-mermaid@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-mermaid/-/theme-mermaid-3.4.0.tgz#ef1d2231d0858767f67538b4fafd7d0ce2a3e845" - integrity sha512-3w5QW0HEZ2O6x2w6lU3ZvOe1gNXP2HIoKDMJBil1VmLBc9PmpAG17VmfhI/p3L2etNmOiVs5GgniUqvn8AFEGQ== +"@docusaurus/theme-mermaid@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-mermaid/-/theme-mermaid-3.8.0.tgz#f0720ec89fd386870f30978ba984b1b126ca92a5" + integrity sha512-ou0NJM37p4xrVuFaZp8qFe5Z/qBq9LuyRTP4KKRa0u2J3zC4f3saBJDgc56FyvvN1OsmU0189KGEPUjTr6hFxg== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/module-type-aliases" "3.4.0" - "@docusaurus/theme-common" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - mermaid "^10.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/module-type-aliases" "3.8.0" + "@docusaurus/theme-common" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" + mermaid ">=11.6.0" tslib "^2.6.0" -"@docusaurus/theme-search-algolia@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.4.0.tgz#c499bad71d668df0d0f15b0e5e33e2fc4e330fcc" - integrity sha512-aiHFx7OCw4Wck1z6IoShVdUWIjntC8FHCw9c5dR8r3q4Ynh+zkS8y2eFFunN/DL6RXPzpnvKCg3vhLQYJDmT9Q== +"@docusaurus/theme-search-algolia@3.8.0", "@docusaurus/theme-search-algolia@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.8.0.tgz#21c2f18e07a73d13ca3b44fcf0ae9aac33bef60f" + integrity sha512-GBZ5UOcPgiu6nUw153+0+PNWvFKweSnvKIL6Rp04H9olKb475jfKjAwCCtju5D2xs5qXHvCMvzWOg5o9f6DtuQ== dependencies: - "@docsearch/react" "^3.5.2" - "@docusaurus/core" "3.4.0" - "@docusaurus/logger" "3.4.0" - "@docusaurus/plugin-content-docs" "3.4.0" - "@docusaurus/theme-common" "3.4.0" - "@docusaurus/theme-translations" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - algoliasearch "^4.18.0" - algoliasearch-helper "^3.13.3" + "@docsearch/react" "^3.9.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/plugin-content-docs" "3.8.0" + "@docusaurus/theme-common" "3.8.0" + "@docusaurus/theme-translations" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" + algoliasearch "^5.17.1" + algoliasearch-helper "^3.22.6" clsx "^2.0.0" eta "^2.2.0" fs-extra "^11.1.1" @@ -1705,15 +2974,30 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-translations@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.4.0.tgz#e6355d01352886c67e38e848b2542582ea3070af" - integrity sha512-zSxCSpmQCCdQU5Q4CnX/ID8CSUUI3fvmq4hU/GNP/XoAWtXo9SAVnM3TzpU8Gb//H3WCsT8mJcTfyOk3d9ftNg== +"@docusaurus/theme-translations@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.8.0.tgz#deb64dccab74361624c3cb352a4949a7ac868c74" + integrity sha512-1DTy/snHicgkCkryWq54fZvsAglTdjTx4qjOXgqnXJ+DIty1B+aPQrAVUu8LiM+6BiILfmNxYsxhKTj+BS3PZg== dependencies: fs-extra "^11.1.1" tslib "^2.6.0" -"@docusaurus/types@3.4.0", "@docusaurus/types@^3.0.0": +"@docusaurus/types@3.8.0", "@docusaurus/types@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.8.0.tgz#f6cd31c4e3e392e0270b8137d7fe4365ea7a022e" + integrity sha512-RDEClpwNxZq02c+JlaKLWoS13qwWhjcNsi2wG1UpzmEnuti/z1Wx4SGpqbUqRPNSd8QWWePR8Cb7DvG0VN/TtA== + dependencies: + "@mdx-js/mdx" "^3.0.0" + "@types/history" "^4.7.11" + "@types/react" "*" + commander "^5.1.0" + joi "^17.9.2" + react-helmet-async "npm:@slorber/react-helmet-async@1.3.0" + utility-types "^3.10.0" + webpack "^5.95.0" + webpack-merge "^5.9.0" + +"@docusaurus/types@^3.0.0": version "3.4.0" resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.4.0.tgz#237c3f737e9db3f7c1a5935a3ef48d6eadde8292" integrity sha512-4jcDO8kXi5Cf9TcyikB/yKmz14f2RZ2qTRerbHAsS+5InE9ZgSLBNLsewtFTcTOXSVcbU3FoGOzcNWAmU1TR0A== @@ -1728,36 +3012,38 @@ webpack "^5.88.1" webpack-merge "^5.9.0" -"@docusaurus/utils-common@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.4.0.tgz#2a43fefd35b85ab9fcc6833187e66c15f8bfbbc6" - integrity sha512-NVx54Wr4rCEKsjOH5QEVvxIqVvm+9kh7q8aYTU5WzUU9/Hctd6aTrcZ3G0Id4zYJ+AeaG5K5qHA4CY5Kcm2iyQ== +"@docusaurus/utils-common@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.8.0.tgz#2b1a6b1ec4a7fac62f1898d523d42f8cc4a8258f" + integrity sha512-3TGF+wVTGgQ3pAc9+5jVchES4uXUAhAt9pwv7uws4mVOxL4alvU3ue/EZ+R4XuGk94pDy7CNXjRXpPjlfZXQfw== dependencies: + "@docusaurus/types" "3.8.0" tslib "^2.6.0" -"@docusaurus/utils-validation@3.4.0", "@docusaurus/utils-validation@^3.0.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.4.0.tgz#0176f6e503ff45f4390ec2ecb69550f55e0b5eb7" - integrity sha512-hYQ9fM+AXYVTWxJOT1EuNaRnrR2WGpRdLDQG07O8UOpsvCPWUVOeo26Rbm0JWY2sGLfzAb+tvJ62yF+8F+TV0g== +"@docusaurus/utils-validation@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.8.0.tgz#aa02e9d998e20998fbcaacd94873878bc3b9a4cd" + integrity sha512-MrnEbkigr54HkdFeg8e4FKc4EF+E9dlVwsY3XQZsNkbv3MKZnbHQ5LsNJDIKDROFe8PBf5C4qCAg5TPBpsjrjg== dependencies: - "@docusaurus/logger" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" fs-extra "^11.2.0" joi "^17.9.2" js-yaml "^4.1.0" lodash "^4.17.21" tslib "^2.6.0" -"@docusaurus/utils@3.4.0", "@docusaurus/utils@^3.0.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.4.0.tgz#c508e20627b7a55e2b541e4a28c95e0637d6a204" - integrity sha512-fRwnu3L3nnWaXOgs88BVBmG1yGjcQqZNHG+vInhEa2Sz2oQB+ZjbEMO5Rh9ePFpZ0YDiDUhpaVjwmS+AU2F14g== +"@docusaurus/utils@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.8.0.tgz#92bad89d2a11f5f246196af153093b12cd79f9ac" + integrity sha512-2wvtG28ALCN/A1WCSLxPASFBFzXCnP0YKCAFIPcvEb6imNu1wg7ni/Svcp71b3Z2FaOFFIv4Hq+j4gD7gA0yfQ== dependencies: - "@docusaurus/logger" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@svgr/webpack" "^8.1.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils-common" "3.8.0" escape-string-regexp "^4.0.0" + execa "5.1.1" file-loader "^6.2.0" fs-extra "^11.1.1" github-slugger "^1.5.0" @@ -1767,9 +3053,9 @@ js-yaml "^4.1.0" lodash "^4.17.21" micromatch "^4.0.5" + p-queue "^6.6.2" prompts "^2.4.2" resolve-pathname "^3.0.0" - shelljs "^0.8.5" tslib "^2.6.0" url-loader "^4.1.1" utility-types "^3.10.0" @@ -1815,6 +3101,25 @@ resolved "https://registry.yarnpkg.com/@hookform/error-message/-/error-message-2.0.1.tgz#6a37419106e13664ad6a29c9dae699ae6cd276b8" integrity sha512-U410sAr92xgxT1idlu9WWOVjndxLdgPUHEB8Schr27C9eh7/xUnITWpCMF93s+lGiG++D4JnbSnrb5A21AdSNg== +"@iconify/types@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@iconify/types/-/types-2.0.0.tgz#ab0e9ea681d6c8a1214f30cd741fe3a20cc57f57" + integrity sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg== + +"@iconify/utils@^2.1.33": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@iconify/utils/-/utils-2.3.0.tgz#1bbbf8c477ebe9a7cacaea78b1b7e8937f9cbfba" + integrity sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA== + dependencies: + "@antfu/install-pkg" "^1.0.0" + "@antfu/utils" "^8.1.0" + "@iconify/types" "^2.0.0" + debug "^4.4.0" + globals "^15.14.0" + kolorist "^1.8.0" + local-pkg "^1.0.0" + mlly "^1.7.4" + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -1932,6 +3237,56 @@ dependencies: "@types/mdx" "^2.0.0" +"@mermaid-js/parser@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@mermaid-js/parser/-/parser-0.4.0.tgz#c1de1f5669f8fcbd0d0c9d124927d36ddc00d8a6" + integrity sha512-wla8XOWvQAwuqy+gxiZqY+c7FokraOTHRWMsbB4AgRx9Sy7zKslNyejy7E+a77qHfey5GXw/ik3IXv/NHMJgaA== + dependencies: + langium "3.3.1" + +"@module-federation/error-codes@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@module-federation/error-codes/-/error-codes-0.14.0.tgz#d54581bfb998ce9ace4cb33f8795c644e461bfeb" + integrity sha512-GGk+EoeSACJikZZyShnLshtq9E2eCrDWbRiB4QAFXCX4oYmGgFfzXlx59vMNwqTKPJWxkEGnPYacJMcr2YYjag== + +"@module-federation/runtime-core@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@module-federation/runtime-core/-/runtime-core-0.14.0.tgz#a00a3666cc25a8bb822a36552c631e6e3f7326cc" + integrity sha512-fGE1Ro55zIFDp/CxQuRhKQ1pJvG7P0qvRm2N+4i8z++2bgDjcxnCKUqDJ8lLD+JfJQvUJf0tuSsJPgevzueD4g== + dependencies: + "@module-federation/error-codes" "0.14.0" + "@module-federation/sdk" "0.14.0" + +"@module-federation/runtime-tools@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@module-federation/runtime-tools/-/runtime-tools-0.14.0.tgz#f5b5f3d19605b6d7c90ed278dc00a5400f1aa49d" + integrity sha512-y/YN0c2DKsLETE+4EEbmYWjqF9G6ZwgZoDIPkaQ9p0pQu0V4YxzWfQagFFxR0RigYGuhJKmSU/rtNoHq+qF8jg== + dependencies: + "@module-federation/runtime" "0.14.0" + "@module-federation/webpack-bundler-runtime" "0.14.0" + +"@module-federation/runtime@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@module-federation/runtime/-/runtime-0.14.0.tgz#e04012d2d928275fd00525904c0b4f8be514dc70" + integrity sha512-kR3cyHw/Y64SEa7mh4CHXOEQYY32LKLK75kJOmBroLNLO7/W01hMNAvGBYTedS7hWpVuefPk1aFZioy3q2VLdQ== + dependencies: + "@module-federation/error-codes" "0.14.0" + "@module-federation/runtime-core" "0.14.0" + "@module-federation/sdk" "0.14.0" + +"@module-federation/sdk@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@module-federation/sdk/-/sdk-0.14.0.tgz#efa38341b7601f58967397cc630068f69691b931" + integrity sha512-lg/OWRsh18hsyTCamOOhEX546vbDiA2O4OggTxxH2wTGr156N6DdELGQlYIKfRdU/0StgtQS81Goc0BgDZlx9A== + +"@module-federation/webpack-bundler-runtime@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.14.0.tgz#21a82505f95fdb3cb202786f8dc20611b4d7f93c" + integrity sha512-POWS6cKBicAAQ3DNY5X7XEUSfOfUsRaBNxbuwEfSGlrkTE9UcWheO06QP2ndHi8tHQuUKcIHi2navhPkJ+k5xg== + dependencies: + "@module-federation/runtime" "0.14.0" + "@module-federation/sdk" "0.14.0" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1953,6 +3308,95 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@parcel/watcher-android-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz#507f836d7e2042f798c7d07ad19c3546f9848ac1" + integrity sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA== + +"@parcel/watcher-darwin-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz#3d26dce38de6590ef79c47ec2c55793c06ad4f67" + integrity sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw== + +"@parcel/watcher-darwin-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz#99f3af3869069ccf774e4ddfccf7e64fd2311ef8" + integrity sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg== + +"@parcel/watcher-freebsd-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz#14d6857741a9f51dfe51d5b08b7c8afdbc73ad9b" + integrity sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ== + +"@parcel/watcher-linux-arm-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz#43c3246d6892381db473bb4f663229ad20b609a1" + integrity sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA== + +"@parcel/watcher-linux-arm-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz#663750f7090bb6278d2210de643eb8a3f780d08e" + integrity sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q== + +"@parcel/watcher-linux-arm64-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz#ba60e1f56977f7e47cd7e31ad65d15fdcbd07e30" + integrity sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w== + +"@parcel/watcher-linux-arm64-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz#f7fbcdff2f04c526f96eac01f97419a6a99855d2" + integrity sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg== + +"@parcel/watcher-linux-x64-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz#4d2ea0f633eb1917d83d483392ce6181b6a92e4e" + integrity sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A== + +"@parcel/watcher-linux-x64-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz#277b346b05db54f55657301dd77bdf99d63606ee" + integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg== + +"@parcel/watcher-win32-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz#7e9e02a26784d47503de1d10e8eab6cceb524243" + integrity sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw== + +"@parcel/watcher-win32-ia32@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz#2d0f94fa59a873cdc584bf7f6b1dc628ddf976e6" + integrity sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ== + +"@parcel/watcher-win32-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz#ae52693259664ba6f2228fa61d7ee44b64ea0947" + integrity sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA== + +"@parcel/watcher@^2.4.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.1.tgz#342507a9cfaaf172479a882309def1e991fb1200" + integrity sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg== + dependencies: + detect-libc "^1.0.3" + is-glob "^4.0.3" + micromatch "^4.0.5" + node-addon-api "^7.0.0" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.5.1" + "@parcel/watcher-darwin-arm64" "2.5.1" + "@parcel/watcher-darwin-x64" "2.5.1" + "@parcel/watcher-freebsd-x64" "2.5.1" + "@parcel/watcher-linux-arm-glibc" "2.5.1" + "@parcel/watcher-linux-arm-musl" "2.5.1" + "@parcel/watcher-linux-arm64-glibc" "2.5.1" + "@parcel/watcher-linux-arm64-musl" "2.5.1" + "@parcel/watcher-linux-x64-glibc" "2.5.1" + "@parcel/watcher-linux-x64-musl" "2.5.1" + "@parcel/watcher-win32-arm64" "2.5.1" + "@parcel/watcher-win32-ia32" "2.5.1" + "@parcel/watcher-win32-x64" "2.5.1" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -2025,6 +3469,81 @@ redux-thunk "^2.4.2" reselect "^4.1.8" +"@rspack/binding-darwin-arm64@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.3.12.tgz#2e7cc00b813dcb155572908d956ab1d75d9747a5" + integrity sha512-8hKjVTBeWPqkMzFPNWIh72oU9O3vFy3e88wRjMPImDCXBiEYrKqGTTLd/J0SO+efdL3SBD1rX1IvdJpxCv6Yrw== + +"@rspack/binding-darwin-x64@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.3.12.tgz#cb148cc62658d74204621a695e1698cda82877f9" + integrity sha512-Sj4m+mCUxL7oCpdu7OmWT7fpBM7hywk5CM9RDc3D7StaBZbvNtNftafCrTZzTYKuZrKmemTh5SFzT5Tz7tf6GA== + +"@rspack/binding-linux-arm64-gnu@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.3.12.tgz#79820857dfbd3819e0026da507c271531c013d0c" + integrity sha512-7MuOxf3/Mhv4mgFdLTvgnt/J+VouNR65DEhorth+RZm3LEWojgoFEphSAMAvpvAOpYSS68Sw4SqsOZi719ia2w== + +"@rspack/binding-linux-arm64-musl@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.3.12.tgz#a794dfe8df6bf75af217597881592add6f6b046e" + integrity sha512-s6KKj20T9Z1bA8caIjU6EzJbwyDo1URNFgBAlafCT2UC6yX7flstDJJ38CxZacA9A2P24RuQK2/jPSZpWrTUFA== + +"@rspack/binding-linux-x64-gnu@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.3.12.tgz#a0e23831a1374d9039a4b29e927cef58a485085c" + integrity sha512-0w/sRREYbRgHgWvs2uMEJSLfvzbZkPHUg6CMcYQGNVK6axYRot6jPyKetyFYA9pR5fB5rsXegpnFaZaVrRIK2g== + +"@rspack/binding-linux-x64-musl@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.3.12.tgz#61620efda6a6805689890c130e6b38c1725baaf4" + integrity sha512-jEdxkPymkRxbijDRsBGdhopcbGXiXDg59lXqIRkVklqbDmZ/O6DHm7gImmlx5q9FoWbz0gqJuOKBz4JqWxjWVA== + +"@rspack/binding-win32-arm64-msvc@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.3.12.tgz#46d874df8bd5b84e82ea83480969f7e0293ccd82" + integrity sha512-ZRvUCb3TDLClAqcTsl/o9UdJf0B5CgzAxgdbnYJbldyuyMeTUB4jp20OfG55M3C2Nute2SNhu2bOOp9Se5Ongw== + +"@rspack/binding-win32-ia32-msvc@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.3.12.tgz#fec435e31e56f3d58b4fa52746643d1d593b2b89" + integrity sha512-1TKPjuXStPJr14f3ZHuv40Xc/87jUXx10pzVtrPnw+f3hckECHrbYU/fvbVzZyuXbsXtkXpYca6ygCDRJAoNeQ== + +"@rspack/binding-win32-x64-msvc@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.3.12.tgz#33b73cbab75920cf8a92e7245a794970f8508b6c" + integrity sha512-lCR0JfnYKpV+a6r2A2FdxyUKUS4tajePgpPJN5uXDgMGwrDtRqvx+d0BHhwjFudQVJq9VVbRaL89s2MQ6u+xYw== + +"@rspack/binding@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding/-/binding-1.3.12.tgz#0a8356fdbd89f08cda3e9bb8aff4ea781dfe972e" + integrity sha512-4Ic8lV0+LCBfTlH5aIOujIRWZOtgmG223zC4L3o8WY/+ESAgpdnK6lSSMfcYgRanYLAy3HOmFIp20jwskMpbAg== + optionalDependencies: + "@rspack/binding-darwin-arm64" "1.3.12" + "@rspack/binding-darwin-x64" "1.3.12" + "@rspack/binding-linux-arm64-gnu" "1.3.12" + "@rspack/binding-linux-arm64-musl" "1.3.12" + "@rspack/binding-linux-x64-gnu" "1.3.12" + "@rspack/binding-linux-x64-musl" "1.3.12" + "@rspack/binding-win32-arm64-msvc" "1.3.12" + "@rspack/binding-win32-ia32-msvc" "1.3.12" + "@rspack/binding-win32-x64-msvc" "1.3.12" + +"@rspack/core@^1.3.10": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/core/-/core-1.3.12.tgz#68df0111cfac7e8f9dfa11a608ac8731181b5483" + integrity sha512-mAPmV4LPPRgxpouUrGmAE4kpF1NEWJGyM5coebsjK/zaCMSjw3mkdxiU2b5cO44oIi0Ifv5iGkvwbdrZOvMyFA== + dependencies: + "@module-federation/runtime-tools" "0.14.0" + "@rspack/binding" "1.3.12" + "@rspack/lite-tapable" "1.0.1" + caniuse-lite "^1.0.30001718" + +"@rspack/lite-tapable@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@rspack/lite-tapable/-/lite-tapable-1.0.1.tgz#d4540a5d28bd6177164bc0ba0bee4bdec0458591" + integrity sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w== + "@sideway/address@^4.1.5": version "4.1.5" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" @@ -2172,84 +3691,152 @@ "@svgr/plugin-jsx" "8.1.0" "@svgr/plugin-svgo" "8.1.0" -"@swc/core-darwin-arm64@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.1.tgz#72d861fb7094b7a0004f4f300e2c5d4ea1549d9e" - integrity sha512-u6GdwOXsOEdNAdSI6nWq6G2BQw5HiSNIZVcBaH1iSvBnxZvWbnIKyDiZKaYnDwTLHLzig2GuUjjE2NaCJPy4jg== +"@swc/core-darwin-arm64@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.29.tgz#bf66e3f4f00e6fe9d95e8a33f780e6c40fca946d" + integrity sha512-whsCX7URzbuS5aET58c75Dloby3Gtj/ITk2vc4WW6pSDQKSPDuONsIcZ7B2ng8oz0K6ttbi4p3H/PNPQLJ4maQ== -"@swc/core-darwin-x64@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.6.1.tgz#8b7070fcee4a4570d0af245c4614ca4e492dfd5b" - integrity sha512-/tXwQibkDNLVbAtr7PUQI0iQjoB708fjhDDDfJ6WILSBVZ3+qs/LHjJ7jHwumEYxVq1XA7Fv2Q7SE/ZSQoWHcQ== +"@swc/core-darwin-x64@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.11.29.tgz#0a77d2d79ef2c789f9d40a86784bbf52c5f9877f" + integrity sha512-S3eTo/KYFk+76cWJRgX30hylN5XkSmjYtCBnM4jPLYn7L6zWYEPajsFLmruQEiTEDUg0gBEWLMNyUeghtswouw== -"@swc/core-linux-arm-gnueabihf@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.1.tgz#bea6d2e75127bbc65a664284f012ffa90c8325d5" - integrity sha512-aDgipxhJTms8iH78emHVutFR2c16LNhO+NTRCdYi+X4PyIn58/DyYTH6VDZ0AeEcS5f132ZFldU5AEgExwihXA== +"@swc/core-linux-arm-gnueabihf@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.29.tgz#80fa3a6a36034ffdbbba73e26c8f27cb13111a33" + integrity sha512-o9gdshbzkUMG6azldHdmKklcfrcMx+a23d/2qHQHPDLUPAN+Trd+sDQUYArK5Fcm7TlpG4sczz95ghN0DMkM7g== -"@swc/core-linux-arm64-gnu@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.1.tgz#5c84d804ec23cf54b31c0bc0b4bdd30ec5d43ce8" - integrity sha512-XkJ+eO4zUKG5g458RyhmKPyBGxI0FwfWFgpfIj5eDybxYJ6s4HBT5MoxyBLorB5kMlZ0XoY/usUMobPVY3nL0g== +"@swc/core-linux-arm64-gnu@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.29.tgz#42da87f445bc3e26da01d494246884006d9b9a1a" + integrity sha512-sLoaciOgUKQF1KX9T6hPGzvhOQaJn+3DHy4LOHeXhQqvBgr+7QcZ+hl4uixPKTzxk6hy6Hb0QOvQEdBAAR1gXw== -"@swc/core-linux-arm64-musl@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.1.tgz#e167a350bec12caebc97304068c3ffbad6c398ce" - integrity sha512-dr6YbLBg/SsNxs1hDqJhxdcrS8dGMlOXJwXIrUvACiA8jAd6S5BxYCaqsCefLYXtaOmu0bbx1FB/evfodqB70Q== +"@swc/core-linux-arm64-musl@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.29.tgz#c9cec610525dc9e9b11ef26319db3780812dfa54" + integrity sha512-PwjB10BC0N+Ce7RU/L23eYch6lXFHz7r3NFavIcwDNa/AAqywfxyxh13OeRy+P0cg7NDpWEETWspXeI4Ek8otw== -"@swc/core-linux-x64-gnu@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.1.tgz#fdd4e1d63b3e53d195e2ddcb9cb5ad9f31995796" - integrity sha512-A0b/3V+yFy4LXh3O9umIE7LXPC7NBWdjl6AQYqymSMcMu0EOb1/iygA6s6uWhz9y3e172Hpb9b/CGsuD8Px/bg== +"@swc/core-linux-x64-gnu@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.29.tgz#1cda2df38a4ab8905ba6ac3aa16e4ad710b6f2de" + integrity sha512-i62vBVoPaVe9A3mc6gJG07n0/e7FVeAvdD9uzZTtGLiuIfVfIBta8EMquzvf+POLycSk79Z6lRhGPZPJPYiQaA== -"@swc/core-linux-x64-musl@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.1.tgz#81a312dd9e62da5f4c48e3cd23b6c6d28a31ac42" - integrity sha512-5dJjlzZXhC87nZZZWbpiDP8kBIO0ibis893F/rtPIQBI5poH+iJuA32EU3wN4/WFHeK4et8z6SGSVghPtWyk4g== +"@swc/core-linux-x64-musl@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.29.tgz#5d634efff33f47c8d6addd84291ab606903d1cfd" + integrity sha512-YER0XU1xqFdK0hKkfSVX1YIyCvMDI7K07GIpefPvcfyNGs38AXKhb2byySDjbVxkdl4dycaxxhRyhQ2gKSlsFQ== -"@swc/core-win32-arm64-msvc@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.1.tgz#e131f579a69c5d807013e54ccb311e10caa27bcb" - integrity sha512-HBi1ZlwvfcUibLtT3g/lP57FaDPC799AD6InolB2KSgkqyBbZJ9wAXM8/CcH67GLIP0tZ7FqblrJTzGXxetTJQ== +"@swc/core-win32-arm64-msvc@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.29.tgz#bc54f2e3f8f180113b7a092b1ee1eaaab24df62b" + integrity sha512-po+WHw+k9g6FAg5IJ+sMwtA/fIUL3zPQ4m/uJgONBATCVnDDkyW6dBA49uHNVtSEvjvhuD8DVWdFP847YTcITw== -"@swc/core-win32-ia32-msvc@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.1.tgz#9f3d88cf0e826aa8222a695177a065ed2899eb21" - integrity sha512-AKqHohlWERclexar5y6ux4sQ8yaMejEXNxeKXm7xPhXrp13/1p4/I3E5bPVX/jMnvpm4HpcKSP0ee2WsqmhhPw== +"@swc/core-win32-ia32-msvc@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.29.tgz#f1df344c06283643d1fe66c6931b350347b73722" + integrity sha512-h+NjOrbqdRBYr5ItmStmQt6x3tnhqgwbj9YxdGPepbTDamFv7vFnhZR0YfB3jz3UKJ8H3uGJ65Zw1VsC+xpFkg== -"@swc/core-win32-x64-msvc@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.1.tgz#b2082710bc46c484a2c9f2e33a15973806e5031d" - integrity sha512-0dLdTLd+ONve8kgC5T6VQ2Y5G+OZ7y0ujjapnK66wpvCBM6BKYGdT/OKhZKZydrC5gUKaxFN6Y5oOt9JOFUrOQ== +"@swc/core-win32-x64-msvc@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.29.tgz#a6f9dc1df66c8db96d70091abedd78cc52544724" + integrity sha512-Q8cs2BDV9wqDvqobkXOYdC+pLUSEpX/KvI0Dgfun1F+LzuLotRFuDhrvkU9ETJA6OnD2+Fn/ieHgloiKA/Mn/g== -"@swc/core@^1.3.74": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.6.1.tgz#a899a205cfaa8e23f805451ef4787987e03b8920" - integrity sha512-Yz5uj5hNZpS5brLtBvKY0L4s2tBAbQ4TjmW8xF1EC3YLFxQRrUjMP49Zm1kp/KYyYvTkSaG48Ffj2YWLu9nChw== +"@swc/core@^1.7.39": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.11.29.tgz#bce20113c47fcd6251d06262b8b8c063f8e86a20" + integrity sha512-g4mThMIpWbNhV8G2rWp5a5/Igv8/2UFRJx2yImrLGMgrDDYZIopqZ/z0jZxDgqNA1QDx93rpwNF7jGsxVWcMlA== dependencies: "@swc/counter" "^0.1.3" - "@swc/types" "^0.1.8" + "@swc/types" "^0.1.21" optionalDependencies: - "@swc/core-darwin-arm64" "1.6.1" - "@swc/core-darwin-x64" "1.6.1" - "@swc/core-linux-arm-gnueabihf" "1.6.1" - "@swc/core-linux-arm64-gnu" "1.6.1" - "@swc/core-linux-arm64-musl" "1.6.1" - "@swc/core-linux-x64-gnu" "1.6.1" - "@swc/core-linux-x64-musl" "1.6.1" - "@swc/core-win32-arm64-msvc" "1.6.1" - "@swc/core-win32-ia32-msvc" "1.6.1" - "@swc/core-win32-x64-msvc" "1.6.1" + "@swc/core-darwin-arm64" "1.11.29" + "@swc/core-darwin-x64" "1.11.29" + "@swc/core-linux-arm-gnueabihf" "1.11.29" + "@swc/core-linux-arm64-gnu" "1.11.29" + "@swc/core-linux-arm64-musl" "1.11.29" + "@swc/core-linux-x64-gnu" "1.11.29" + "@swc/core-linux-x64-musl" "1.11.29" + "@swc/core-win32-arm64-msvc" "1.11.29" + "@swc/core-win32-ia32-msvc" "1.11.29" + "@swc/core-win32-x64-msvc" "1.11.29" "@swc/counter@^0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== -"@swc/types@^0.1.8": - version "0.1.8" - resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.8.tgz#2c81d107c86cfbd0c3a05ecf7bb54c50dfa58a95" - integrity sha512-RNFA3+7OJFNYY78x0FYwi1Ow+iF1eF5WvmfY1nXPOEH4R2p/D4Cr1vzje7dNAI2aLFqpv8Wyz4oKSWqIZArpQA== +"@swc/html-darwin-arm64@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-darwin-arm64/-/html-darwin-arm64-1.11.29.tgz#7bd6d10115ffe155ecd757387b5aff318b02b5a0" + integrity sha512-q53kn/HI0n/+pecsOB2gxqITbRAhtBG7VI520SIWuCGXHPsTQ/1VOrhLMNvyfw1xVhRyFal7BpAvfGUORCl0sw== + +"@swc/html-darwin-x64@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-darwin-x64/-/html-darwin-x64-1.11.29.tgz#ccafed56081932ffaa51be1788cef88eb9a144d1" + integrity sha512-YfQPjh5WoDqOxsA7vDOOSnxEPc1Ki4SuZ0ufR4t8jYdMOFsU3AhZQ/sgBZLpTzegBTutUn7/7yy8VSoFngeR7Q== + +"@swc/html-linux-arm-gnueabihf@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-linux-arm-gnueabihf/-/html-linux-arm-gnueabihf-1.11.29.tgz#e784a1a0f69034e9dd52a9019ff80f0d5eb91433" + integrity sha512-dC3aEv1mqAUkY9TiZWOE2IcYpvxJzw0LdvkDzGW5072JSlZZYQMqq2Llwg63LIp6qBlj1JLHMLnBqk7Ubatmjw== + +"@swc/html-linux-arm64-gnu@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.11.29.tgz#ef015d81d3a011d6c273a428eee130b3aac790b7" + integrity sha512-seo+lCiBUggTR9NsHE4qVC+7+XIfLHK7yxWiIsXb8nNAXDcqVZ0Rxv8O1Y1GTeJfUlcCt1koahCG2AeyWpYFBg== + +"@swc/html-linux-arm64-musl@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-linux-arm64-musl/-/html-linux-arm64-musl-1.11.29.tgz#b20e4b442287367c4c1d62db2b8065106542f432" + integrity sha512-bK8K6t3hHgaZZ1vMNaZ+8x42EWJPEX1Dx4zi6ulMhKa1uan+DjW5SiMlUg0an16fFSYfE+r9oFC4cFEbGP1o4Q== + +"@swc/html-linux-x64-gnu@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.11.29.tgz#c45505b3e22c02dc8bdef8b3da48ba855527a62c" + integrity sha512-34tSms5TkRUCr+J6uuSE/11ECcfIpp5R1ODuIgxZRUd/u88pQGKzLVNLWGPLw4b3cZSjnAn+PFJl7BtaYl0UyQ== + +"@swc/html-linux-x64-musl@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-linux-x64-musl/-/html-linux-x64-musl-1.11.29.tgz#86116e7db3cf02b1a627bc94c374d26ce6f9d68a" + integrity sha512-oJLLrX94ccaniWdQt8PH6K2u8aN/ehBo/YPg84LycFtaud/k73Fa1kh6Neq8vbWI4CugIWTl4LXWoHm+l+QYeA== + +"@swc/html-win32-arm64-msvc@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-win32-arm64-msvc/-/html-win32-arm64-msvc-1.11.29.tgz#cf7e57b8c0b52f7f93abc307b0cb78d8213b3c13" + integrity sha512-nw4TCFfA4YV6jicRdicJZPKW+ihOZPMKEG/4bj1/6HqXw1T2pXI070ASOLE0KOHYuoyV/jConEHfIjlU0olneA== + +"@swc/html-win32-ia32-msvc@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-win32-ia32-msvc/-/html-win32-ia32-msvc-1.11.29.tgz#61c91409c3fcdf942891c02aa2a2eac1892d1907" + integrity sha512-rO6X4qOofGpKV8pyZ7VblJn+J3PHEqeWHJkJfzwP7c04Flr1oLyuLbTU8lwf8enXrTAZqitHZs+OpofKcUwHEw== + +"@swc/html-win32-x64-msvc@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-win32-x64-msvc/-/html-win32-x64-msvc-1.11.29.tgz#0e78b507d2bf28315487655852008cdecfe84535" + integrity sha512-GSCihzBItEPJAeLzkAtw0ZGbxRGMsGt1Z1ugo0uHva1R3Eybkqu9qoax1tGAON+EJzeiHRqphhNgh8MVDpnKnQ== + +"@swc/html@^1.7.39": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html/-/html-1.11.29.tgz#6e9e1b8ea65baa0d6f25cb883565a5e7d22d2858" + integrity sha512-Tsk/o6Eo3lDvHPGjLqVwXGEdC1bemGzByPWx/TrF5N7qEsanRblPeRcJzLl6LbWa80pRYIRB6T4VqdXXZqklaw== + dependencies: + "@swc/counter" "^0.1.3" + optionalDependencies: + "@swc/html-darwin-arm64" "1.11.29" + "@swc/html-darwin-x64" "1.11.29" + "@swc/html-linux-arm-gnueabihf" "1.11.29" + "@swc/html-linux-arm64-gnu" "1.11.29" + "@swc/html-linux-arm64-musl" "1.11.29" + "@swc/html-linux-x64-gnu" "1.11.29" + "@swc/html-linux-x64-musl" "1.11.29" + "@swc/html-win32-arm64-msvc" "1.11.29" + "@swc/html-win32-ia32-msvc" "1.11.29" + "@swc/html-win32-x64-msvc" "1.11.29" + +"@swc/types@^0.1.21": + version "0.1.21" + resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.21.tgz#6fcadbeca1d8bc89e1ab3de4948cef12344a38c0" + integrity sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ== dependencies: "@swc/counter" "^0.1.3" @@ -2314,23 +3901,216 @@ dependencies: "@types/node" "*" -"@types/d3-scale-chromatic@^3.0.0": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz#fc0db9c10e789c351f4c42d96f31f2e4df8f5644" - integrity sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw== +"@types/d3-array@*": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" + integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== -"@types/d3-scale@^4.0.3": - version "4.0.8" - resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb" - integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ== +"@types/d3-axis@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.6.tgz#e760e5765b8188b1defa32bc8bb6062f81e4c795" + integrity sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-brush@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.6.tgz#c2f4362b045d472e1b186cdbec329ba52bdaee6c" + integrity sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-chord@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.6.tgz#1706ca40cf7ea59a0add8f4456efff8f8775793d" + integrity sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg== + +"@types/d3-color@*": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" + integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== + +"@types/d3-contour@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.6.tgz#9ada3fa9c4d00e3a5093fed0356c7ab929604231" + integrity sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg== + dependencies: + "@types/d3-array" "*" + "@types/geojson" "*" + +"@types/d3-delaunay@*": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz#185c1a80cc807fdda2a3fe960f7c11c4a27952e1" + integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw== + +"@types/d3-dispatch@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz#096efdf55eb97480e3f5621ff9a8da552f0961e7" + integrity sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ== + +"@types/d3-drag@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02" + integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-dsv@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz#0a351f996dc99b37f4fa58b492c2d1c04e3dac17" + integrity sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g== + +"@types/d3-ease@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" + integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== + +"@types/d3-fetch@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz#c04a2b4f23181aa376f30af0283dbc7b3b569980" + integrity sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA== + dependencies: + "@types/d3-dsv" "*" + +"@types/d3-force@*": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.10.tgz#6dc8fc6e1f35704f3b057090beeeb7ac674bff1a" + integrity sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw== + +"@types/d3-format@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.4.tgz#b1e4465644ddb3fdf3a263febb240a6cd616de90" + integrity sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g== + +"@types/d3-geo@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.1.0.tgz#b9e56a079449174f0a2c8684a9a4df3f60522440" + integrity sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ== + dependencies: + "@types/geojson" "*" + +"@types/d3-hierarchy@*": + version "3.1.7" + resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz#6023fb3b2d463229f2d680f9ac4b47466f71f17b" + integrity sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg== + +"@types/d3-interpolate@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" + integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.1.tgz#f632b380c3aca1dba8e34aa049bcd6a4af23df8a" + integrity sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg== + +"@types/d3-polygon@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz#dfae54a6d35d19e76ac9565bcb32a8e54693189c" + integrity sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA== + +"@types/d3-quadtree@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz#d4740b0fe35b1c58b66e1488f4e7ed02952f570f" + integrity sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg== + +"@types/d3-random@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.3.tgz#ed995c71ecb15e0cd31e22d9d5d23942e3300cfb" + integrity sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ== + +"@types/d3-scale-chromatic@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#dc6d4f9a98376f18ea50bad6c39537f1b5463c39" + integrity sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ== + +"@types/d3-scale@*": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.9.tgz#57a2f707242e6fe1de81ad7bfcccaaf606179afb" + integrity sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw== dependencies: "@types/d3-time" "*" +"@types/d3-selection@*": + version "3.0.11" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.11.tgz#bd7a45fc0a8c3167a631675e61bc2ca2b058d4a3" + integrity sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w== + +"@types/d3-shape@*": + version "3.1.7" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.7.tgz#2b7b423dc2dfe69c8c93596e673e37443348c555" + integrity sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time-format@*": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz#d6bc1e6b6a7db69cccfbbdd4c34b70632d9e9db2" + integrity sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg== + "@types/d3-time@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be" integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw== +"@types/d3-timer@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" + integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== + +"@types/d3-transition@*": + version "3.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.9.tgz#1136bc57e9ddb3c390dccc9b5ff3b7d2b8d94706" + integrity sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-zoom@*": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b" + integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw== + dependencies: + "@types/d3-interpolate" "*" + "@types/d3-selection" "*" + +"@types/d3@^7.4.3": + version "7.4.3" + resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.4.3.tgz#d4550a85d08f4978faf0a4c36b848c61eaac07e2" + integrity sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww== + dependencies: + "@types/d3-array" "*" + "@types/d3-axis" "*" + "@types/d3-brush" "*" + "@types/d3-chord" "*" + "@types/d3-color" "*" + "@types/d3-contour" "*" + "@types/d3-delaunay" "*" + "@types/d3-dispatch" "*" + "@types/d3-drag" "*" + "@types/d3-dsv" "*" + "@types/d3-ease" "*" + "@types/d3-fetch" "*" + "@types/d3-force" "*" + "@types/d3-format" "*" + "@types/d3-geo" "*" + "@types/d3-hierarchy" "*" + "@types/d3-interpolate" "*" + "@types/d3-path" "*" + "@types/d3-polygon" "*" + "@types/d3-quadtree" "*" + "@types/d3-random" "*" + "@types/d3-scale" "*" + "@types/d3-scale-chromatic" "*" + "@types/d3-selection" "*" + "@types/d3-shape" "*" + "@types/d3-time" "*" + "@types/d3-time-format" "*" + "@types/d3-timer" "*" + "@types/d3-transition" "*" + "@types/d3-zoom" "*" + "@types/debug@^4.0.0": version "4.1.12" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" @@ -2338,7 +4118,7 @@ dependencies: "@types/ms" "*" -"@types/eslint-scope@^3.7.3": +"@types/eslint-scope@^3.7.3", "@types/eslint-scope@^3.7.7": version "3.7.7" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== @@ -2366,6 +4146,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/estree@^1.0.6": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" + integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== + "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": version "4.19.3" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.3.tgz#e469a13e4186c9e1c0418fb17be8bc8ff1b19a7a" @@ -2386,6 +4171,11 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/geojson@*": + version "7946.0.16" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a" + integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== + "@types/gtag.js@^0.0.12": version "0.0.12" resolved "https://registry.yarnpkg.com/@types/gtag.js/-/gtag.js-0.0.12.tgz#095122edca896689bdfcdd73b057e23064d23572" @@ -2459,7 +4249,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -2512,11 +4302,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== -"@types/parse-json@^4.0.0": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" - integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== - "@types/parse5@^6.0.0": version "6.0.3" resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb" @@ -2629,6 +4414,11 @@ dependencies: "@types/node" "*" +"@types/trusted-types@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + "@types/unist@*", "@types/unist@^3.0.0": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20" @@ -2678,21 +4468,44 @@ "@webassemblyjs/helper-numbers" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" +"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" + integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== + dependencies: + "@webassemblyjs/helper-numbers" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/floating-point-hex-parser@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== +"@webassemblyjs/floating-point-hex-parser@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz#fcca1eeddb1cc4e7b6eed4fc7956d6813b21b9fb" + integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== + "@webassemblyjs/helper-api-error@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== +"@webassemblyjs/helper-api-error@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz#e0a16152248bc38daee76dd7e21f15c5ef3ab1e7" + integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== + "@webassemblyjs/helper-buffer@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== +"@webassemblyjs/helper-buffer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz#822a9bc603166531f7d5df84e67b5bf99b72b96b" + integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== + "@webassemblyjs/helper-numbers@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" @@ -2702,11 +4515,25 @@ "@webassemblyjs/helper-api-error" "1.11.6" "@xtuc/long" "4.2.2" +"@webassemblyjs/helper-numbers@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz#dbd932548e7119f4b8a7877fd5a8d20e63490b2d" + integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.13.2" + "@webassemblyjs/helper-api-error" "1.13.2" + "@xtuc/long" "4.2.2" + "@webassemblyjs/helper-wasm-bytecode@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== +"@webassemblyjs/helper-wasm-bytecode@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz#e556108758f448aae84c850e593ce18a0eb31e0b" + integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== + "@webassemblyjs/helper-wasm-section@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" @@ -2717,6 +4544,16 @@ "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/wasm-gen" "1.12.1" +"@webassemblyjs/helper-wasm-section@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz#9629dda9c4430eab54b591053d6dc6f3ba050348" + integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/ieee754@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" @@ -2724,6 +4561,13 @@ dependencies: "@xtuc/ieee754" "^1.2.0" +"@webassemblyjs/ieee754@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz#1c5eaace1d606ada2c7fd7045ea9356c59ee0dba" + integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== + dependencies: + "@xtuc/ieee754" "^1.2.0" + "@webassemblyjs/leb128@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" @@ -2731,11 +4575,23 @@ dependencies: "@xtuc/long" "4.2.2" +"@webassemblyjs/leb128@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz#57c5c3deb0105d02ce25fa3fd74f4ebc9fd0bbb0" + integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== + dependencies: + "@xtuc/long" "4.2.2" + "@webassemblyjs/utf8@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== +"@webassemblyjs/utf8@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1" + integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== + "@webassemblyjs/wasm-edit@^1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" @@ -2750,6 +4606,20 @@ "@webassemblyjs/wasm-parser" "1.12.1" "@webassemblyjs/wast-printer" "1.12.1" +"@webassemblyjs/wasm-edit@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" + integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/helper-wasm-section" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-opt" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + "@webassemblyjs/wast-printer" "1.14.1" + "@webassemblyjs/wasm-gen@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" @@ -2761,6 +4631,17 @@ "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" +"@webassemblyjs/wasm-gen@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz#991e7f0c090cb0bb62bbac882076e3d219da9570" + integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" + "@webassemblyjs/wasm-opt@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" @@ -2771,6 +4652,16 @@ "@webassemblyjs/wasm-gen" "1.12.1" "@webassemblyjs/wasm-parser" "1.12.1" +"@webassemblyjs/wasm-opt@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz#e6f71ed7ccae46781c206017d3c14c50efa8106b" + integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + "@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" @@ -2783,6 +4674,18 @@ "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" +"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb" + integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-api-error" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" + "@webassemblyjs/wast-printer@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" @@ -2791,6 +4694,14 @@ "@webassemblyjs/ast" "1.12.1" "@xtuc/long" "4.2.2" +"@webassemblyjs/wast-printer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz#3bb3e9638a8ae5fdaf9610e7a06b4d9f9aa6fe07" + integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@xtuc/long" "4.2.2" + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -2801,13 +4712,6 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -2838,7 +4742,12 @@ acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.7.1, acorn@^8.8.2: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.0.tgz#1627bfa2e058148036133b8d9b51a700663c294c" integrity sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw== -address@^1.0.1, address@^1.1.2: +acorn@^8.14.0: + version "8.14.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" + integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== + +address@^1.0.1: version "1.2.2" resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== @@ -2870,7 +4779,7 @@ ajv-formats@2.1.1, ajv-formats@^2.1.1: dependencies: ajv "^8.0.0" -ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: +ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== @@ -2892,7 +4801,7 @@ ajv@8.11.0: require-from-string "^2.0.2" uri-js "^4.2.2" -ajv@^6.12.2, ajv@^6.12.5: +ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -2912,33 +4821,38 @@ ajv@^8.0.0, ajv@^8.9.0: require-from-string "^2.0.2" uri-js "^4.4.1" -algoliasearch-helper@^3.13.3: - version "3.21.0" - resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.21.0.tgz#d28fdb61199b5c229714788bfb812376b18aaf28" - integrity sha512-hjVOrL15I3Y3K8xG0icwG1/tWE+MocqBrhW6uVBWpU+/kVEMK0BnM2xdssj6mZM61eJ4iRxHR0djEI3ENOpR8w== +algoliasearch-helper@^3.22.6: + version "3.25.0" + resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.25.0.tgz#15cc79ad7909db66b8bb5a5a9c38b40e3941fa2f" + integrity sha512-vQoK43U6HXA9/euCqLjvyNdM4G2Fiu/VFp4ae0Gau9sZeIKBPvUPnXfLYAe65Bg7PFuw03coeu5K6lTPSXRObw== dependencies: "@algolia/events" "^4.0.1" -algoliasearch@^4.18.0, algoliasearch@^4.19.1: - version "4.23.3" - resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.23.3.tgz#e09011d0a3b0651444916a3e6bbcba064ec44b60" - integrity sha512-Le/3YgNvjW9zxIQMRhUHuhiUjAlKY/zsdZpfq4dlLqg6mEm0nL6yk+7f2hDOtLpxsgE4jSzDmvHL7nXdBp5feg== +algoliasearch@^5.14.2, algoliasearch@^5.17.1: + version "5.25.0" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-5.25.0.tgz#7337b097deadeca0e6e985c0f8724abea189994f" + integrity sha512-n73BVorL4HIwKlfJKb4SEzAYkR3Buwfwbh+MYxg2mloFph2fFGV58E90QTzdbfzWrLn4HE5Czx/WTjI8fcHaMg== dependencies: - "@algolia/cache-browser-local-storage" "4.23.3" - "@algolia/cache-common" "4.23.3" - "@algolia/cache-in-memory" "4.23.3" - "@algolia/client-account" "4.23.3" - "@algolia/client-analytics" "4.23.3" - "@algolia/client-common" "4.23.3" - "@algolia/client-personalization" "4.23.3" - "@algolia/client-search" "4.23.3" - "@algolia/logger-common" "4.23.3" - "@algolia/logger-console" "4.23.3" - "@algolia/recommend" "4.23.3" - "@algolia/requester-browser-xhr" "4.23.3" - "@algolia/requester-common" "4.23.3" - "@algolia/requester-node-http" "4.23.3" - "@algolia/transporter" "4.23.3" + "@algolia/client-abtesting" "5.25.0" + "@algolia/client-analytics" "5.25.0" + "@algolia/client-common" "5.25.0" + "@algolia/client-insights" "5.25.0" + "@algolia/client-personalization" "5.25.0" + "@algolia/client-query-suggestions" "5.25.0" + "@algolia/client-search" "5.25.0" + "@algolia/ingestion" "1.25.0" + "@algolia/monitoring" "1.25.0" + "@algolia/recommend" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" + +allof-merge@^0.6.6: + version "0.6.6" + resolved "https://registry.yarnpkg.com/allof-merge/-/allof-merge-0.6.6.tgz#1c675c7170e1b24bd3dc96db9c3459c0e7cfbea2" + integrity sha512-116eZBf2he0/J4Tl7EYMz96I5Anaeio+VL0j/H2yxW9CoYQAMMv8gYcwkVRoO7XfIOv/qzSTfVzDVGAYxKFi3g== + dependencies: + json-crawl "^0.5.3" ansi-align@^3.0.1: version "3.0.1" @@ -2947,6 +4861,13 @@ ansi-align@^3.0.1: dependencies: string-width "^4.1.0" +ansi-escapes@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + ansi-html-community@^0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" @@ -3021,26 +4942,6 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -asn1.js@^4.10.1: - version "4.10.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" - integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -assert@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-2.1.0.tgz#6d92a238d05dc02e7427c881fb8be81c8448b2dd" - integrity sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw== - dependencies: - call-bind "^1.0.2" - is-nan "^1.3.2" - object-is "^1.1.5" - object.assign "^4.1.4" - util "^0.12.5" - astring@^1.8.0: version "1.8.6" resolved "https://registry.yarnpkg.com/astring/-/astring-1.8.6.tgz#2c9c157cf1739d67561c56ba896e6948f6b93731" @@ -3061,7 +4962,7 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -autoprefixer@^10.4.13, autoprefixer@^10.4.14, autoprefixer@^10.4.19: +autoprefixer@^10.4.13, autoprefixer@^10.4.19: version "10.4.19" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.19.tgz#ad25a856e82ee9d7898c59583c1afeb3fa65f89f" integrity sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew== @@ -3073,24 +4974,22 @@ autoprefixer@^10.4.13, autoprefixer@^10.4.14, autoprefixer@^10.4.19: picocolors "^1.0.0" postcss-value-parser "^4.2.0" -available-typed-arrays@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" - integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== +autoprefixer@^10.4.21: + version "10.4.21" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.21.tgz#77189468e7a8ad1d9a37fbc08efc9f480cf0a95d" + integrity sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ== dependencies: - possible-typed-array-names "^1.0.0" + browserslist "^4.24.4" + caniuse-lite "^1.0.30001702" + fraction.js "^4.3.7" + normalize-range "^0.1.2" + picocolors "^1.1.1" + postcss-value-parser "^4.2.0" -axios@^0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a" - integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g== - dependencies: - follow-redirects "^1.14.7" - -babel-loader@^9.1.3: - version "9.1.3" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.3.tgz#3d0e01b4e69760cc694ee306fe16d358aa1c6f9a" - integrity sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw== +babel-loader@^9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.2.1.tgz#04c7835db16c246dd19ba0914418f3937797587b" + integrity sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA== dependencies: find-cache-dir "^4.0.0" schema-utils "^4.0.0" @@ -3111,7 +5010,7 @@ babel-plugin-polyfill-corejs2@^0.4.10: "@babel/helper-define-polyfill-provider" "^0.6.2" semver "^6.3.1" -babel-plugin-polyfill-corejs3@^0.10.1, babel-plugin-polyfill-corejs3@^0.10.4: +babel-plugin-polyfill-corejs3@^0.10.4: version "0.10.4" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz#789ac82405ad664c20476d0233b485281deb9c77" integrity sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg== @@ -3119,6 +5018,14 @@ babel-plugin-polyfill-corejs3@^0.10.1, babel-plugin-polyfill-corejs3@^0.10.4: "@babel/helper-define-polyfill-provider" "^0.6.1" core-js-compat "^3.36.1" +babel-plugin-polyfill-corejs3@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz#4e4e182f1bb37c7ba62e2af81d8dd09df31344f6" + integrity sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.3" + core-js-compat "^3.40.0" + babel-plugin-polyfill-regenerator@^0.6.1: version "0.6.2" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz#addc47e240edd1da1058ebda03021f382bba785e" @@ -3165,16 +5072,6 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: - version "4.12.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" - integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== - -bn.js@^5.0.0, bn.js@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" - integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== - body-parser@1.20.2: version "1.20.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" @@ -3256,74 +5153,7 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" -brorand@^1.0.1, brorand@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== - -browserify-aes@^1.0.4, browserify-aes@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" - integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== - dependencies: - bn.js "^5.0.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.3" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.3.tgz#7afe4c01ec7ee59a89a558a4b75bd85ae62d4208" - integrity sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw== - dependencies: - bn.js "^5.2.1" - browserify-rsa "^4.1.0" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.5" - hash-base "~3.0" - inherits "^2.0.4" - parse-asn1 "^5.1.7" - readable-stream "^2.3.8" - safe-buffer "^5.2.1" - -browserify-zlib@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" - integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== - dependencies: - pako "~1.0.5" - -browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.22.2, browserslist@^4.23.0: +browserslist@^4.0.0, browserslist@^4.21.10, browserslist@^4.22.2, browserslist@^4.23.0: version "4.23.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.1.tgz#ce4af0534b3d37db5c1a4ca98b9080f985041e96" integrity sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw== @@ -3333,6 +5163,16 @@ browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^ node-releases "^2.0.14" update-browserslist-db "^1.0.16" +browserslist@^4.24.0, browserslist@^4.24.2, browserslist@^4.24.4, browserslist@^4.24.5: + version "4.24.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.5.tgz#aa0f5b8560fe81fde84c6dcb38f759bafba0e11b" + integrity sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw== + dependencies: + caniuse-lite "^1.0.30001716" + electron-to-chromium "^1.5.149" + node-releases "^2.0.19" + update-browserslist-db "^1.1.3" + buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -3343,11 +5183,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ== - buffer@^5.2.1, buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -3364,11 +5199,6 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" -builtin-status-codes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" - integrity sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ== - bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -3397,7 +5227,15 @@ cacheable-request@^10.2.8: normalize-url "^8.0.0" responselike "^3.0.0" -call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.7: +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bind@^1.0.5, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== @@ -3408,6 +5246,14 @@ call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.7: get-intrinsic "^1.2.4" set-function-length "^1.2.1" +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + call-me-maybe@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa" @@ -3456,6 +5302,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001599, caniuse-lite@^1.0.30001629: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz" integrity sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA== +caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001716, caniuse-lite@^1.0.30001718: + version "1.0.30001718" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz#dae13a9c80d517c30c6197515a96131c194d8f82" + integrity sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw== + ccount@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" @@ -3470,7 +5321,7 @@ chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: +chalk@^4.0.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -3525,7 +5376,7 @@ cheerio-select@^2.1.0: domhandler "^5.0.3" domutils "^3.0.1" -cheerio@^1.0.0-rc.12: +cheerio@1.0.0-rc.12: version "1.0.0-rc.12" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== @@ -3538,7 +5389,26 @@ cheerio@^1.0.0-rc.12: parse5 "^7.0.0" parse5-htmlparser2-tree-adapter "^7.0.0" -"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.2, chokidar@^3.5.3: +chevrotain-allstar@~0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz#b7412755f5d83cc139ab65810cdb00d8db40e6ca" + integrity sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw== + dependencies: + lodash-es "^4.17.21" + +chevrotain@~11.0.3: + version "11.0.3" + resolved "https://registry.yarnpkg.com/chevrotain/-/chevrotain-11.0.3.tgz#88ffc1fb4b5739c715807eaeedbbf200e202fc1b" + integrity sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw== + dependencies: + "@chevrotain/cst-dts-gen" "11.0.3" + "@chevrotain/gast" "11.0.3" + "@chevrotain/regexp-to-ast" "11.0.3" + "@chevrotain/types" "11.0.3" + "@chevrotain/utils" "11.0.3" + lodash-es "4.17.21" + +chokidar@^3.5.3: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -3553,6 +5423,13 @@ cheerio@^1.0.0-rc.12: optionalDependencies: fsevents "~2.3.2" +chokidar@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -3568,14 +5445,6 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - clean-css@^5.2.2, clean-css@^5.3.2, clean-css@~5.3.2: version "5.3.3" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" @@ -3768,6 +5637,16 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +confbox@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" + integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w== + +confbox@^0.2.1: + version "0.2.2" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.2.2.tgz#8652f53961c74d9e081784beed78555974a9c110" + integrity sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ== + config-chain@^1.1.11: version "1.1.13" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" @@ -3792,20 +5671,10 @@ connect-history-api-fallback@^2.0.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== -consola@^2.15.3: - version "2.15.3" - resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" - integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== - -console-browserify@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" - integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== - -constants-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" - integrity sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ== +consola@^3.2.3: + version "3.4.2" + resolved "https://registry.yarnpkg.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7" + integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA== content-disposition@0.5.2: version "0.5.2" @@ -3870,6 +5739,13 @@ core-js-compat@^3.31.0, core-js-compat@^3.36.1: dependencies: browserslist "^4.23.0" +core-js-compat@^3.40.0: + version "3.42.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.42.0.tgz#ce19c29706ee5806e26d3cb3c542d4cfc0ed51bb" + integrity sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ== + dependencies: + browserslist "^4.24.4" + core-js-pure@^3.30.2: version "3.37.1" resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.37.1.tgz#2b4b34281f54db06c9a9a5bd60105046900553bd" @@ -3892,16 +5768,12 @@ cose-base@^1.0.0: dependencies: layout-base "^1.0.0" -cosmiconfig@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" - integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== +cose-base@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cose-base/-/cose-base-2.2.0.tgz#1c395c35b6e10bb83f9769ca8b817d614add5c01" + integrity sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g== dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.1.0" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.7.2" + layout-base "^2.0.0" cosmiconfig@^8.1.3, cosmiconfig@^8.3.5: version "8.3.6" @@ -3913,37 +5785,6 @@ cosmiconfig@^8.1.3, cosmiconfig@^8.3.5: parse-json "^5.2.0" path-type "^4.0.0" -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" - -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - cross-fetch@3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" @@ -3960,23 +5801,6 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -crypto-browserify@^3.12.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - crypto-js@^4.1.1: version "4.2.0" resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" @@ -3989,11 +5813,27 @@ crypto-random-string@^4.0.0: dependencies: type-fest "^1.0.1" +css-blank-pseudo@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-7.0.1.tgz#32020bff20a209a53ad71b8675852b49e8d57e46" + integrity sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag== + dependencies: + postcss-selector-parser "^7.0.0" + css-declaration-sorter@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz#6dec1c9523bc4a643e088aab8f09e67a54961024" integrity sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow== +css-has-pseudo@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-7.0.2.tgz#fb42e8de7371f2896961e1f6308f13c2c7019b72" + integrity sha512-nzol/h+E0bId46Kn2dQH5VElaknX2Sr0hFuB/1EomdC7j+OISt2ZzK7EHX9DZDY53WbIVAR7FYKSO2XnSf07MQ== + dependencies: + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + postcss-value-parser "^4.2.0" + css-loader@^6.8.1: version "6.11.0" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.11.0.tgz#33bae3bf6363d0a7c2cf9031c96c744ff54d85ba" @@ -4020,6 +5860,11 @@ css-minimizer-webpack-plugin@^5.0.1: schema-utils "^4.0.1" serialize-javascript "^6.0.1" +css-prefers-color-scheme@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz#ba001b99b8105b8896ca26fc38309ddb2278bd3c" + integrity sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ== + css-select@^4.1.3: version "4.3.0" resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" @@ -4063,6 +5908,11 @@ css-what@^6.0.1, css-what@^6.1.0: resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== +cssdb@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-8.3.0.tgz#940becad497b8509ad822a28fb0cfe54c969ccfe" + integrity sha512-c7bmItIg38DgGjSwDPZOYF/2o0QU/sSgkWOMyl8votOfgFuyiFKWPesmCGEsrGLxEA9uL540cp8LdaGEjUGsZQ== + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -4149,10 +5999,17 @@ cytoscape-cose-bilkent@^4.1.0: dependencies: cose-base "^1.0.0" -cytoscape@^3.28.1: - version "3.29.2" - resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.29.2.tgz#c99f42513c80a75e2e94858add32896c860202ac" - integrity sha512-2G1ycU28Nh7OHT9rkXRLpCDP30MKH1dXJORZuBhtEhEW7pKwgPi77ImqlCWinouyE1PNepIOGZBOrE84DG7LyQ== +cytoscape-fcose@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz#e4d6f6490df4fab58ae9cea9e5c3ab8d7472f471" + integrity sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ== + dependencies: + cose-base "^2.2.0" + +cytoscape@^3.29.3: + version "3.32.0" + resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.32.0.tgz#34bc2402c9bc7457ab7d9492745f034b7bf47644" + integrity sha512-5JHBC9n75kz5851jeklCPmZWcg3hUe6sjqJvyk3+hVqFaKcHwHgxsjeN1yLmggoUc6STbtm9/NQyabQehfjvWQ== "d3-array@1 - 2": version "2.12.1" @@ -4389,7 +6246,7 @@ d3-zoom@3: d3-selection "2 - 3" d3-transition "2 - 3" -d3@^7.4.0, d3@^7.8.2: +d3@^7.9.0: version "7.9.0" resolved "https://registry.yarnpkg.com/d3/-/d3-7.9.0.tgz#579e7acb3d749caf8860bd1741ae8d371070cd5d" integrity sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA== @@ -4425,25 +6282,25 @@ d3@^7.4.0, d3@^7.8.2: d3-transition "3" d3-zoom "3" -dagre-d3-es@7.0.10: - version "7.0.10" - resolved "https://registry.yarnpkg.com/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz#19800d4be674379a3cd8c86a8216a2ac6827cadc" - integrity sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A== +dagre-d3-es@7.0.11: + version "7.0.11" + resolved "https://registry.yarnpkg.com/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz#2237e726c0577bfe67d1a7cfd2265b9ab2c15c40" + integrity sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw== dependencies: - d3 "^7.8.2" + d3 "^7.9.0" lodash-es "^4.17.21" -dayjs@^1.11.7: - version "1.11.11" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e" - integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg== +dayjs@^1.11.13: + version "1.11.13" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" + integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== debounce@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== -debug@2.6.9, debug@^2.6.0: +debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -4464,6 +6321,13 @@ debug@4.3.4: dependencies: ms "2.1.2" +debug@^4.4.0: + version "4.4.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" + integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== + dependencies: + ms "^2.1.3" + decode-named-character-reference@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" @@ -4483,7 +6347,7 @@ deep-extend@^0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== -deepmerge@^4.0.0, deepmerge@^4.2.2, deepmerge@^4.3.1: +deepmerge@^4.0.0, deepmerge@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== @@ -4514,7 +6378,7 @@ define-lazy-prop@^2.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== -define-properties@^1.1.3, define-properties@^1.2.1: +define-properties@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== @@ -4523,20 +6387,6 @@ define-properties@^1.1.3, define-properties@^1.2.1: has-property-descriptors "^1.0.0" object-keys "^1.1.1" -del@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/del/-/del-6.1.1.tgz#3b70314f1ec0aa325c6b14eb36b95786671edb7a" - integrity sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg== - dependencies: - globby "^11.0.1" - graceful-fs "^4.2.4" - is-glob "^4.0.1" - is-path-cwd "^2.2.0" - is-path-inside "^3.0.2" - p-map "^4.0.0" - rimraf "^3.0.2" - slash "^3.0.0" - delaunator@5: version "5.0.1" resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.1.tgz#39032b08053923e924d6094fe2cde1a99cc51278" @@ -4559,32 +6409,26 @@ dequal@^2.0.0: resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== -des.js@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.1.0.tgz#1d37f5766f3bbff4ee9638e871a8768c173b81da" - integrity sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - destroy@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== + +detect-libc@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8" + integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA== + detect-node@^2.0.4: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== -detect-port-alt@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275" - integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== - dependencies: - address "^1.0.1" - debug "^2.6.0" - detect-port@^1.5.1: version "1.6.1" resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.6.1.tgz#45e4073997c5f292b957cb678fb0bb8ed4250a67" @@ -4615,15 +6459,6 @@ diff@^5.0.0: resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -4643,29 +6478,26 @@ dns-packet@^5.2.2: dependencies: "@leichtgewicht/ip-codec" "^2.0.1" -docusaurus-plugin-image-zoom@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/docusaurus-plugin-image-zoom/-/docusaurus-plugin-image-zoom-1.0.1.tgz#17afec39f2e630cac50a4ed3a8bbdad8d0aa8b9d" - integrity sha512-96IpSKUx2RWy3db9aZ0s673OQo5DWgV9UVWouS+CPOSIVEdCWh6HKmWf6tB9rsoaiIF3oNn9keiyv6neEyKb1Q== +docusaurus-plugin-image-zoom@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/docusaurus-plugin-image-zoom/-/docusaurus-plugin-image-zoom-3.0.1.tgz#76095fdc288b58d351d19bf902bd3c0a3113ec09" + integrity sha512-mQrqA99VpoMQJNbi02qkWAMVNC4+kwc6zLLMNzraHAJlwn+HrlUmZSEDcTwgn+H4herYNxHKxveE2WsYy73eGw== dependencies: - medium-zoom "^1.0.6" + medium-zoom "^1.1.0" validate-peer-dependencies "^2.2.0" -docusaurus-plugin-openapi-docs@3.0.1, docusaurus-plugin-openapi-docs@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-3.0.1.tgz#954fdc4103d7e47133aede210a98353b3e0f0f99" - integrity sha512-6SRqwey/TXMNu2G02mbWgxrifhpjGOjDr30N+58AR0Ytgc+HXMqlPAUIvTe+e7sOBfAtBbiNlmOWv5KSYIjf3w== +docusaurus-plugin-openapi-docs@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-4.4.0.tgz#010b2bfc57aeac1b62c41bf1ef386dcc52c5e91f" + integrity sha512-VFW0euAyM6i6U6Q2WrNXkp1LnxQFGszZbmloMFYrs1qwBjPLkuHfQ4OJMXGDsGcGl4zNDJ9cwODmJlmdwl1hwg== dependencies: "@apidevtools/json-schema-ref-parser" "^11.5.4" - "@docusaurus/plugin-content-docs" "^3.0.1" - "@docusaurus/utils" "^3.0.1" - "@docusaurus/utils-validation" "^3.0.1" "@redocly/openapi-core" "^1.10.5" + allof-merge "^0.6.6" chalk "^4.1.2" clsx "^1.1.1" fs-extra "^9.0.1" json-pointer "^0.6.2" - json-schema-merge-allof "^0.8.1" json5 "^2.2.3" lodash "^4.17.20" mustache "^4.2.0" @@ -4675,13 +6507,6 @@ docusaurus-plugin-openapi-docs@3.0.1, docusaurus-plugin-openapi-docs@^3.0.1: swagger2openapi "^7.0.8" xml-formatter "^2.6.1" -docusaurus-plugin-sass@^0.2.3: - version "0.2.5" - resolved "https://registry.yarnpkg.com/docusaurus-plugin-sass/-/docusaurus-plugin-sass-0.2.5.tgz#6bfb8a227ac6265be685dcbc24ba1989e27b8005" - integrity sha512-Z+D0fLFUKcFpM+bqSUmqKIU+vO+YF1xoEQh5hoFreg2eMf722+siwXDD+sqtwU8E4MvVpuvsQfaHwODNlxJAEg== - dependencies: - sass-loader "^10.1.1" - docusaurus-theme-github-codeblock@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/docusaurus-theme-github-codeblock/-/docusaurus-theme-github-codeblock-2.0.2.tgz#88b7044b81f9091330e8e4a07a1bdc9114a9fb93" @@ -4689,25 +6514,25 @@ docusaurus-theme-github-codeblock@^2.0.2: dependencies: "@docusaurus/types" "^3.0.0" -docusaurus-theme-openapi-docs@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-3.0.1.tgz#49789c63377f294e624a9632eddb8265a421020f" - integrity sha512-tqypV91tC3wuWj9O+4n0M/e5AgHOeMT2nvPj1tjlPkC7/dLinZvpwQStT4YDUPYSoHRseqxd7lhivFQHcmlryg== +docusaurus-theme-openapi-docs@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-4.4.0.tgz#601eb34d43fa49c6fe1418f3fed06e3044ce377f" + integrity sha512-wmc2b946rqBcdjgEHi6Up7e8orasYk5RnIUerTfmZ/Hi006I8FIjMnJEmHAF6t5PbFiiYnlkB6vYK0CC5xBnCQ== dependencies: - "@docusaurus/theme-common" "^3.0.1" "@hookform/error-message" "^2.0.1" "@reduxjs/toolkit" "^1.7.1" + allof-merge "^0.6.6" + buffer "^6.0.3" clsx "^1.1.1" copy-text-to-clipboard "^3.1.0" crypto-js "^4.1.1" - docusaurus-plugin-openapi-docs "^3.0.1" - docusaurus-plugin-sass "^0.2.3" file-saver "^2.0.5" lodash "^4.17.20" - node-polyfill-webpack-plugin "^2.0.1" + pako "^2.1.0" postman-code-generators "^1.10.1" postman-collection "^4.4.0" prism-react-renderer "^2.3.0" + process "^0.11.10" react-hook-form "^7.43.8" react-live "^4.0.0" react-magic-dropzone "^1.0.1" @@ -4715,9 +6540,11 @@ docusaurus-theme-openapi-docs@3.0.1: react-modal "^3.15.1" react-redux "^7.2.0" rehype-raw "^6.1.1" - sass "^1.58.1" - sass-loader "^13.3.2" - webpack "^5.61.0" + remark-gfm "3.0.1" + sass "^1.80.4" + sass-loader "^16.0.2" + unist-util-visit "^5.0.0" + url "^0.11.1" xml-formatter "^2.6.1" dom-converter@^0.2.0: @@ -4745,11 +6572,6 @@ dom-serializer@^2.0.0: domhandler "^5.0.2" entities "^4.2.0" -domain-browser@^4.22.0: - version "4.23.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-4.23.0.tgz#427ebb91efcb070f05cffdfb8a4e9a6c25f8c94b" - integrity sha512-ArzcM/II1wCCujdCNyQjXrAFwS4mrLh4C7DZWlaI8mdh7h3BfKdNd3bKXITfl2PT9FtfQqaGvhi1vPRQPimjGA== - domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" @@ -4769,10 +6591,12 @@ domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" -dompurify@^3.0.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.5.tgz#2c6a113fc728682a0f55684b1388c58ddb79dc38" - integrity sha512-lwG+n5h8QNpxtyrJW/gJWckL+1/DQiYMX8f7t8Z2AZTPw1esVrqjI63i7Zc2Gz0aKzLVMYC1V1PL/ky+aY/NgA== +dompurify@^3.2.4: + version "3.2.6" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.6.tgz#ca040a6ad2b88e2a92dc45f38c79f84a714a1cad" + integrity sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ== + optionalDependencies: + "@types/trusted-types" "^2.0.7" domutils@^2.5.2, domutils@^2.8.0: version "2.8.0" @@ -4807,6 +6631,15 @@ dot-prop@^6.0.1: dependencies: is-obj "^2.0.0" +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" @@ -4827,23 +6660,10 @@ electron-to-chromium@^1.4.796: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.803.tgz#cf55808a5ee12e2a2778bbe8cdc941ef87c2093b" integrity sha512-61H9mLzGOCLLVsnLiRzCbc63uldP0AniRYPV3hbGVtONA1pI7qSGILdbofR7A8TMbOypDocEAjH/e+9k1QIe3g== -elkjs@^0.9.0: - version "0.9.3" - resolved "https://registry.yarnpkg.com/elkjs/-/elkjs-0.9.3.tgz#16711f8ceb09f1b12b99e971b138a8384a529161" - integrity sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ== - -elliptic@^6.5.3, elliptic@^6.5.5: - version "6.5.7" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.7.tgz#8ec4da2cb2939926a1b9a73619d768207e647c8b" - integrity sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q== - dependencies: - bn.js "^4.11.9" - brorand "^1.1.0" - hash.js "^1.0.0" - hmac-drbg "^1.0.1" - inherits "^2.0.4" - minimalistic-assert "^1.0.1" - minimalistic-crypto-utils "^1.0.1" +electron-to-chromium@^1.5.149: + version "1.5.158" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.158.tgz#e5f01fc7fdf810d9d223e30593e0839c306276d4" + integrity sha512-9vcp2xHhkvraY6AHw2WMi+GDSLPX42qe2xjYaVoZqFRJiOcilVQFq9mZmpuHEQpzlgGDelKlV7ZiGcmMsc8WxQ== emoji-regex@^8.0.0: version "8.0.0" @@ -4890,6 +6710,14 @@ enhanced-resolve@^5.17.0: graceful-fs "^4.2.4" tapable "^2.2.0" +enhanced-resolve@^5.17.1: + version "5.18.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz#728ab082f8b7b6836de51f1637aab5d3b9568faf" + integrity sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + entities@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" @@ -4914,6 +6742,11 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" @@ -4924,6 +6757,13 @@ es-module-lexer@^1.2.1: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.3.tgz#25969419de9c0b1fbe54279789023e8a9a788412" integrity sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg== +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + es6-promise@^3.2.1: version "3.3.1" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" @@ -4934,6 +6774,11 @@ escalade@^3.1.1, escalade@^3.1.2: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + escape-goat@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-4.0.0.tgz#9424820331b510b0666b98f7873fe11ac4aa8081" @@ -5095,30 +6940,17 @@ eval@^0.1.8: "@types/node" "*" require-like ">= 0.1.1" -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - -eventemitter3@^4.0.0: +eventemitter3@^4.0.0, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -events@^3.2.0, events@^3.3.0: +events@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - -execa@^5.0.0: +execa@5.1.1, execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== @@ -5175,6 +7007,11 @@ express@^4.17.3: utils-merge "1.0.1" vary "~1.1.2" +exsolve@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.5.tgz#1f5b6b4fe82ad6b28a173ccb955a635d77859dcf" + integrity sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg== + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -5229,13 +7066,6 @@ fast-safe-stringify@^2.0.7: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== -fast-url-parser@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" - integrity sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ== - dependencies: - punycode "^1.3.2" - fastq@^1.6.0: version "1.17.1" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" @@ -5271,6 +7101,13 @@ feed@^4.2.2: dependencies: xml-js "^1.6.11" +figures@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + file-loader@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" @@ -5289,11 +7126,6 @@ file-type@3.9.0: resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" integrity sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA== -filesize@^8.0.6: - version "8.0.7" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-8.0.7.tgz#695e70d80f4e47012c132d57a059e80c6b580bd8" - integrity sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ== - fill-range@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" @@ -5301,11 +7133,6 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" -filter-obj@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-2.0.2.tgz#fff662368e505d69826abb113f0f6a98f56e9d5f" - integrity sha512-lO3ttPjHZRfjMcxWKb1j1eDhTFsu4meeR3lnMcnBFhk6RuLhvEiuALu2TlfL310ph4lCYYwgF/ElIjdP739tdg== - finalhandler@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" @@ -5327,21 +7154,6 @@ find-cache-dir@^4.0.0: common-path-prefix "^3.0.0" pkg-dir "^7.0.0" -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - find-up@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-6.3.0.tgz#2abab3d3280b2dc7ac10199ef324c4e002c8c790" @@ -5355,18 +7167,11 @@ flat@^5.0.2: resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== -follow-redirects@^1.0.0, follow-redirects@^1.14.7: +follow-redirects@^1.0.0: version "1.15.6" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== -for-each@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== - dependencies: - is-callable "^1.1.3" - foreach@^2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.6.tgz#87bcc8a1a0e74000ff2bf9802110708cfb02eb6e" @@ -5380,25 +7185,6 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" -fork-ts-checker-webpack-plugin@^6.5.0: - version "6.5.3" - resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz#eda2eff6e22476a2688d10661688c47f611b37f3" - integrity sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ== - dependencies: - "@babel/code-frame" "^7.8.3" - "@types/json-schema" "^7.0.5" - chalk "^4.1.0" - chokidar "^3.4.2" - cosmiconfig "^6.0.0" - deepmerge "^4.2.2" - fs-extra "^9.0.0" - glob "^7.1.6" - memfs "^3.1.2" - minimatch "^3.0.4" - schema-utils "2.7.0" - semver "^7.3.2" - tapable "^1.0.0" - form-data-encoder@^2.1.2: version "2.1.4" resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5" @@ -5438,7 +7224,7 @@ fs-extra@^11.1.1, fs-extra@^11.2.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@^9.0.0, fs-extra@^9.0.1: +fs-extra@^9.0.1: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== @@ -5489,11 +7275,35 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: has-symbols "^1.0.3" hasown "^2.0.0" +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -5541,7 +7351,7 @@ glob@^10.3.10: minipass "^7.1.2" path-scurry "^1.11.1" -glob@^7.0.0, glob@^7.1.3, glob@^7.1.6: +glob@^7.0.0, glob@^7.1.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -5560,28 +7370,17 @@ global-dirs@^3.0.0: dependencies: ini "2.0.0" -global-modules@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" - integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== - dependencies: - global-prefix "^3.0.0" - -global-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" - integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== - dependencies: - ini "^1.3.5" - kind-of "^6.0.2" - which "^1.3.1" - globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globby@^11.0.1, globby@^11.0.4, globby@^11.1.0: +globals@^15.14.0: + version "15.15.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-15.15.0.tgz#7c4761299d41c32b075715a4ce1ede7897ff72a8" + integrity sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg== + +globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -5611,6 +7410,11 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + got@^12.1.0: version "12.6.1" resolved "https://registry.yarnpkg.com/got/-/got-12.6.1.tgz#8869560d1383353204b5a9435f782df9c091f549" @@ -5662,6 +7466,11 @@ gzip-size@^6.0.0: dependencies: duplexer "^0.1.2" +hachure-fill@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/hachure-fill/-/hachure-fill-0.5.2.tgz#d19bc4cc8750a5962b47fb1300557a85fcf934cc" + integrity sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg== + handle-thing@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" @@ -5694,44 +7503,17 @@ has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== -has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" - integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== - dependencies: - has-symbols "^1.0.3" +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== has-yarn@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-3.0.0.tgz#c3c21e559730d1d3b57e28af1f30d06fac38147d" integrity sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA== -hash-base@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" - integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== - dependencies: - inherits "^2.0.4" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -hash-base@~3.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" - integrity sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -hasown@^2.0.0: +hasown@^2.0.0, hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== @@ -5966,15 +7748,6 @@ history@^4.9.0: tiny-warning "^1.0.0" value-equal "^1.0.1" -hmac-drbg@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" @@ -6043,10 +7816,10 @@ html-void-elements@^3.0.0: resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== -html-webpack-plugin@^5.5.3: - version "5.6.0" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz#50a8fa6709245608cb00e811eacecb8e0d7b7ea0" - integrity sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw== +html-webpack-plugin@^5.6.0: + version "5.6.3" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz#a31145f0fee4184d53a794f9513147df1e653685" + integrity sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg== dependencies: "@types/html-minifier-terser" "^6.0.0" html-minifier-terser "^6.0.2" @@ -6148,11 +7921,6 @@ http2-wrapper@^2.1.10: quick-lru "^5.1.1" resolve-alpn "^1.2.0" -https-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg== - https-proxy-agent@5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" @@ -6195,24 +7963,22 @@ ignore@^5.2.0, ignore@^5.2.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== -image-size@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.2.1.tgz#ee118aedfe666db1a6ee12bed5821cde3740276d" - integrity sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw== - dependencies: - queue "6.0.2" +image-size@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-2.0.2.tgz#84a7b43704db5736f364bf0d1b029821299b4bdc" + integrity sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w== -immer@^9.0.21, immer@^9.0.7: +immer@^9.0.21: version "9.0.21" resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== -immutable@^4.0.0: - version "4.3.6" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.6.tgz#6a05f7858213238e587fb83586ffa3b4b27f0447" - integrity sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ== +immutable@^5.0.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.2.tgz#e8169476414505e5a4fa650107b65e1227d16d4b" + integrity sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ== -import-fresh@^3.1.0, import-fresh@^3.3.0: +import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -6235,10 +8001,10 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -infima@0.2.0-alpha.43: - version "0.2.0-alpha.43" - resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.43.tgz#f7aa1d7b30b6c08afef441c726bac6150228cbe0" - integrity sha512-2uw57LvUqW0rK/SWYnd/2rRfxNA5DDNOh33jxF7fy46VWoNhGxiUQyVZHbBMjQ33mQem0cjdDVwgWVAmlRfgyQ== +infima@0.2.0-alpha.45: + version "0.2.0-alpha.45" + resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.45.tgz#542aab5a249274d81679631b492973dd2c1e7466" + integrity sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw== inflight@^1.0.4: version "1.0.6" @@ -6248,7 +8014,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -6263,7 +8029,7 @@ ini@2.0.0: resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== -ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: +ini@^1.3.4, ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== @@ -6323,14 +8089,6 @@ is-alphanumerical@^2.0.0: is-alphabetical "^2.0.0" is-decimal "^2.0.0" -is-arguments@^1.0.4: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -6348,11 +8106,6 @@ is-buffer@^2.0.0: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== -is-callable@^1.1.3: - version "1.2.7" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - is-ci@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" @@ -6392,13 +8145,6 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-generator-function@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" - integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== - dependencies: - has-tostringtag "^1.0.0" - is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -6419,14 +8165,6 @@ is-installed-globally@^0.4.0: global-dirs "^3.0.0" is-path-inside "^3.0.2" -is-nan@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" - integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - is-npm@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-6.0.0.tgz#b59e75e8915543ca5d881ecff864077cba095261" @@ -6447,11 +8185,6 @@ is-obj@^2.0.0: resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== -is-path-cwd@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" - integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== - is-path-inside@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" @@ -6486,23 +8219,11 @@ is-regexp@^1.0.0: resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== -is-root@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" - integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== - is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -is-typed-array@^1.1.3: - version "1.1.13" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" - integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== - dependencies: - which-typed-array "^1.1.14" - is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -6585,7 +8306,7 @@ jiti@^1.20.0, jiti@^1.21.0: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== -joi@^17.6.0, joi@^17.9.2: +joi@^17.9.2: version "17.13.1" resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.1.tgz#9c7b53dc3b44dd9ae200255cc3b398874918a6ca" integrity sha512-vaBlIKCyo4FCUtCm7Eu4QZd/q02bWcxfUO6YSXAZOWF6gzcLBeba8kwotUdYJjDLW8Cz8RywsSOqiNJZW0mNvg== @@ -6626,16 +8347,31 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== +jsesc@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" + integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== + json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== +json-crawl@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/json-crawl/-/json-crawl-0.5.3.tgz#3a2e1d308d4fc5a444902f1f94f4a9e03d584c6b" + integrity sha512-BEjjCw8c7SxzNK4orhlWD5cXQh8vCk2LqDr4WgQq4CV+5dvopeYwt1Tskg67SuSLKvoFH5g0yuYtg7rcfKV6YA== + json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -6655,7 +8391,7 @@ json-schema-compare@^0.2.2: dependencies: lodash "^4.17.4" -json-schema-merge-allof@0.8.1, json-schema-merge-allof@^0.8.1: +json-schema-merge-allof@0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/json-schema-merge-allof/-/json-schema-merge-allof-0.8.1.tgz#ed2828cdd958616ff74f932830a26291789eaaf2" integrity sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w== @@ -6702,7 +8438,7 @@ keyv@^4.5.3: dependencies: json-buffer "3.0.1" -khroma@^2.0.0: +khroma@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/khroma/-/khroma-2.1.0.tgz#45f2ce94ce231a437cf5b63c2e886e6eb42bbbb1" integrity sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw== @@ -6722,10 +8458,21 @@ kleur@^4.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== -klona@^2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" - integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== +kolorist@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/kolorist/-/kolorist-1.8.0.tgz#edddbbbc7894bc13302cdf740af6374d4a04743c" + integrity sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ== + +langium@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/langium/-/langium-3.3.1.tgz#da745a40d5ad8ee565090fed52eaee643be4e591" + integrity sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w== + dependencies: + chevrotain "~11.0.3" + chevrotain-allstar "~0.3.0" + vscode-languageserver "~9.0.1" + vscode-languageserver-textdocument "~1.0.11" + vscode-uri "~3.0.8" latest-version@^7.0.0: version "7.0.0" @@ -6747,11 +8494,84 @@ layout-base@^1.0.0: resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-1.0.2.tgz#1291e296883c322a9dd4c5dd82063721b53e26e2" integrity sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg== +layout-base@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-2.0.1.tgz#d0337913586c90f9c2c075292069f5c2da5dd285" + integrity sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg== + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== +lightningcss-darwin-arm64@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz#3d47ce5e221b9567c703950edf2529ca4a3700ae" + integrity sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ== + +lightningcss-darwin-x64@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz#e81105d3fd6330860c15fe860f64d39cff5fbd22" + integrity sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA== + +lightningcss-freebsd-x64@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz#a0e732031083ff9d625c5db021d09eb085af8be4" + integrity sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig== + +lightningcss-linux-arm-gnueabihf@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz#1f5ecca6095528ddb649f9304ba2560c72474908" + integrity sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q== + +lightningcss-linux-arm64-gnu@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz#eee7799726103bffff1e88993df726f6911ec009" + integrity sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw== + +lightningcss-linux-arm64-musl@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz#f2e4b53f42892feeef8f620cbb889f7c064a7dfe" + integrity sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ== + +lightningcss-linux-x64-gnu@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz#2fc7096224bc000ebb97eea94aea248c5b0eb157" + integrity sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw== + +lightningcss-linux-x64-musl@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz#66dca2b159fd819ea832c44895d07e5b31d75f26" + integrity sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ== + +lightningcss-win32-arm64-msvc@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz#7d8110a19d7c2d22bfdf2f2bb8be68e7d1b69039" + integrity sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA== + +lightningcss-win32-x64-msvc@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz#fd7dd008ea98494b85d24b4bea016793f2e0e352" + integrity sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg== + +lightningcss@^1.27.0: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.30.1.tgz#78e979c2d595bfcb90d2a8c0eb632fe6c5bfed5d" + integrity sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg== + dependencies: + detect-libc "^2.0.3" + optionalDependencies: + lightningcss-darwin-arm64 "1.30.1" + lightningcss-darwin-x64 "1.30.1" + lightningcss-freebsd-x64 "1.30.1" + lightningcss-linux-arm-gnueabihf "1.30.1" + lightningcss-linux-arm64-gnu "1.30.1" + lightningcss-linux-arm64-musl "1.30.1" + lightningcss-linux-x64-gnu "1.30.1" + lightningcss-linux-x64-musl "1.30.1" + lightningcss-win32-arm64-msvc "1.30.1" + lightningcss-win32-x64-msvc "1.30.1" + lilconfig@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" @@ -6791,25 +8611,14 @@ loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" -loader-utils@^3.2.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.3.1.tgz#735b9a19fd63648ca7adbd31c2327dfe281304e5" - integrity sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg== - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== +local-pkg@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-1.1.1.tgz#f5fe74a97a3bd3c165788ee08ca9fbe998dc58dd" + integrity sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg== dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" + mlly "^1.7.4" + pkg-types "^2.0.1" + quansync "^0.2.8" locate-path@^7.1.0: version "7.2.0" @@ -6818,7 +8627,7 @@ locate-path@^7.1.0: dependencies: p-locate "^6.0.0" -lodash-es@^4.17.21: +lodash-es@4.17.21, lodash-es@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== @@ -6896,19 +8705,27 @@ markdown-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz#34bebc83e9938cae16e0e017e4a9814a8330d3c4" integrity sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q== +markdown-table@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b" + integrity sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A== + dependencies: + repeat-string "^1.0.0" + markdown-table@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd" integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw== -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" +marked@^15.0.7: + version "15.0.12" + resolved "https://registry.yarnpkg.com/marked/-/marked-15.0.12.tgz#30722c7346e12d0a2d0207ab9b0c4f0102d86c4e" + integrity sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA== + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== mdast-util-definitions@^5.0.0: version "5.1.2" @@ -6933,6 +8750,16 @@ mdast-util-directive@^3.0.0: stringify-entities "^4.0.0" unist-util-visit-parents "^6.0.0" +mdast-util-find-and-replace@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz#cc2b774f7f3630da4bd592f61966fecade8b99b1" + integrity sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw== + dependencies: + "@types/mdast" "^3.0.0" + escape-string-regexp "^5.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents "^5.0.0" + mdast-util-find-and-replace@^3.0.0, mdast-util-find-and-replace@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz#a6fc7b62f0994e973490e45262e4bc07607b04e0" @@ -6943,7 +8770,7 @@ mdast-util-find-and-replace@^3.0.0, mdast-util-find-and-replace@^3.0.1: unist-util-is "^6.0.0" unist-util-visit-parents "^6.0.0" -mdast-util-from-markdown@^1.0.0, mdast-util-from-markdown@^1.1.0, mdast-util-from-markdown@^1.2.0, mdast-util-from-markdown@^1.3.0: +mdast-util-from-markdown@^1.0.0, mdast-util-from-markdown@^1.1.0, mdast-util-from-markdown@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0" integrity sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww== @@ -6991,6 +8818,16 @@ mdast-util-frontmatter@^2.0.0: mdast-util-to-markdown "^2.0.0" micromark-extension-frontmatter "^2.0.0" +mdast-util-gfm-autolink-literal@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz#67a13abe813d7eba350453a5333ae1bc0ec05c06" + integrity sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA== + dependencies: + "@types/mdast" "^3.0.0" + ccount "^2.0.0" + mdast-util-find-and-replace "^2.0.0" + micromark-util-character "^1.0.0" + mdast-util-gfm-autolink-literal@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz#5baf35407421310a08e68c15e5d8821e8898ba2a" @@ -7002,6 +8839,15 @@ mdast-util-gfm-autolink-literal@^2.0.0: mdast-util-find-and-replace "^3.0.0" micromark-util-character "^2.0.0" +mdast-util-gfm-footnote@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz#ce5e49b639c44de68d5bf5399877a14d5020424e" + integrity sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + micromark-util-normalize-identifier "^1.0.0" + mdast-util-gfm-footnote@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz#25a1753c7d16db8bfd53cd84fe50562bd1e6d6a9" @@ -7013,6 +8859,14 @@ mdast-util-gfm-footnote@^2.0.0: mdast-util-to-markdown "^2.0.0" micromark-util-normalize-identifier "^2.0.0" +mdast-util-gfm-strikethrough@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz#5470eb105b483f7746b8805b9b989342085795b7" + integrity sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + mdast-util-gfm-strikethrough@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz#d44ef9e8ed283ac8c1165ab0d0dfd058c2764c16" @@ -7022,6 +8876,16 @@ mdast-util-gfm-strikethrough@^2.0.0: mdast-util-from-markdown "^2.0.0" mdast-util-to-markdown "^2.0.0" +mdast-util-gfm-table@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz#3552153a146379f0f9c4c1101b071d70bbed1a46" + integrity sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg== + dependencies: + "@types/mdast" "^3.0.0" + markdown-table "^3.0.0" + mdast-util-from-markdown "^1.0.0" + mdast-util-to-markdown "^1.3.0" + mdast-util-gfm-table@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz#7a435fb6223a72b0862b33afbd712b6dae878d38" @@ -7033,6 +8897,14 @@ mdast-util-gfm-table@^2.0.0: mdast-util-from-markdown "^2.0.0" mdast-util-to-markdown "^2.0.0" +mdast-util-gfm-task-list-item@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz#b280fcf3b7be6fd0cc012bbe67a59831eb34097b" + integrity sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + mdast-util-gfm-task-list-item@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz#e68095d2f8a4303ef24094ab642e1047b991a936" @@ -7043,6 +8915,19 @@ mdast-util-gfm-task-list-item@^2.0.0: mdast-util-from-markdown "^2.0.0" mdast-util-to-markdown "^2.0.0" +mdast-util-gfm@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz#e92f4d8717d74bdba6de57ed21cc8b9552e2d0b6" + integrity sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg== + dependencies: + mdast-util-from-markdown "^1.0.0" + mdast-util-gfm-autolink-literal "^1.0.0" + mdast-util-gfm-footnote "^1.0.0" + mdast-util-gfm-strikethrough "^1.0.0" + mdast-util-gfm-table "^1.0.0" + mdast-util-gfm-task-list-item "^1.0.0" + mdast-util-to-markdown "^1.0.0" + mdast-util-gfm@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz#3f2aecc879785c3cb6a81ff3a243dc11eca61095" @@ -7277,12 +9162,12 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== -medium-zoom@^1.0.6: +medium-zoom@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/medium-zoom/-/medium-zoom-1.1.0.tgz#6efb6bbda861a02064ee71a2617a8dc4381ecc71" integrity sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ== -memfs@^3.1.2, memfs@^3.4.3: +memfs@^3.4.3: version "3.6.0" resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== @@ -7309,31 +9194,31 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -mermaid@^10.4.0, mermaid@^10.9.1: - version "10.9.1" - resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.9.1.tgz#5f582c23f3186c46c6aa673e59eeb46d741b2ea6" - integrity sha512-Mx45Obds5W1UkW1nv/7dHRsbfMM1aOKA2+Pxs/IGHNonygDHwmng8xTHyS9z4KWVi0rbko8gjiBmuwwXQ7tiNA== +mermaid@>=11.6.0: + version "11.6.0" + resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-11.6.0.tgz#eee45cdc3087be561a19faf01745596d946bb575" + integrity sha512-PE8hGUy1LDlWIHWBP05SFdqUHGmRcCcK4IzpOKPE35eOw+G9zZgcnMpyunJVUEOgb//KBORPjysKndw8bFLuRg== dependencies: - "@braintree/sanitize-url" "^6.0.1" - "@types/d3-scale" "^4.0.3" - "@types/d3-scale-chromatic" "^3.0.0" - cytoscape "^3.28.1" + "@braintree/sanitize-url" "^7.0.4" + "@iconify/utils" "^2.1.33" + "@mermaid-js/parser" "^0.4.0" + "@types/d3" "^7.4.3" + cytoscape "^3.29.3" cytoscape-cose-bilkent "^4.1.0" - d3 "^7.4.0" + cytoscape-fcose "^2.2.0" + d3 "^7.9.0" d3-sankey "^0.12.3" - dagre-d3-es "7.0.10" - dayjs "^1.11.7" - dompurify "^3.0.5" - elkjs "^0.9.0" + dagre-d3-es "7.0.11" + dayjs "^1.11.13" + dompurify "^3.2.4" katex "^0.16.9" - khroma "^2.0.0" + khroma "^2.1.0" lodash-es "^4.17.21" - mdast-util-from-markdown "^1.3.0" - non-layered-tidy-tree-layout "^2.0.2" - stylis "^4.1.3" + marked "^15.0.7" + roughjs "^4.6.6" + stylis "^4.3.6" ts-dedent "^2.2.0" - uuid "^9.0.0" - web-worker "^1.2.0" + uuid "^11.1.0" methods@~1.1.2: version "1.1.2" @@ -7407,6 +9292,16 @@ micromark-extension-frontmatter@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-extension-gfm-autolink-literal@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz#5853f0e579bbd8ef9e39a7c0f0f27c5a063a66e7" + integrity sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + micromark-extension-gfm-autolink-literal@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.0.0.tgz#f1e50b42e67d441528f39a67133eddde2bbabfd9" @@ -7417,6 +9312,20 @@ micromark-extension-gfm-autolink-literal@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-extension-gfm-footnote@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz#05e13034d68f95ca53c99679040bc88a6f92fe2e" + integrity sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q== + dependencies: + micromark-core-commonmark "^1.0.0" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + micromark-extension-gfm-footnote@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.0.0.tgz#91afad310065a94b636ab1e9dab2c60d1aab953c" @@ -7431,6 +9340,18 @@ micromark-extension-gfm-footnote@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-extension-gfm-strikethrough@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz#c8212c9a616fa3bf47cb5c711da77f4fdc2f80af" + integrity sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-classify-character "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + micromark-extension-gfm-strikethrough@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.0.0.tgz#6917db8e320da70e39ffbf97abdbff83e6783e61" @@ -7443,6 +9364,17 @@ micromark-extension-gfm-strikethrough@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-extension-gfm-table@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz#dcb46074b0c6254c3fc9cc1f6f5002c162968008" + integrity sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + micromark-extension-gfm-table@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.0.0.tgz#2cf3fe352d9e089b7ef5fff003bdfe0da29649b7" @@ -7454,6 +9386,13 @@ micromark-extension-gfm-table@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-extension-gfm-tagfilter@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz#aa7c4dd92dabbcb80f313ebaaa8eb3dac05f13a7" + integrity sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g== + dependencies: + micromark-util-types "^1.0.0" + micromark-extension-gfm-tagfilter@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz#f26d8a7807b5985fba13cf61465b58ca5ff7dc57" @@ -7461,6 +9400,17 @@ micromark-extension-gfm-tagfilter@^2.0.0: dependencies: micromark-util-types "^2.0.0" +micromark-extension-gfm-task-list-item@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz#b52ce498dc4c69b6a9975abafc18f275b9dde9f4" + integrity sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + micromark-extension-gfm-task-list-item@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.0.1.tgz#ee8b208f1ced1eb9fb11c19a23666e59d86d4838" @@ -7472,6 +9422,20 @@ micromark-extension-gfm-task-list-item@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-extension-gfm@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz#e517e8579949a5024a493e49204e884aa74f5acf" + integrity sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ== + dependencies: + micromark-extension-gfm-autolink-literal "^1.0.0" + micromark-extension-gfm-footnote "^1.0.0" + micromark-extension-gfm-strikethrough "^1.0.0" + micromark-extension-gfm-table "^1.0.0" + micromark-extension-gfm-tagfilter "^1.0.0" + micromark-extension-gfm-task-list-item "^1.0.0" + micromark-util-combine-extensions "^1.0.0" + micromark-util-types "^1.0.0" + micromark-extension-gfm@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz#3e13376ab95dd7a5cfd0e29560dfe999657b3c5b" @@ -8026,14 +9990,6 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.3" picomatch "^2.3.1" -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - mime-db@1.48.0: version "1.48.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" @@ -8097,25 +10053,20 @@ mimic-response@^4.0.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-4.0.0.tgz#35468b19e7c75d10f5165ea25e75a5ceea7cf70f" integrity sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg== -mini-css-extract-plugin@^2.7.6: - version "2.9.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz#c73a1327ccf466f69026ac22a8e8fd707b78a235" - integrity sha512-Zs1YsZVfemekSZG+44vBsYTLQORkPMwnlv+aehcxK/NLKC+EGhDB39/YePYYqx/sTk6NnYpuqikhSn7+JIevTA== +mini-css-extract-plugin@^2.9.1: + version "2.9.2" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz#966031b468917a5446f4c24a80854b2947503c5b" + integrity sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w== dependencies: schema-utils "^4.0.0" tapable "^2.2.1" -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: +minimalistic-assert@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== - -minimatch@3.1.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1: +minimatch@3.1.2, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -8136,7 +10087,7 @@ minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.2.0: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -8151,6 +10102,16 @@ mkdirp-classic@^0.5.2: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== +mlly@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.4.tgz#3d7295ea2358ec7a271eaa5d000a0f84febe100f" + integrity sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw== + dependencies: + acorn "^8.14.0" + pathe "^2.0.1" + pkg-types "^1.3.0" + ufo "^1.5.4" + mri@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" @@ -8171,7 +10132,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3: +ms@2.1.3, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -8221,6 +10182,11 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" +node-addon-api@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" + integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== + node-emoji@^2.1.0: version "2.1.3" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-2.1.3.tgz#93cfabb5cc7c3653aa52f29d6ffb7927d8047c06" @@ -8257,37 +10223,6 @@ node-forge@^1: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== -node-polyfill-webpack-plugin@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-2.0.1.tgz#141d86f177103a8517c71d99b7c6a46edbb1bb58" - integrity sha512-ZUMiCnZkP1LF0Th2caY6J/eKKoA0TefpoVa68m/LQU1I/mE8rGt4fNYGgNuCcK+aG8P8P43nbeJ2RqJMOL/Y1A== - dependencies: - assert "^2.0.0" - browserify-zlib "^0.2.0" - buffer "^6.0.3" - console-browserify "^1.2.0" - constants-browserify "^1.0.0" - crypto-browserify "^3.12.0" - domain-browser "^4.22.0" - events "^3.3.0" - filter-obj "^2.0.2" - https-browserify "^1.0.0" - os-browserify "^0.3.0" - path-browserify "^1.0.1" - process "^0.11.10" - punycode "^2.1.1" - querystring-es3 "^0.2.1" - readable-stream "^4.0.0" - stream-browserify "^3.0.0" - stream-http "^3.2.0" - string_decoder "^1.3.0" - timers-browserify "^2.0.12" - tty-browserify "^0.0.1" - type-fest "^2.14.0" - url "^0.11.0" - util "^0.12.4" - vm-browserify "^1.1.2" - node-readfiles@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/node-readfiles/-/node-readfiles-0.2.0.tgz#dbbd4af12134e2e635c245ef93ffcf6f60673a5d" @@ -8300,10 +10235,10 @@ node-releases@^2.0.14: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== -non-layered-tidy-tree-layout@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz#57d35d13c356643fc296a55fb11ac15e74da7804" - integrity sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw== +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" @@ -8339,6 +10274,14 @@ nth-check@^2.0.1: dependencies: boolbase "^1.0.0" +null-loader@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/null-loader/-/null-loader-4.0.1.tgz#8e63bd3a2dd3c64236a4679428632edd0a6dbc6a" + integrity sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + oas-kit-common@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/oas-kit-common/-/oas-kit-common-1.0.8.tgz#6d8cacf6e9097967a4c7ea8bcbcbd77018e1f535" @@ -8412,20 +10355,17 @@ object-inspect@^1.13.1: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== -object-is@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" - integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.0, object.assign@^4.1.4: +object.assign@^4.1.0: version "4.1.5" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== @@ -8502,29 +10442,15 @@ opener@^1.5.2: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== -os-browserify@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A== - p-cancelable@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050" integrity sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw== -p-limit@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== p-limit@^4.0.0: version "4.0.0" @@ -8533,20 +10459,6 @@ p-limit@^4.0.0: dependencies: yocto-queue "^1.0.0" -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - p-locate@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-6.0.0.tgz#3da9a49d4934b901089dca3302fa65dc5a05c04f" @@ -8561,6 +10473,14 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" +p-queue@^6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" + integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== + dependencies: + eventemitter3 "^4.0.4" + p-timeout "^3.2.0" + p-retry@^4.5.0: version "4.6.2" resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" @@ -8569,10 +10489,12 @@ p-retry@^4.5.0: "@types/retry" "0.12.0" retry "^0.13.1" -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +p-timeout@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" + integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== + dependencies: + p-finally "^1.0.0" package-json@^8.1.0: version "8.1.1" @@ -8584,10 +10506,15 @@ package-json@^8.1.0: registry-url "^6.0.0" semver "^7.3.7" -pako@~1.0.5: - version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== +package-manager-detector@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/package-manager-detector/-/package-manager-detector-1.3.0.tgz#b42d641c448826e03c2b354272456a771ce453c0" + integrity sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ== + +pako@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== param-case@^3.0.4: version "3.0.4" @@ -8604,18 +10531,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-asn1@^5.0.0, parse-asn1@^5.1.7: - version "5.1.7" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.7.tgz#73cdaaa822125f9647165625eb45f8a051d2df06" - integrity sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg== - dependencies: - asn1.js "^4.10.1" - browserify-aes "^1.2.0" - evp_bytestokey "^1.0.3" - hash-base "~3.0" - pbkdf2 "^3.1.2" - safe-buffer "^5.2.1" - parse-entities@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.1.tgz#4e2a01111fb1c986549b944af39eeda258fc9e4e" @@ -8630,7 +10545,7 @@ parse-entities@^4.0.0: is-decimal "^2.0.0" is-hexadecimal "^2.0.0" -parse-json@^5.0.0, parse-json@^5.2.0: +parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -8683,15 +10598,10 @@ path-browserify@1.0.1, path-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== +path-data-parser@0.1.0, path-data-parser@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/path-data-parser/-/path-data-parser-0.1.0.tgz#8f5ba5cc70fc7becb3dcefaea08e2659aba60b8c" + integrity sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w== path-exists@^5.0.0: version "5.0.0" @@ -8743,10 +10653,10 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== -path-to-regexp@2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45" - integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ== +path-to-regexp@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b" + integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw== path-to-regexp@^1.7.0: version "1.8.0" @@ -8768,16 +10678,10 @@ path@0.12.7: process "^0.11.1" util "^0.10.3" -pbkdf2@^3.0.3, pbkdf2@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" - integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" +pathe@^2.0.1, pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== pend@~1.2.0: version "1.2.0" @@ -8798,6 +10702,11 @@ picocolors@^1.0.0, picocolors@^1.0.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -8820,22 +10729,48 @@ pkg-dir@^7.0.0: dependencies: find-up "^6.3.0" -pkg-up@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" - integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== +pkg-types@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.3.1.tgz#bd7cc70881192777eef5326c19deb46e890917df" + integrity sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ== dependencies: - find-up "^3.0.0" + confbox "^0.1.8" + mlly "^1.7.4" + pathe "^2.0.1" + +pkg-types@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-2.1.0.tgz#70c9e1b9c74b63fdde749876ee0aa007ea9edead" + integrity sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A== + dependencies: + confbox "^0.2.1" + exsolve "^1.0.1" + pathe "^2.0.3" pluralize@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== -possible-typed-array-names@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" - integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== +points-on-curve@0.2.0, points-on-curve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/points-on-curve/-/points-on-curve-0.2.0.tgz#7dbb98c43791859434284761330fa893cb81b4d1" + integrity sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A== + +points-on-path@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/points-on-path/-/points-on-path-0.2.1.tgz#553202b5424c53bed37135b318858eacff85dd52" + integrity sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g== + dependencies: + path-data-parser "0.1.0" + points-on-curve "0.2.0" + +postcss-attribute-case-insensitive@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz#0c4500e3bcb2141848e89382c05b5a31c23033a3" + integrity sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw== + dependencies: + postcss-selector-parser "^7.0.0" postcss-calc@^9.0.1: version "9.0.1" @@ -8845,6 +10780,40 @@ postcss-calc@^9.0.1: postcss-selector-parser "^6.0.11" postcss-value-parser "^4.2.0" +postcss-clamp@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-clamp/-/postcss-clamp-4.1.0.tgz#7263e95abadd8c2ba1bd911b0b5a5c9c93e02363" + integrity sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-color-functional-notation@^7.0.10: + version "7.0.10" + resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.10.tgz#f1e9c3e4371889dcdfeabfa8515464fd8338cedc" + integrity sha512-k9qX+aXHBiLTRrWoCJuUFI6F1iF6QJQUXNVWJVSbqZgj57jDhBlOvD8gNUGl35tgqDivbGLhZeW3Ongz4feuKA== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +postcss-color-hex-alpha@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz#5dd3eba1f8facb4ea306cba6e3f7712e876b0c76" + integrity sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-color-rebeccapurple@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz#5ada28406ac47e0796dff4056b0a9d5a6ecead98" + integrity sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + postcss-colormin@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-6.1.0.tgz#076e8d3fb291fbff7b10e6b063be9da42ff6488d" @@ -8863,6 +10832,44 @@ postcss-convert-values@^6.1.0: browserslist "^4.23.0" postcss-value-parser "^4.2.0" +postcss-custom-media@^11.0.6: + version "11.0.6" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-11.0.6.tgz#6b450e5bfa209efb736830066682e6567bd04967" + integrity sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw== + dependencies: + "@csstools/cascade-layer-name-parser" "^2.0.5" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/media-query-list-parser" "^4.0.3" + +postcss-custom-properties@^14.0.5: + version "14.0.5" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-14.0.5.tgz#a180444de695f6e11ee2390be93ff6537663e86c" + integrity sha512-UWf/vhMapZatv+zOuqlfLmYXeOhhHLh8U8HAKGI2VJ00xLRYoAJh4xv8iX6FB6+TLXeDnm0DBLMi00E0hodbQw== + dependencies: + "@csstools/cascade-layer-name-parser" "^2.0.5" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-custom-selectors@^8.0.5: + version "8.0.5" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-8.0.5.tgz#9448ed37a12271d7ab6cb364b6f76a46a4a323e8" + integrity sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg== + dependencies: + "@csstools/cascade-layer-name-parser" "^2.0.5" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + postcss-selector-parser "^7.0.0" + +postcss-dir-pseudo-class@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz#80d9e842c9ae9d29f6bf5fd3cf9972891d6cc0ca" + integrity sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA== + dependencies: + postcss-selector-parser "^7.0.0" + postcss-discard-comments@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz#e768dcfdc33e0216380623652b0a4f69f4678b6c" @@ -8890,6 +10897,47 @@ postcss-discard-unused@^6.0.5: dependencies: postcss-selector-parser "^6.0.16" +postcss-double-position-gradients@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.2.tgz#185f8eab2db9cf4e34be69b5706c905895bb52ae" + integrity sha512-7qTqnL7nfLRyJK/AHSVrrXOuvDDzettC+wGoienURV8v2svNbu6zJC52ruZtHaO6mfcagFmuTGFdzRsJKB3k5Q== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-focus-visible@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz#1f7904904368a2d1180b220595d77b6f8a957868" + integrity sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-focus-within@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz#ac01ce80d3f2e8b2b3eac4ff84f8e15cd0057bc7" + integrity sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-font-variant@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz#efd59b4b7ea8bb06127f2d031bfbb7f24d32fa66" + integrity sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA== + +postcss-gap-properties@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz#d5ff0bdf923c06686499ed2b12e125fe64054fed" + integrity sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw== + +postcss-image-set-function@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz#538e94e16716be47f9df0573b56bbaca86e1da53" + integrity sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + postcss-import@^15.1.0: version "15.1.0" resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" @@ -8906,6 +10954,17 @@ postcss-js@^4.0.1: dependencies: camelcase-css "^2.0.1" +postcss-lab-function@^7.0.10: + version "7.0.10" + resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-7.0.10.tgz#0537bd7245b935fc133298c8896bcbd160540cae" + integrity sha512-tqs6TCEv9tC1Riq6fOzHuHcZyhg4k3gIAMB8GGY/zA1ssGdm6puHMVE7t75aOSoFg7UD2wyrFFhbldiCMyyFTQ== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + postcss-load-config@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" @@ -8923,6 +10982,13 @@ postcss-loader@^7.3.3: jiti "^1.20.0" semver "^7.5.4" +postcss-logical@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-8.1.0.tgz#4092b16b49e3ecda70c4d8945257da403d167228" + integrity sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA== + dependencies: + postcss-value-parser "^4.2.0" + postcss-merge-idents@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-6.0.3.tgz#7b9c31c7bc823c94bec50f297f04e3c2b838ea65" @@ -9016,6 +11082,15 @@ postcss-nested@^6.0.1: dependencies: postcss-selector-parser "^6.0.11" +postcss-nesting@^13.0.1: + version "13.0.1" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-13.0.1.tgz#c405796d7245a3e4c267a9956cacfe9670b5d43e" + integrity sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ== + dependencies: + "@csstools/selector-resolve-nested" "^3.0.0" + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + postcss-normalize-charset@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz#1ec25c435057a8001dac942942a95ffe66f721e1" @@ -9078,6 +11153,11 @@ postcss-normalize-whitespace@^6.0.2: dependencies: postcss-value-parser "^4.2.0" +postcss-opacity-percentage@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz#0b0db5ed5db5670e067044b8030b89c216e1eb0a" + integrity sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ== + postcss-ordered-values@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz#366bb663919707093451ab70c3f99c05672aaae5" @@ -9086,6 +11166,102 @@ postcss-ordered-values@^6.0.2: cssnano-utils "^4.0.2" postcss-value-parser "^4.2.0" +postcss-overflow-shorthand@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz#f5252b4a2ee16c68cd8a9029edb5370c4a9808af" + integrity sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-page-break@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-3.0.4.tgz#7fbf741c233621622b68d435babfb70dd8c1ee5f" + integrity sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ== + +postcss-place@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-10.0.0.tgz#ba36ee4786ca401377ced17a39d9050ed772e5a9" + integrity sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-preset-env@^10.1.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-10.2.0.tgz#ea95a6fc70efb1a26f81e5cf0ccdb321a3439b6e" + integrity sha512-cl13sPBbSqo1Q7Ryb19oT5NZO5IHFolRbIMdgDq4f9w1MHYiL6uZS7uSsjXJ1KzRIcX5BMjEeyxmAevVXENa3Q== + dependencies: + "@csstools/postcss-cascade-layers" "^5.0.1" + "@csstools/postcss-color-function" "^4.0.10" + "@csstools/postcss-color-mix-function" "^3.0.10" + "@csstools/postcss-color-mix-variadic-function-arguments" "^1.0.0" + "@csstools/postcss-content-alt-text" "^2.0.6" + "@csstools/postcss-exponential-functions" "^2.0.9" + "@csstools/postcss-font-format-keywords" "^4.0.0" + "@csstools/postcss-gamut-mapping" "^2.0.10" + "@csstools/postcss-gradients-interpolation-method" "^5.0.10" + "@csstools/postcss-hwb-function" "^4.0.10" + "@csstools/postcss-ic-unit" "^4.0.2" + "@csstools/postcss-initial" "^2.0.1" + "@csstools/postcss-is-pseudo-class" "^5.0.1" + "@csstools/postcss-light-dark-function" "^2.0.9" + "@csstools/postcss-logical-float-and-clear" "^3.0.0" + "@csstools/postcss-logical-overflow" "^2.0.0" + "@csstools/postcss-logical-overscroll-behavior" "^2.0.0" + "@csstools/postcss-logical-resize" "^3.0.0" + "@csstools/postcss-logical-viewport-units" "^3.0.4" + "@csstools/postcss-media-minmax" "^2.0.9" + "@csstools/postcss-media-queries-aspect-ratio-number-values" "^3.0.5" + "@csstools/postcss-nested-calc" "^4.0.0" + "@csstools/postcss-normalize-display-values" "^4.0.0" + "@csstools/postcss-oklab-function" "^4.0.10" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/postcss-random-function" "^2.0.1" + "@csstools/postcss-relative-color-syntax" "^3.0.10" + "@csstools/postcss-scope-pseudo-class" "^4.0.1" + "@csstools/postcss-sign-functions" "^1.1.4" + "@csstools/postcss-stepped-value-functions" "^4.0.9" + "@csstools/postcss-text-decoration-shorthand" "^4.0.2" + "@csstools/postcss-trigonometric-functions" "^4.0.9" + "@csstools/postcss-unset-value" "^4.0.0" + autoprefixer "^10.4.21" + browserslist "^4.24.5" + css-blank-pseudo "^7.0.1" + css-has-pseudo "^7.0.2" + css-prefers-color-scheme "^10.0.0" + cssdb "^8.3.0" + postcss-attribute-case-insensitive "^7.0.1" + postcss-clamp "^4.1.0" + postcss-color-functional-notation "^7.0.10" + postcss-color-hex-alpha "^10.0.0" + postcss-color-rebeccapurple "^10.0.0" + postcss-custom-media "^11.0.6" + postcss-custom-properties "^14.0.5" + postcss-custom-selectors "^8.0.5" + postcss-dir-pseudo-class "^9.0.1" + postcss-double-position-gradients "^6.0.2" + postcss-focus-visible "^10.0.1" + postcss-focus-within "^9.0.1" + postcss-font-variant "^5.0.0" + postcss-gap-properties "^6.0.0" + postcss-image-set-function "^7.0.0" + postcss-lab-function "^7.0.10" + postcss-logical "^8.1.0" + postcss-nesting "^13.0.1" + postcss-opacity-percentage "^3.0.0" + postcss-overflow-shorthand "^6.0.0" + postcss-page-break "^3.0.4" + postcss-place "^10.0.0" + postcss-pseudo-class-any-link "^10.0.1" + postcss-replace-overflow-wrap "^4.0.0" + postcss-selector-not "^8.0.1" + +postcss-pseudo-class-any-link@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz#06455431171bf44b84d79ebaeee9fd1c05946544" + integrity sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q== + dependencies: + postcss-selector-parser "^7.0.0" + postcss-reduce-idents@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz#b0d9c84316d2a547714ebab523ec7d13704cd486" @@ -9108,6 +11284,18 @@ postcss-reduce-transforms@^6.0.2: dependencies: postcss-value-parser "^4.2.0" +postcss-replace-overflow-wrap@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz#d2df6bed10b477bf9c52fab28c568b4b29ca4319" + integrity sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw== + +postcss-selector-not@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz#f2df9c6ac9f95e9fe4416ca41a957eda16130172" + integrity sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA== + dependencies: + postcss-selector-parser "^7.0.0" + postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.16, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: version "6.1.0" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz#49694cb4e7c649299fea510a29fa6577104bcf53" @@ -9116,6 +11304,14 @@ postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.16, postcss-select cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss-selector-parser@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz#4d6af97eba65d73bc4d84bcb343e865d7dd16262" + integrity sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-sort-media-queries@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/postcss-sort-media-queries/-/postcss-sort-media-queries-5.2.0.tgz#4556b3f982ef27d3bac526b99b6c0d3359a6cf97" @@ -9246,7 +11442,7 @@ pretty-time@^1.1.0: resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e" integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== -prism-react-renderer@^2.0.6, prism-react-renderer@^2.1.0, prism-react-renderer@^2.3.0: +prism-react-renderer@^2.0.6, prism-react-renderer@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-2.3.1.tgz#e59e5450052ede17488f6bc85de1553f584ff8d5" integrity sha512-Rdf+HzBLR7KYjzpJ1rSoxT9ioO85nZngQEoFIhL07XhtJHlCU3SOz0GJ6+qvMyQe0Se+BV3qpe6Yd/NmQF5Juw== @@ -9314,18 +11510,6 @@ proxy-from-env@1.1.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -9334,7 +11518,7 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^1.3.2, punycode@^1.4.1: +punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== @@ -9384,50 +11568,35 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" -qs@^6.11.2: - version "6.12.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a" - integrity sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ== +qs@^6.12.3: + version "6.14.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" + integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== dependencies: - side-channel "^1.0.6" + side-channel "^1.1.0" -querystring-es3@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - integrity sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA== +quansync@^0.2.8: + version "0.2.10" + resolved "https://registry.yarnpkg.com/quansync/-/quansync-0.2.10.tgz#32053cf166fa36511aae95fc49796116f2dc20e1" + integrity sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A== queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -queue@6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.2.tgz#b91525283e2315c7553d2efa18d83e76432fed65" - integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA== - dependencies: - inherits "~2.0.3" - quick-lru@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: +randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - range-parser@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" @@ -9474,36 +11643,6 @@ react-copy-to-clipboard@^5.1.0: copy-to-clipboard "^3.3.1" prop-types "^15.8.1" -react-dev-utils@^12.0.1: - version "12.0.1" - resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73" - integrity sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ== - dependencies: - "@babel/code-frame" "^7.16.0" - address "^1.1.2" - browserslist "^4.18.1" - chalk "^4.1.2" - cross-spawn "^7.0.3" - detect-port-alt "^1.1.6" - escape-string-regexp "^4.0.0" - filesize "^8.0.6" - find-up "^5.0.0" - fork-ts-checker-webpack-plugin "^6.5.0" - global-modules "^2.0.0" - globby "^11.0.4" - gzip-size "^6.0.0" - immer "^9.0.7" - is-root "^2.1.0" - loader-utils "^3.2.0" - open "^8.4.0" - pkg-up "^3.1.0" - prompts "^2.4.2" - react-error-overlay "^6.0.11" - recursive-readdir "^2.2.2" - shell-quote "^1.7.3" - strip-ansi "^6.0.1" - text-table "^0.2.0" - react-dom@^18.2.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" @@ -9512,12 +11651,7 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.2" -react-error-overlay@^6.0.11: - version "6.0.11" - resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" - integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== - -react-fast-compare@^3.0.1, react-fast-compare@^3.2.0, react-fast-compare@^3.2.2: +react-fast-compare@^3.0.1, react-fast-compare@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== @@ -9527,19 +11661,10 @@ react-google-charts@^5.2.1: resolved "https://registry.yarnpkg.com/react-google-charts/-/react-google-charts-5.2.1.tgz#d9cbe8ed45d7c0fafefea5c7c3361bee76648454" integrity sha512-mCbPiObP8yWM5A9ogej7Qp3/HX4EzOwuEzUYvcfHtL98Xt4V/brD14KgfDzSNNtyD48MNXCpq5oVaYKt0ykQUQ== -react-helmet-async@*: - version "2.0.5" - resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-2.0.5.tgz#cfc70cd7bb32df7883a8ed55502a1513747223ec" - integrity sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg== - dependencies: - invariant "^2.2.4" - react-fast-compare "^3.2.2" - shallowequal "^1.1.0" - -react-helmet-async@^1.3.0: +react-helmet-async@^1.3.0, "react-helmet-async@npm:@slorber/react-helmet-async@1.3.0": version "1.3.0" - resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-1.3.0.tgz#7bd5bf8c5c69ea9f02f6083f14ce33ef545c222e" - integrity sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg== + resolved "https://registry.yarnpkg.com/@slorber/react-helmet-async/-/react-helmet-async-1.3.0.tgz#11fbc6094605cf60aa04a28c17e0aab894b4ecff" + integrity sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A== dependencies: "@babel/runtime" "^7.12.5" invariant "^2.2.4" @@ -9567,10 +11692,10 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== -react-json-view-lite@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/react-json-view-lite/-/react-json-view-lite-1.4.0.tgz#0ff493245f4550abe5e1f1836f170fa70bb95914" - integrity sha512-wh6F6uJyYAmQ4fK0e8dSQMEWuvTs2Wr3el3sLD9bambX1+pSWUVXIz1RFaoy3TI1mZ0FqdpKq9YgbgTTgyrmXA== +react-json-view-lite@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/react-json-view-lite/-/react-json-view-lite-2.4.1.tgz#0d06696a06aaf4a74e890302b76cf8cddcc45d60" + integrity sha512-fwFYknRIBxjbFm0kBDrzgBy1xa5tDg2LyXXBepC5f1b+MY3BUClMCsvanMPn089JbV1Eg3nZcrp0VCuH43aXnA== react-lifecycles-compat@^3.0.0: version "3.0.4" @@ -9708,7 +11833,7 @@ read-cache@^1.0.0: dependencies: pify "^2.3.0" -readable-stream@^2.0.1, readable-stream@^2.3.8: +readable-stream@^2.0.1: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -9721,7 +11846,7 @@ readable-stream@^2.0.1, readable-stream@^2.3.8: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: +readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -9730,16 +11855,10 @@ readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^4.0.0: - version "4.5.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" - integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== - dependencies: - abort-controller "^3.0.0" - buffer "^6.0.3" - events "^3.3.0" - process "^0.11.10" - string_decoder "^1.3.0" +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== readdirp@~3.6.0: version "3.6.0" @@ -9748,11 +11867,6 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -reading-time@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/reading-time/-/reading-time-1.5.0.tgz#d2a7f1b6057cb2e169beaf87113cc3411b5bc5bb" - integrity sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg== - rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -9760,13 +11874,6 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" -recursive-readdir@^2.2.2: - version "2.2.3" - resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" - integrity sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA== - dependencies: - minimatch "^3.0.5" - redux-thunk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b" @@ -9791,6 +11898,13 @@ regenerate-unicode-properties@^10.1.0: dependencies: regenerate "^1.4.2" +regenerate-unicode-properties@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz#626e39df8c372338ea9b8028d1f99dc3fd9c3db0" + integrity sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA== + dependencies: + regenerate "^1.4.2" + regenerate@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" @@ -9820,6 +11934,18 @@ regexpu-core@^5.3.1: unicode-match-property-ecmascript "^2.0.0" unicode-match-property-value-ecmascript "^2.1.0" +regexpu-core@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.2.0.tgz#0e5190d79e542bf294955dccabae04d3c7d53826" + integrity sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.2.0" + regjsgen "^0.8.0" + regjsparser "^0.12.0" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + registry-auth-token@^5.0.1: version "5.0.2" resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-5.0.2.tgz#8b026cc507c8552ebbe06724136267e63302f756" @@ -9834,6 +11960,18 @@ registry-url@^6.0.0: dependencies: rc "1.2.8" +regjsgen@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" + integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== + +regjsparser@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.12.0.tgz#0e846df6c6530586429377de56e0475583b088dc" + integrity sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ== + dependencies: + jsesc "~3.0.2" + regjsparser@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" @@ -9895,6 +12033,16 @@ remark-frontmatter@^5.0.0: micromark-extension-frontmatter "^2.0.0" unified "^11.0.0" +remark-gfm@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-3.0.1.tgz#0b180f095e3036545e9dddac0e8df3fa5cfee54f" + integrity sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-gfm "^2.0.0" + micromark-extension-gfm "^2.0.0" + unified "^10.0.0" + remark-gfm@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-4.0.0.tgz#aea777f0744701aa288b67d28c43565c7e8c35de" @@ -9975,6 +12123,11 @@ renderkid@^3.0.0: lodash "^4.17.21" strip-ansi "^6.0.1" +repeat-string@^1.0.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -10055,23 +12208,20 @@ rimraf@3.0.2, rimraf@^3.0.2: dependencies: glob "^7.1.3" -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - robust-predicates@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== -rtl-detect@^1.0.4: - version "1.1.2" - resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.1.2.tgz#ca7f0330af5c6bb626c15675c642ba85ad6273c6" - integrity sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ== +roughjs@^4.6.6: + version "4.6.6" + resolved "https://registry.yarnpkg.com/roughjs/-/roughjs-4.6.6.tgz#1059f49a5e0c80dee541a005b20cc322b222158b" + integrity sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ== + dependencies: + hachure-fill "^0.5.2" + path-data-parser "^0.1.0" + points-on-curve "^0.2.0" + points-on-path "^0.2.1" rtlcss@^4.1.0: version "4.1.1" @@ -10095,13 +12245,6 @@ rw@1: resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== -rxjs@^7.5.4: - version "7.8.1" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" - integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== - dependencies: - tslib "^2.1.0" - sade@^1.7.3: version "1.8.1" resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" @@ -10114,7 +12257,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -10124,32 +12267,23 @@ safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sass-loader@^10.1.1: - version "10.5.2" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.5.2.tgz#1ca30534fff296417b853c7597ca3b0bbe8c37d0" - integrity sha512-vMUoSNOUKJILHpcNCCyD23X34gve1TS7Rjd9uXHeKqhvBG39x6XbswFDtpbTElj6XdMFezoWhkh5vtKudf2cgQ== - dependencies: - klona "^2.0.4" - loader-utils "^2.0.0" - neo-async "^2.6.2" - schema-utils "^3.0.0" - semver "^7.3.2" - -sass-loader@^13.3.2: - version "13.3.3" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-13.3.3.tgz#60df5e858788cffb1a3215e5b92e9cba61e7e133" - integrity sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA== +sass-loader@^16.0.2: + version "16.0.5" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-16.0.5.tgz#257bc90119ade066851cafe7f2c3f3504c7cda98" + integrity sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw== dependencies: neo-async "^2.6.2" -sass@^1.58.1: - version "1.77.5" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.5.tgz#5f9009820297521356e962c0bed13ee36710edfe" - integrity sha512-oDfX1mukIlxacPdQqNb6mV2tVCrnE+P3nVYioy72V5tlk56CPNcO4TCuFcaCRKKfJ1M3lH95CleRS+dVKL2qMg== +sass@^1.80.4: + version "1.89.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.89.0.tgz#6df72360c5c3ec2a9833c49adafe57b28206752d" + integrity sha512-ld+kQU8YTdGNjOLfRWBzewJpU5cwEv/h5yyqlSeJcj6Yh8U4TDA9UA5FPicqDz/xgRPWRSYIQNiFks21TbA9KQ== dependencies: - chokidar ">=3.0.0 <4.0.0" - immutable "^4.0.0" + chokidar "^4.0.0" + immutable "^5.0.2" source-map-js ">=0.6.2 <2.0.0" + optionalDependencies: + "@parcel/watcher" "^2.4.1" sax@^1.2.4: version "1.4.1" @@ -10163,14 +12297,10 @@ scheduler@^0.23.2: dependencies: loose-envify "^1.1.0" -schema-utils@2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" - integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== - dependencies: - "@types/json-schema" "^7.0.4" - ajv "^6.12.2" - ajv-keywords "^3.4.1" +schema-dts@^1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/schema-dts/-/schema-dts-1.1.5.tgz#9237725d305bac3469f02b292a035107595dc324" + integrity sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg== schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0: version "3.3.0" @@ -10191,6 +12321,16 @@ schema-utils@^4.0.0, schema-utils@^4.0.1: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" +schema-utils@^4.3.0, schema-utils@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.2.tgz#0c10878bf4a73fd2b1dfd14b9462b26788c806ae" + integrity sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + section-matter@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" @@ -10238,7 +12378,7 @@ semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.4: +semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.4: version "7.6.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== @@ -10262,25 +12402,24 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" -serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: +serialize-javascript@^6.0.0, serialize-javascript@^6.0.1, serialize-javascript@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== dependencies: randombytes "^2.1.0" -serve-handler@^6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.5.tgz#a4a0964f5c55c7e37a02a633232b6f0d6f068375" - integrity sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg== +serve-handler@^6.1.6: + version "6.1.6" + resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.6.tgz#50803c1d3e947cd4a341d617f8209b22bd76cfa1" + integrity sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ== dependencies: bytes "3.0.0" content-disposition "0.5.2" - fast-url-parser "1.1.3" mime-types "2.1.18" minimatch "3.1.2" path-is-inside "1.0.2" - path-to-regexp "2.2.1" + path-to-regexp "3.3.0" range-parser "1.2.0" serve-index@^1.9.1: @@ -10318,11 +12457,6 @@ set-function-length@^1.2.1: gopd "^1.0.1" has-property-descriptors "^1.0.2" -setimmediate@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== - setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" @@ -10333,14 +12467,6 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -10365,12 +12491,12 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@^1.7.3, shell-quote@^1.8.1: +shell-quote@^1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== -shelljs@0.8.5, shelljs@^0.8.5: +shelljs@0.8.5: version "0.8.5" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== @@ -10423,7 +12549,36 @@ should@^13.2.1: should-type-adaptors "^1.0.1" should-util "^1.0.0" -side-channel@^1.0.4, side-channel@^1.0.6: +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== @@ -10433,6 +12588,17 @@ side-channel@^1.0.4, side-channel@^1.0.6: get-intrinsic "^1.2.4" object-inspect "^1.13.1" +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -10457,16 +12623,6 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== -sitemap@7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/sitemap/-/sitemap-7.1.1.tgz#eeed9ad6d95499161a3eadc60f8c6dce4bea2bef" - integrity sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg== - dependencies: - "@types/node" "^17.0.5" - "@types/sax" "^1.2.1" - arg "^5.0.0" - sax "^1.2.4" - sitemap@^7.1.1: version "7.1.2" resolved "https://registry.yarnpkg.com/sitemap/-/sitemap-7.1.2.tgz#6ce1deb43f6f177c68bc59cf93632f54e3ae6b72" @@ -10592,28 +12748,10 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -std-env@^3.0.1: - version "3.7.0" - resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" - integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== - -stream-browserify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" - integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== - dependencies: - inherits "~2.0.4" - readable-stream "^3.5.0" - -stream-http@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-3.2.0.tgz#1872dfcf24cb15752677e40e5c3f9cc1926028b5" - integrity sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.4" - readable-stream "^3.6.0" - xtend "^4.0.2" +std-env@^3.7.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.9.0.tgz#1a6f7243b339dca4c9fd55e1c7504c77ef23e8f1" + integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw== "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" @@ -10642,7 +12780,7 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -string_decoder@^1.1.1, string_decoder@^1.3.0: +string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -10736,10 +12874,10 @@ stylehacks@^6.1.1: browserslist "^4.23.0" postcss-selector-parser "^6.0.16" -stylis@^4.1.3: - version "4.3.2" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.2.tgz#8f76b70777dd53eb669c6f58c997bf0a9972e444" - integrity sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg== +stylis@^4.3.6: + version "4.3.6" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.6.tgz#7c7b97191cb4f195f03ecab7d52f7902ed378320" + integrity sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ== sucrase@^3.31.0, sucrase@^3.32.0: version "3.35.0" @@ -10815,7 +12953,7 @@ swagger2openapi@7.0.8, swagger2openapi@^7.0.8: yaml "^1.10.0" yargs "^17.0.1" -swc-loader@^0.2.3: +swc-loader@^0.2.6: version "0.2.6" resolved "https://registry.yarnpkg.com/swc-loader/-/swc-loader-0.2.6.tgz#bf0cba8eeff34bb19620ead81d1277fefaec6bc8" integrity sha512-9Zi9UP2YmDpgmQVbyOPJClY0dwf58JDyDMQ7uRc4krmc72twNI2fvlBWHLqVekBpPc7h5NJkGVT1zNDxFrqhvg== @@ -10850,11 +12988,6 @@ tailwindcss@^3.2.4: resolve "^1.22.2" sucrase "^3.32.0" -tapable@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" - integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== - tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" @@ -10892,6 +13025,17 @@ terser-webpack-plugin@^5.3.10, terser-webpack-plugin@^5.3.9: serialize-javascript "^6.0.1" terser "^5.26.0" +terser-webpack-plugin@^5.3.11: + version "5.3.14" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz#9031d48e57ab27567f02ace85c7d690db66c3e06" + integrity sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.25" + jest-worker "^27.4.5" + schema-utils "^4.3.0" + serialize-javascript "^6.0.2" + terser "^5.31.1" + terser@^5.10.0, terser@^5.15.1, terser@^5.26.0: version "5.31.1" resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.1.tgz#735de3c987dd671e95190e6b98cfe2f07f3cf0d4" @@ -10902,10 +13046,15 @@ terser@^5.10.0, terser@^5.15.1, terser@^5.26.0: commander "^2.20.0" source-map-support "~0.5.20" -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +terser@^5.31.1: + version "5.40.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.40.0.tgz#839a80db42bfee8340085f44ea99b5cba36c55c8" + integrity sha512-cfeKl/jjwSR5ar7d0FGmave9hFGJT8obyo0z+CrQOylLDbk7X81nPU6vq9VORa5jU30SkDnT2FXjLbR8HLP+xA== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.14.0" + commander "^2.20.0" + source-map-support "~0.5.20" thenify-all@^1.0.0: version "1.6.0" @@ -10931,13 +13080,6 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== -timers-browserify@^2.0.12: - version "2.0.12" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" - integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ== - dependencies: - setimmediate "^1.0.4" - tiny-invariant@^1.0.2: version "1.3.3" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" @@ -10948,6 +13090,16 @@ tiny-warning@^1.0.0: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tinyexec@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.0.1.tgz#70c31ab7abbb4aea0a24f55d120e5990bfa1e0b1" + integrity sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw== + +tinypool@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.2.tgz#706193cc532f4c100f66aa00b01c42173d9051b2" + integrity sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA== + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -11005,22 +13157,22 @@ ts-interface-checker@^0.1.9: resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== -tslib@^2.0.3, tslib@^2.1.0, tslib@^2.6.0: +tslib@^2.0.3, tslib@^2.6.0: version "2.6.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== -tty-browserify@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811" - integrity sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== type-fest@^1.0.1: version "1.4.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== -type-fest@^2.13.0, type-fest@^2.14.0, type-fest@^2.5.0: +type-fest@^2.13.0, type-fest@^2.5.0: version "2.19.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== @@ -11040,6 +13192,11 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +ufo@^1.5.4: + version "1.6.1" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.1.tgz#ac2db1d54614d1b22c1d603e3aef44a85d8f146b" + integrity sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA== + unbzip2-stream@1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" @@ -11191,7 +13348,7 @@ unist-util-stringify-position@^4.0.0: dependencies: "@types/unist" "^3.0.0" -unist-util-visit-parents@^5.1.1: +unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1: version "5.1.3" resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== @@ -11243,6 +13400,14 @@ update-browserslist-db@^1.0.16: escalade "^3.1.2" picocolors "^1.0.1" +update-browserslist-db@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" + integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + update-notifier@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-6.0.2.tgz#a6990253dfe6d5a02bd04fbb6a61543f55026b60" @@ -11279,13 +13444,13 @@ url-loader@^4.1.1: mime-types "^2.1.27" schema-utils "^3.0.0" -url@^0.11.0: - version "0.11.3" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.3.tgz#6f495f4b935de40ce4a0a52faee8954244f3d3ad" - integrity sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw== +url@^0.11.1: + version "0.11.4" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.4.tgz#adca77b3562d56b72746e76b330b7f27b6721f3c" + integrity sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg== dependencies: punycode "^1.4.1" - qs "^6.11.2" + qs "^6.12.3" use-editable@^2.3.3: version "2.3.3" @@ -11304,17 +13469,6 @@ util@^0.10.3: dependencies: inherits "2.0.3" -util@^0.12.4, util@^0.12.5: - version "0.12.5" - resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" - integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== - dependencies: - inherits "^2.0.3" - is-arguments "^1.0.4" - is-generator-function "^1.0.7" - is-typed-array "^1.1.3" - which-typed-array "^1.1.2" - utila@~0.4: version "0.4.0" resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" @@ -11335,10 +13489,10 @@ uuid@8.3.2, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== +uuid@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" + integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== uvu@^0.5.0: version "0.5.6" @@ -11449,21 +13603,40 @@ vfile@^6.0.0, vfile@^6.0.1: unist-util-stringify-position "^4.0.0" vfile-message "^4.0.0" -vm-browserify@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" - integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +vscode-jsonrpc@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" + integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== -wait-on@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-6.0.1.tgz#16bbc4d1e4ebdd41c5b4e63a2e16dbd1f4e5601e" - integrity sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw== +vscode-languageserver-protocol@3.17.5: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea" + integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== dependencies: - axios "^0.25.0" - joi "^17.6.0" - lodash "^4.17.21" - minimist "^1.2.5" - rxjs "^7.5.4" + vscode-jsonrpc "8.2.0" + vscode-languageserver-types "3.17.5" + +vscode-languageserver-textdocument@~1.0.11: + version "1.0.12" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz#457ee04271ab38998a093c68c2342f53f6e4a631" + integrity sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA== + +vscode-languageserver-types@3.17.5: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" + integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== + +vscode-languageserver@~9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz#500aef82097eb94df90d008678b0b6b5f474015b" + integrity sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g== + dependencies: + vscode-languageserver-protocol "3.17.5" + +vscode-uri@~3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" + integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== warning@^4.0.3: version "4.0.3" @@ -11492,17 +13665,12 @@ web-namespaces@^2.0.0: resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== -web-worker@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.3.0.tgz#e5f2df5c7fe356755a5fb8f8410d4312627e6776" - integrity sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA== - webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== -webpack-bundle-analyzer@^4.9.0: +webpack-bundle-analyzer@^4.10.2: version "4.10.2" resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz#633af2862c213730be3dbdf40456db171b60d5bd" integrity sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw== @@ -11531,7 +13699,7 @@ webpack-dev-middleware@^5.3.4: range-parser "^1.2.1" schema-utils "^4.0.0" -webpack-dev-server@^4.15.1: +webpack-dev-server@^4.15.2: version "4.15.2" resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz#9e0c70a42a012560860adb186986da1248333173" integrity sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g== @@ -11576,12 +13744,21 @@ webpack-merge@^5.9.0: flat "^5.0.2" wildcard "^2.0.0" +webpack-merge@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-6.0.1.tgz#50c776868e080574725abc5869bd6e4ef0a16c6a" + integrity sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.1" + webpack-sources@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@^5.61.0, webpack@^5.88.1: +webpack@^5.88.1: version "5.92.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.92.0.tgz#cc114c71e6851d220b1feaae90159ed52c876bdf" integrity sha512-Bsw2X39MYIgxouNATyVpCNVWBCuUwDgWtN78g6lSdPJRLaQ/PUVm/oXcaRAyY/sMFoKFQrsPeqvTizWtq7QPCA== @@ -11611,15 +13788,49 @@ webpack@^5.61.0, webpack@^5.88.1: watchpack "^2.4.1" webpack-sources "^3.2.3" -webpackbar@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-5.0.2.tgz#d3dd466211c73852741dfc842b7556dcbc2b0570" - integrity sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ== +webpack@^5.95.0: + version "5.99.9" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.9.tgz#d7de799ec17d0cce3c83b70744b4aedb537d8247" + integrity sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg== dependencies: - chalk "^4.1.0" - consola "^2.15.3" + "@types/eslint-scope" "^3.7.7" + "@types/estree" "^1.0.6" + "@types/json-schema" "^7.0.15" + "@webassemblyjs/ast" "^1.14.1" + "@webassemblyjs/wasm-edit" "^1.14.1" + "@webassemblyjs/wasm-parser" "^1.14.1" + acorn "^8.14.0" + browserslist "^4.24.0" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.17.1" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.11" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^4.3.2" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.11" + watchpack "^2.4.1" + webpack-sources "^3.2.3" + +webpackbar@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-6.0.1.tgz#5ef57d3bf7ced8b19025477bc7496ea9d502076b" + integrity sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q== + dependencies: + ansi-escapes "^4.3.2" + chalk "^4.1.2" + consola "^3.2.3" + figures "^3.2.0" + markdown-table "^2.0.0" pretty-time "^1.1.0" - std-env "^3.0.1" + std-env "^3.7.0" + wrap-ansi "^7.0.0" websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" @@ -11643,24 +13854,6 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" -which-typed-array@^1.1.14, which-typed-array@^1.1.2: - version "1.1.15" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" - integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.2" - -which@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -11675,7 +13868,7 @@ widest-line@^4.0.1: dependencies: string-width "^5.0.1" -wildcard@^2.0.0: +wildcard@^2.0.0, wildcard@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== @@ -11761,11 +13954,6 @@ xml-parser-xo@^3.2.0: resolved "https://registry.yarnpkg.com/xml-parser-xo/-/xml-parser-xo-3.2.0.tgz#c633ab55cf1976d6b03ab4a6a85045093ac32b73" integrity sha512-8LRU6cq+d7mVsoDaMhnkkt3CTtAs4153p49fRo+HIB3I1FD1o5CeXRjRH29sQevIfVJIcPjKSsPU/+Ujhq09Rg== -xtend@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" @@ -11786,7 +13974,7 @@ yaml-ast-parser@0.0.43: resolved "https://registry.yarnpkg.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb" integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A== -yaml@1.10.2, yaml@^1.10.0, yaml@^1.7.2: +yaml@1.10.2, yaml@^1.10.0: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== @@ -11822,11 +14010,6 @@ yauzl@^2.10.0: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - yocto-queue@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" From cdf1860083e02906d0399d5c210135ccb1fda242 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Mon, 2 Jun 2025 00:42:11 -0700 Subject: [PATCH 10/35] chore: remove unparsed md characters (#9983) This pull request includes a minor change to the `README.md` file. It removes a broken markdown link syntax for an image and replaces it with the correct image syntax to properly display the "New Login Showcase" image. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 285e50964c..3d33e20e57 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ Use [Console](https://zitadel.com/docs/guides/manage/console/overview) or our [A ### Login V2 Check out our new Login V2 version in our [typescript repository](https://github.com/zitadel/typescript) or in our [documentation](https://zitadel.com/docs/guides/integrate/login/hosted-login#hosted-login-version-2-beta) -[![New Login Showcase](https://github.com/user-attachments/assets/cb5c5212-128b-4dc9-b11d-cabfd3f73e26)] +![New Login Showcase](https://github.com/user-attachments/assets/cb5c5212-128b-4dc9-b11d-cabfd3f73e26) ## Security From b660d6ab9a2cd26c579693264d5d20a39ecc6c7d Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Mon, 2 Jun 2025 10:16:13 +0200 Subject: [PATCH 11/35] fix(queue): reset projection list before each `Register` call (#10001) # Which Problems Are Solved if Zitadel was started using `start-from-init` or `start-from-setup` there were rare cases where a panic occured when `Notifications.LegacyEnabled` was set to false. The cause was a list which was not reset before refilling. # How the Problems Are Solved The list is now reset before each time it gets filled. # Additional Changes Ensure all contexts are canceled for the init and setup functions for `start-from-init- or `start-from-setup` commands. # Additional Context none --- cmd/start/start_from_init.go | 11 +++++++++-- cmd/start/start_from_setup.go | 7 ++++++- internal/notification/projections.go | 3 +++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/cmd/start/start_from_init.go b/cmd/start/start_from_init.go index 62d705b33c..41972e16ad 100644 --- a/cmd/start/start_from_init.go +++ b/cmd/start/start_from_init.go @@ -1,6 +1,8 @@ package start import ( + "context" + "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/zitadel/logging" @@ -29,14 +31,19 @@ Requirements: masterKey, err := key.MasterKey(cmd) logging.OnError(err).Panic("No master key provided") - initialise.InitAll(cmd.Context(), initialise.MustNewConfig(viper.GetViper())) + initCtx, cancel := context.WithCancel(cmd.Context()) + initialise.InitAll(initCtx, initialise.MustNewConfig(viper.GetViper())) + cancel() err = setup.BindInitProjections(cmd) logging.OnError(err).Fatal("unable to bind \"init-projections\" flag") setupConfig := setup.MustNewConfig(viper.GetViper()) setupSteps := setup.MustNewSteps(viper.New()) - setup.Setup(cmd.Context(), setupConfig, setupSteps, masterKey) + + setupCtx, cancel := context.WithCancel(cmd.Context()) + setup.Setup(setupCtx, setupConfig, setupSteps, masterKey) + cancel() startConfig := MustNewConfig(viper.GetViper()) diff --git a/cmd/start/start_from_setup.go b/cmd/start/start_from_setup.go index a8b7295f2a..3e8a13705e 100644 --- a/cmd/start/start_from_setup.go +++ b/cmd/start/start_from_setup.go @@ -1,6 +1,8 @@ package start import ( + "context" + "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/zitadel/logging" @@ -34,7 +36,10 @@ Requirements: setupConfig := setup.MustNewConfig(viper.GetViper()) setupSteps := setup.MustNewSteps(viper.New()) - setup.Setup(cmd.Context(), setupConfig, setupSteps, masterKey) + + setupCtx, cancel := context.WithCancel(cmd.Context()) + setup.Setup(setupCtx, setupConfig, setupSteps, masterKey) + cancel() startConfig := MustNewConfig(viper.GetViper()) diff --git a/internal/notification/projections.go b/internal/notification/projections.go index 7fda08135c..7fedaaf301 100644 --- a/internal/notification/projections.go +++ b/internal/notification/projections.go @@ -43,6 +43,9 @@ func Register( queue.ShouldStart() } + // make sure the slice does not contain old values + projections = nil + q := handlers.NewNotificationQueries(queries, es, externalDomain, externalPort, externalSecure, fileSystemPath, userEncryption, smtpEncryption, smsEncryption) c := newChannels(q) projections = append(projections, handlers.NewUserNotifier(ctx, projection.ApplyCustomConfig(userHandlerCustomConfig), commands, q, c, otpEmailTmpl, notificationWorkerConfig, queue)) From b46c41e4bf50af7f3873c6d0deb62206d77ec6e9 Mon Sep 17 00:00:00 2001 From: Iraq <66622793+kkrime@users.noreply.github.com> Date: Mon, 2 Jun 2025 10:40:19 +0200 Subject: [PATCH 12/35] fix(settings): fix for setting restricted languages (#9947) # Which Problems Are Solved Zitadel encounters a migration error when setting `restricted languages` and fails to start. # How the Problems Are Solved The problem is that there is a check that checks that at least one of the restricted languages is the same as the `default language`, however, in the `authz instance` (where the default language is pulled form) is never set. I've added code to set the `default language` in the `authz instance` # Additional Context - Closes https://github.com/zitadel/zitadel/issues/9787 --------- Co-authored-by: Livio Spring --- internal/api/authz/instance.go | 30 +++++++++++++++++---------- internal/command/instance.go | 27 +++++++++++++----------- internal/command/instance_test.go | 34 ++++++++++++++++--------------- 3 files changed, 52 insertions(+), 39 deletions(-) diff --git a/internal/api/authz/instance.go b/internal/api/authz/instance.go index 7ee8d605ca..0fe6d6c8aa 100644 --- a/internal/api/authz/instance.go +++ b/internal/api/authz/instance.go @@ -9,9 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/feature" ) -var ( - emptyInstance = &instance{} -) +var emptyInstance = &instance{} type Instance interface { InstanceID() string @@ -33,13 +31,13 @@ type InstanceVerifier interface { } type instance struct { - id string - domain string - projectID string - appID string - clientID string - orgID string - features feature.Features + id string + projectID string + appID string + clientID string + orgID string + defaultLanguage language.Tag + features feature.Features } func (i *instance) Block() *bool { @@ -67,7 +65,7 @@ func (i *instance) ConsoleApplicationID() string { } func (i *instance) DefaultLanguage() language.Tag { - return language.Und + return i.defaultLanguage } func (i *instance) DefaultOrganisationID() string { @@ -106,6 +104,16 @@ func WithInstanceID(ctx context.Context, id string) context.Context { return context.WithValue(ctx, instanceKey, &instance{id: id}) } +func WithDefaultLanguage(ctx context.Context, defaultLanguage language.Tag) context.Context { + i, ok := ctx.Value(instanceKey).(*instance) + if !ok { + i = new(instance) + } + + i.defaultLanguage = defaultLanguage + return context.WithValue(ctx, instanceKey, i) +} + func WithConsole(ctx context.Context, projectID, appID string) context.Context { i, ok := ctx.Value(instanceKey).(*instance) if !ok { diff --git a/internal/command/instance.go b/internal/command/instance.go index d71be53468..cfafb1d298 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -221,7 +221,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str if err := setup.generateIDs(c.idGenerator); err != nil { return "", "", nil, nil, err } - ctx = contextWithInstanceSetupInfo(ctx, setup.zitadel.instanceID, setup.zitadel.projectID, setup.zitadel.consoleAppID, c.externalDomain) + ctx = contextWithInstanceSetupInfo(ctx, setup.zitadel.instanceID, setup.zitadel.projectID, setup.zitadel.consoleAppID, c.externalDomain, setup.DefaultLanguage) validations, pat, machineKey, err := setUpInstance(ctx, c, setup) if err != nil { @@ -255,19 +255,22 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str return setup.zitadel.instanceID, token, machineKey, details, nil } -func contextWithInstanceSetupInfo(ctx context.Context, instanceID, projectID, consoleAppID, externalDomain string) context.Context { - return authz.WithConsole( - authz.SetCtxData( - http.WithRequestedHost( - authz.WithInstanceID( - ctx, - instanceID), - externalDomain, +func contextWithInstanceSetupInfo(ctx context.Context, instanceID, projectID, consoleAppID, externalDomain string, defaultLanguage language.Tag) context.Context { + return authz.WithDefaultLanguage( + authz.WithConsole( + authz.SetCtxData( + http.WithRequestedHost( + authz.WithInstanceID( + ctx, + instanceID), + externalDomain, + ), + authz.CtxData{ResourceOwner: instanceID}, ), - authz.CtxData{ResourceOwner: instanceID}, + projectID, + consoleAppID, ), - projectID, - consoleAppID, + defaultLanguage, ) } diff --git a/internal/command/instance_test.go b/internal/command/instance_test.go index 16e51d844d..2b82818a7e 100644 --- a/internal/command/instance_test.go +++ b/internal/command/instance_test.go @@ -345,6 +345,7 @@ func instanceElementsEvents(ctx context.Context, instanceID, instanceName string instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeOTPEmail, 8, 5*time.Minute, false, false, true, false), } } + func instanceElementsConfig() *SecretGenerators { return &SecretGenerators{ ClientSecret: &crypto.GeneratorConfig{Length: 64, IncludeLowerLetters: true, IncludeUpperLetters: true, IncludeDigits: true}, @@ -668,22 +669,23 @@ func TestCommandSide_setupMinimalInterfaces(t *testing.T) { eventstore: expectEventstore( slices.Concat( projectFilters(), - []expect{expectPush( - projectAddedEvents(context.Background(), - "INSTANCE", - "ORG", - "PROJECT", - "owner", - false, - )..., - ), + []expect{ + expectPush( + projectAddedEvents(context.Background(), + "INSTANCE", + "ORG", + "PROJECT", + "owner", + false, + )..., + ), }, )..., ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, projectClientIDs()...), }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), instanceAgg: instance.NewAggregate("INSTANCE"), orgAgg: org.NewAggregate("ORG"), owner: "owner", @@ -767,7 +769,7 @@ func TestCommandSide_setupAdmins(t *testing.T) { }, }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), instanceAgg: instance.NewAggregate("INSTANCE"), orgAgg: org.NewAggregate("ORG"), human: instanceSetupHumanConfig(), @@ -806,7 +808,7 @@ func TestCommandSide_setupAdmins(t *testing.T) { keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), instanceAgg: instance.NewAggregate("INSTANCE"), orgAgg: org.NewAggregate("ORG"), machine: instanceSetupMachineConfig(), @@ -855,7 +857,7 @@ func TestCommandSide_setupAdmins(t *testing.T) { keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), instanceAgg: instance.NewAggregate("INSTANCE"), orgAgg: org.NewAggregate("ORG"), machine: instanceSetupMachineConfig(), @@ -972,7 +974,7 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), instanceAgg: instance.NewAggregate("INSTANCE"), orgName: "ZITADEL", machine: &AddMachine{ @@ -1097,7 +1099,7 @@ func TestCommandSide_setupInstanceElements(t *testing.T) { ), }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), instanceAgg: instance.NewAggregate("INSTANCE"), setup: setupInstanceElementsConfig(), }, @@ -1183,7 +1185,7 @@ func TestCommandSide_setUpInstance(t *testing.T) { }, }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), setup: setupInstanceConfig(), }, res: res{ From b3d22dba0535f3818b69d2f8f62216f5e0365695 Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Mon, 2 Jun 2025 17:29:56 +0200 Subject: [PATCH 13/35] docs(10016): cockroach compatibility (#10010) # Which Problems Are Solved If the sql statement of technical advisory 10016 gets executed on cockroach the following error is raised: ``` ERROR: WITH clause "fixed" does not return any columns SQLSTATE: 0A000 HINT: missing RETURNING clause? ``` # How the Problems Are Solved Fixed the statement by adding `returning` to statement --- docs/docs/support/advisory/a10016.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/docs/support/advisory/a10016.md b/docs/docs/support/advisory/a10016.md index 84dd1cd34c..38d73e6078 100644 --- a/docs/docs/support/advisory/a10016.md +++ b/docs/docs/support/advisory/a10016.md @@ -78,6 +78,7 @@ with and s.aggregate_id = b.aggregate_id and s.aggregate_type = b.aggregate_type and s.sequence = b.sequence + returning * ) select b.projection_name, From ae1a2e93c1e10a05d43762654ba8b4c8daed6a3d Mon Sep 17 00:00:00 2001 From: Iraq <66622793+kkrime@users.noreply.github.com> Date: Mon, 2 Jun 2025 18:27:53 +0200 Subject: [PATCH 14/35] feat(api): moving organization API resourced based (#9943) --- cmd/start/start.go | 2 +- docs/docusaurus.config.js | 8 + docs/sidebars.js | 13 + internal/api/grpc/instance/converter.go | 4 +- .../v2beta/integration_test/instance_test.go | 5 +- .../v2beta/integration_test/query_test.go | 5 +- internal/api/grpc/management/org_converter.go | 4 +- internal/api/grpc/management/user.go | 4 + internal/api/grpc/metadata/v2beta/metadata.go | 49 + internal/api/grpc/object/v2beta/converter.go | 55 + internal/api/grpc/org/v2beta/helper.go | 256 +++ .../org/v2beta/integration_test/org_test.go | 1815 ++++++++++++++++- internal/api/grpc/org/v2beta/org.go | 238 ++- internal/api/grpc/org/v2beta/org_test.go | 45 +- internal/api/grpc/org/v2beta/server.go | 4 + internal/query/org_metadata.go | 1 - proto/zitadel/admin.proto | 28 +- proto/zitadel/management.proto | 258 +-- proto/zitadel/metadata/v2beta/metadata.proto | 57 + proto/zitadel/org/v2beta/org.proto | 169 ++ proto/zitadel/org/v2beta/org_service.proto | 825 +++++++- 21 files changed, 3542 insertions(+), 303 deletions(-) create mode 100644 internal/api/grpc/metadata/v2beta/metadata.go create mode 100644 internal/api/grpc/org/v2beta/helper.go create mode 100644 proto/zitadel/metadata/v2beta/metadata.proto create mode 100644 proto/zitadel/org/v2beta/org.proto diff --git a/cmd/start/start.go b/cmd/start/start.go index af76b29e99..2fc1fb8413 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -470,7 +470,7 @@ func startAPIs( if err := apis.RegisterService(ctx, settings_v2beta.CreateServer(commands, queries)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, org_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil { + if err := apis.RegisterService(ctx, org_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { return nil, err } if err := apis.RegisterService(ctx, feature_v2beta.CreateServer(commands, queries)); err != nil { diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index c161d38d9f..43830eafd0 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -342,6 +342,14 @@ module.exports = { categoryLinkSource: "auto", }, }, + org_v2beta: { + specPath: ".artifacts/openapi/zitadel/org/v2beta/org_service.swagger.json", + outputDir: "docs/apis/resources/org_service_v2beta", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "auto", + }, + }, project_v2beta: { specPath: ".artifacts/openapi/zitadel/project/v2beta/project_service.swagger.json", outputDir: "docs/apis/resources/project_service_v2", diff --git a/docs/sidebars.js b/docs/sidebars.js index b7a399ecf1..f9b97703e5 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -10,6 +10,7 @@ const sidebar_api_oidc_service_v2 = require("./docs/apis/resources/oidc_service_ const sidebar_api_settings_service_v2 = require("./docs/apis/resources/settings_service_v2/sidebar.ts").default const sidebar_api_feature_service_v2 = require("./docs/apis/resources/feature_service_v2/sidebar.ts").default const sidebar_api_org_service_v2 = require("./docs/apis/resources/org_service_v2/sidebar.ts").default +const sidebar_api_org_service_v2beta = require("./docs/apis/resources/org_service_v2beta/sidebar.ts").default const sidebar_api_idp_service_v2 = require("./docs/apis/resources/idp_service_v2/sidebar.ts").default const sidebar_api_actions_v2 = require("./docs/apis/resources/action_service_v2/sidebar.ts").default const sidebar_api_project_service_v2 = require("./docs/apis/resources/project_service_v2/sidebar.ts").default @@ -791,6 +792,18 @@ module.exports = { }, items: sidebar_api_org_service_v2, }, + { + type: "category", + label: "Organization (Beta)", + link: { + type: "generated-index", + title: "Organization Service beta API", + slug: "/apis/resources/org_service/v2beta", + description: + "This API is intended to manage organizations for ZITADEL. \n", + }, + items: sidebar_api_org_service_v2beta, + }, { type: "category", label: "Identity Provider", diff --git a/internal/api/grpc/instance/converter.go b/internal/api/grpc/instance/converter.go index 4094da4a77..b894a064ff 100644 --- a/internal/api/grpc/instance/converter.go +++ b/internal/api/grpc/instance/converter.go @@ -28,7 +28,7 @@ func InstanceToPb(instance *query.Instance) *instance_pb.Instance { Name: instance.Name, Domains: DomainsToPb(instance.Domains), Version: build.Version(), - State: instance_pb.State_STATE_RUNNING, //TODO: change when delete is implemented + State: instance_pb.State_STATE_RUNNING, // TODO: change when delete is implemented } } @@ -44,7 +44,7 @@ func InstanceDetailToPb(instance *query.Instance) *instance_pb.InstanceDetail { Name: instance.Name, Domains: DomainsToPb(instance.Domains), Version: build.Version(), - State: instance_pb.State_STATE_RUNNING, //TODO: change when delete is implemented + State: instance_pb.State_STATE_RUNNING, // TODO: change when delete is implemented } } diff --git a/internal/api/grpc/instance/v2beta/integration_test/instance_test.go b/internal/api/grpc/instance/v2beta/integration_test/instance_test.go index 5187bbc78d..ae277c6d13 100644 --- a/internal/api/grpc/instance/v2beta/integration_test/instance_test.go +++ b/internal/api/grpc/instance/v2beta/integration_test/instance_test.go @@ -9,10 +9,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/zitadel/internal/integration" - instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + + "github.com/zitadel/zitadel/internal/integration" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" ) func TestDeleteInstace(t *testing.T) { diff --git a/internal/api/grpc/instance/v2beta/integration_test/query_test.go b/internal/api/grpc/instance/v2beta/integration_test/query_test.go index 0828b006e3..e59a16a932 100644 --- a/internal/api/grpc/instance/v2beta/integration_test/query_test.go +++ b/internal/api/grpc/instance/v2beta/integration_test/query_test.go @@ -11,12 +11,13 @@ import ( "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "github.com/zitadel/zitadel/internal/integration" filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" "github.com/zitadel/zitadel/pkg/grpc/object/v2" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" ) func TestGetInstance(t *testing.T) { diff --git a/internal/api/grpc/management/org_converter.go b/internal/api/grpc/management/org_converter.go index 879b5e0763..03de84cdf4 100644 --- a/internal/api/grpc/management/org_converter.go +++ b/internal/api/grpc/management/org_converter.go @@ -26,7 +26,7 @@ func ListOrgDomainsRequestToModel(req *mgmt_pb.ListOrgDomainsRequest) (*query.Or Limit: limit, Asc: asc, }, - //SortingColumn: //TODO: sorting + // SortingColumn: //TODO: sorting Queries: queries, }, nil } @@ -89,7 +89,7 @@ func ListOrgMembersRequestToModel(ctx context.Context, req *mgmt_pb.ListOrgMembe Offset: offset, Limit: limit, Asc: asc, - //SortingColumn: //TODO: sorting + // SortingColumn: //TODO: sorting }, Queries: queries, }, diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 5b82eb5afe..f318051e63 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -901,6 +901,7 @@ func (s *Server) ListHumanLinkedIDPs(ctx context.Context, req *mgmt_pb.ListHuman Details: obj_grpc.ToListDetails(res.Count, res.Sequence, res.LastRun), }, nil } + func (s *Server) RemoveHumanLinkedIDP(ctx context.Context, req *mgmt_pb.RemoveHumanLinkedIDPRequest) (*mgmt_pb.RemoveHumanLinkedIDPResponse, error) { objectDetails, err := s.command.RemoveUserIDPLink(ctx, RemoveHumanLinkedIDPRequestToDomain(ctx, req)) if err != nil { @@ -947,18 +948,21 @@ func cascadingIAMMembership(membership *query.IAMMembership) *command.CascadingI } return &command.CascadingIAMMembership{IAMID: membership.IAMID} } + func cascadingOrgMembership(membership *query.OrgMembership) *command.CascadingOrgMembership { if membership == nil { return nil } return &command.CascadingOrgMembership{OrgID: membership.OrgID} } + func cascadingProjectMembership(membership *query.ProjectMembership) *command.CascadingProjectMembership { if membership == nil { return nil } return &command.CascadingProjectMembership{ProjectID: membership.ProjectID} } + func cascadingProjectGrantMembership(membership *query.ProjectGrantMembership) *command.CascadingProjectGrantMembership { if membership == nil { return nil diff --git a/internal/api/grpc/metadata/v2beta/metadata.go b/internal/api/grpc/metadata/v2beta/metadata.go new file mode 100644 index 0000000000..57da21dfd2 --- /dev/null +++ b/internal/api/grpc/metadata/v2beta/metadata.go @@ -0,0 +1,49 @@ +package metadata + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + v2beta_object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + meta_pb "github.com/zitadel/zitadel/pkg/grpc/metadata/v2beta" +) + +// code in this file is copied from internal/api/grpc/metadata/metadata.go + +func OrgMetadataListToPb(dataList []*query.OrgMetadata) []*meta_pb.Metadata { + mds := make([]*meta_pb.Metadata, len(dataList)) + for i, data := range dataList { + mds[i] = OrgMetadataToPb(data) + } + return mds +} + +func OrgMetadataToPb(data *query.OrgMetadata) *meta_pb.Metadata { + return &meta_pb.Metadata{ + Key: data.Key, + Value: data.Value, + CreationDate: timestamppb.New(data.CreationDate), + ChangeDate: timestamppb.New(data.ChangeDate), + } +} + +func OrgMetadataQueriesToQuery(queries []*meta_pb.MetadataQuery) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = OrgMetadataQueryToQuery(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func OrgMetadataQueryToQuery(metadataQuery *meta_pb.MetadataQuery) (query.SearchQuery, error) { + switch q := metadataQuery.Query.(type) { + case *meta_pb.MetadataQuery_KeyQuery: + return query.NewOrgMetadataKeySearchQuery(q.KeyQuery.Key, v2beta_object.TextMethodToQuery(q.KeyQuery.Method)) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "METAD-fdg23", "List.Query.Invalid") + } +} diff --git a/internal/api/grpc/object/v2beta/converter.go b/internal/api/grpc/object/v2beta/converter.go index 9b14bb677a..73d5f18843 100644 --- a/internal/api/grpc/object/v2beta/converter.go +++ b/internal/api/grpc/object/v2beta/converter.go @@ -9,6 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + org_pb "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" ) func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details { @@ -34,6 +35,7 @@ func ToListDetails(response query.SearchResponse) *object.ListDetails { return details } + func ListQueryToQuery(query *object.ListQuery) (offset, limit uint64, asc bool) { if query == nil { return 0, 0, false @@ -73,3 +75,56 @@ func TextMethodToQuery(method object.TextQueryMethod) query.TextComparison { return -1 } } + +func ListQueryToModel(query *object.ListQuery) (offset, limit uint64, asc bool) { + if query == nil { + return 0, 0, false + } + return query.Offset, uint64(query.Limit), query.Asc +} + +func DomainsToPb(domains []*query.Domain) []*org_pb.Domain { + d := make([]*org_pb.Domain, len(domains)) + for i, domain := range domains { + d[i] = DomainToPb(domain) + } + return d +} + +func DomainToPb(d *query.Domain) *org_pb.Domain { + return &org_pb.Domain{ + OrganizationId: d.OrgID, + DomainName: d.Domain, + IsVerified: d.IsVerified, + IsPrimary: d.IsPrimary, + ValidationType: DomainValidationTypeFromModel(d.ValidationType), + } +} + +func DomainValidationTypeFromModel(validationType domain.OrgDomainValidationType) org_pb.DomainValidationType { + switch validationType { + case domain.OrgDomainValidationTypeDNS: + return org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS + case domain.OrgDomainValidationTypeHTTP: + return org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP + case domain.OrgDomainValidationTypeUnspecified: + // added to please golangci-lint + return org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_UNSPECIFIED + default: + return org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_UNSPECIFIED + } +} + +func DomainValidationTypeToDomain(validationType org_pb.DomainValidationType) domain.OrgDomainValidationType { + switch validationType { + case org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP: + return domain.OrgDomainValidationTypeHTTP + case org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS: + return domain.OrgDomainValidationTypeDNS + case org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_UNSPECIFIED: + // added to please golangci-lint + return domain.OrgDomainValidationTypeUnspecified + default: + return domain.OrgDomainValidationTypeUnspecified + } +} diff --git a/internal/api/grpc/org/v2beta/helper.go b/internal/api/grpc/org/v2beta/helper.go new file mode 100644 index 0000000000..39bad0dae2 --- /dev/null +++ b/internal/api/grpc/org/v2beta/helper.go @@ -0,0 +1,256 @@ +package org + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + // TODO fix below + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" + metadata "github.com/zitadel/zitadel/internal/api/grpc/metadata/v2beta" + v2beta_object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + v2beta "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" +) + +// NOTE: most of this code is copied from `internal/api/grpc/admin/*`, as we will eventually axe the previous versons of the API, +// we will have code duplication until then + +func listOrgRequestToModel(systemDefaults systemdefaults.SystemDefaults, request *v2beta_org.ListOrganizationsRequest) (*query.OrgSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(systemDefaults, request.Pagination) + if err != nil { + return nil, err + } + queries, err := OrgQueriesToModel(request.Filter) + if err != nil { + return nil, err + } + return &query.OrgSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + SortingColumn: FieldNameToOrgColumn(request.SortingColumn), + Asc: asc, + }, + Queries: queries, + }, nil +} + +func OrganizationViewToPb(org *query.Org) *v2beta_org.Organization { + return &v2beta_org.Organization{ + Id: org.ID, + State: OrgStateToPb(org.State), + Name: org.Name, + PrimaryDomain: org.Domain, + CreationDate: timestamppb.New(org.CreationDate), + ChangedDate: timestamppb.New(org.ChangeDate), + } +} + +func OrgStateToPb(state domain.OrgState) v2beta_org.OrgState { + switch state { + case domain.OrgStateActive: + return v2beta_org.OrgState_ORG_STATE_ACTIVE + case domain.OrgStateInactive: + return v2beta_org.OrgState_ORG_STATE_INACTIVE + case domain.OrgStateRemoved: + // added to please golangci-lint + return v2beta_org.OrgState_ORG_STATE_REMOVED + case domain.OrgStateUnspecified: + // added to please golangci-lint + return v2beta_org.OrgState_ORG_STATE_UNSPECIFIED + default: + return v2beta_org.OrgState_ORG_STATE_UNSPECIFIED + } +} + +func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *org.CreateOrganizationResponse, err error) { + admins := make([]*org.CreatedAdmin, len(createdOrg.CreatedAdmins)) + for i, admin := range createdOrg.CreatedAdmins { + admins[i] = &org.CreatedAdmin{ + UserId: admin.ID, + EmailCode: admin.EmailCode, + PhoneCode: admin.PhoneCode, + } + } + return &org.CreateOrganizationResponse{ + CreationDate: timestamppb.New(createdOrg.ObjectDetails.EventDate), + Id: createdOrg.ObjectDetails.ResourceOwner, + CreatedAdmins: admins, + }, nil +} + +func OrgViewsToPb(orgs []*query.Org) []*v2beta_org.Organization { + o := make([]*v2beta_org.Organization, len(orgs)) + for i, org := range orgs { + o[i] = OrganizationViewToPb(org) + } + return o +} + +func OrgQueriesToModel(queries []*v2beta_org.OrganizationSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = OrgQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func OrgQueryToModel(apiQuery *v2beta_org.OrganizationSearchFilter) (query.SearchQuery, error) { + switch q := apiQuery.Filter.(type) { + case *v2beta_org.OrganizationSearchFilter_DomainFilter: + return query.NewOrgVerifiedDomainSearchQuery(v2beta_object.TextMethodToQuery(q.DomainFilter.Method), q.DomainFilter.Domain) + case *v2beta_org.OrganizationSearchFilter_NameFilter: + return query.NewOrgNameSearchQuery(v2beta_object.TextMethodToQuery(q.NameFilter.Method), q.NameFilter.Name) + case *v2beta_org.OrganizationSearchFilter_StateFilter: + return query.NewOrgStateSearchQuery(OrgStateToDomain(q.StateFilter.State)) + case *v2beta_org.OrganizationSearchFilter_IdFilter: + return query.NewOrgIDSearchQuery(q.IdFilter.Id) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-vR9nC", "List.Query.Invalid") + } +} + +func OrgStateToDomain(state v2beta_org.OrgState) domain.OrgState { + switch state { + case v2beta_org.OrgState_ORG_STATE_ACTIVE: + return domain.OrgStateActive + case v2beta_org.OrgState_ORG_STATE_INACTIVE: + return domain.OrgStateInactive + case v2beta_org.OrgState_ORG_STATE_REMOVED: + // added to please golangci-lint + return domain.OrgStateRemoved + case v2beta_org.OrgState_ORG_STATE_UNSPECIFIED: + fallthrough + default: + return domain.OrgStateUnspecified + } +} + +func FieldNameToOrgColumn(fieldName v2beta_org.OrgFieldName) query.Column { + switch fieldName { + case v2beta_org.OrgFieldName_ORG_FIELD_NAME_NAME: + return query.OrgColumnName + case v2beta_org.OrgFieldName_ORG_FIELD_NAME_CREATION_DATE: + return query.OrgColumnCreationDate + case v2beta_org.OrgFieldName_ORG_FIELD_NAME_UNSPECIFIED: + return query.Column{} + default: + return query.Column{} + } +} + +func ListOrgDomainsRequestToModel(systemDefaults systemdefaults.SystemDefaults, request *org.ListOrganizationDomainsRequest) (*query.OrgDomainSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(systemDefaults, request.Pagination) + if err != nil { + return nil, err + } + queries, err := DomainQueriesToModel(request.Filters) + if err != nil { + return nil, err + } + return &query.OrgDomainSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, + // SortingColumn: //TODO: sorting + Queries: queries, + }, nil +} + +func ListQueryToModel(query *v2beta.ListQuery) (offset, limit uint64, asc bool) { + if query == nil { + return 0, 0, false + } + return query.Offset, uint64(query.Limit), query.Asc +} + +func DomainQueriesToModel(queries []*v2beta_org.DomainSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = DomainQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func DomainQueryToModel(searchQuery *v2beta_org.DomainSearchFilter) (query.SearchQuery, error) { + switch q := searchQuery.Filter.(type) { + case *v2beta_org.DomainSearchFilter_DomainNameFilter: + return query.NewOrgDomainDomainSearchQuery(v2beta_object.TextMethodToQuery(q.DomainNameFilter.Method), q.DomainNameFilter.Name) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-Ags89", "List.Query.Invalid") + } +} + +func RemoveOrgDomainRequestToDomain(ctx context.Context, req *v2beta_org.DeleteOrganizationDomainRequest) *domain.OrgDomain { + return &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.OrganizationId, + }, + Domain: req.Domain, + } +} + +func GenerateOrgDomainValidationRequestToDomain(ctx context.Context, req *v2beta_org.GenerateOrganizationDomainValidationRequest) *domain.OrgDomain { + return &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.OrganizationId, + }, + Domain: req.Domain, + ValidationType: v2beta_object.DomainValidationTypeToDomain(req.Type), + } +} + +func ValidateOrgDomainRequestToDomain(ctx context.Context, req *v2beta_org.VerifyOrganizationDomainRequest) *domain.OrgDomain { + return &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.OrganizationId, + }, + Domain: req.Domain, + } +} + +func BulkSetOrgMetadataToDomain(req *v2beta_org.SetOrganizationMetadataRequest) []*domain.Metadata { + metadata := make([]*domain.Metadata, len(req.Metadata)) + for i, data := range req.Metadata { + metadata[i] = &domain.Metadata{ + Key: data.Key, + Value: data.Value, + } + } + return metadata +} + +func ListOrgMetadataToDomain(systemDefaults systemdefaults.SystemDefaults, request *v2beta_org.ListOrganizationMetadataRequest) (*query.OrgMetadataSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(systemDefaults, request.Pagination) + if err != nil { + return nil, err + } + queries, err := metadata.OrgMetadataQueriesToQuery(request.Filter) + if err != nil { + return nil, err + } + return &query.OrgMetadataSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, + Queries: queries, + }, nil +} diff --git a/internal/api/grpc/org/v2beta/integration_test/org_test.go b/internal/api/grpc/org/v2beta/integration_test/org_test.go index a2b2bf6047..4e0ec26121 100644 --- a/internal/api/grpc/org/v2beta/integration_test/org_test.go +++ b/internal/api/grpc/org/v2beta/integration_test/org_test.go @@ -4,7 +4,9 @@ package org_test import ( "context" + "errors" "os" + "strings" "testing" "time" @@ -14,7 +16,10 @@ import ( "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/admin" + v2beta_object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" "github.com/zitadel/zitadel/pkg/grpc/user/v2" user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) @@ -22,7 +27,7 @@ import ( var ( CTX context.Context Instance *integration.Instance - Client org.OrganizationServiceClient + Client v2beta_org.OrganizationServiceClient User *user.AddHumanUserResponse ) @@ -40,20 +45,21 @@ func TestMain(m *testing.M) { }()) } -func TestServer_AddOrganization(t *testing.T) { +func TestServer_CreateOrganization(t *testing.T) { idpResp := Instance.AddGenericOAuthProvider(CTX, Instance.DefaultOrg.Id) tests := []struct { name string ctx context.Context - req *org.AddOrganizationRequest - want *org.AddOrganizationResponse + req *v2beta_org.CreateOrganizationRequest + id string + want *v2beta_org.CreateOrganizationResponse wantErr bool }{ { name: "missing permission", - ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &org.AddOrganizationRequest{ + ctx: Instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &v2beta_org.CreateOrganizationRequest{ Name: "name", Admins: nil, }, @@ -62,7 +68,7 @@ func TestServer_AddOrganization(t *testing.T) { { name: "empty name", ctx: CTX, - req: &org.AddOrganizationRequest{ + req: &v2beta_org.CreateOrganizationRequest{ Name: "", Admins: nil, }, @@ -71,34 +77,22 @@ func TestServer_AddOrganization(t *testing.T) { { name: "invalid admin type", ctx: CTX, - req: &org.AddOrganizationRequest{ + req: &v2beta_org.CreateOrganizationRequest{ Name: gofakeit.AppName(), - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*v2beta_org.CreateOrganizationRequest_Admin{ {}, }, }, wantErr: true, }, - { - name: "no admin, custom org ID", - ctx: CTX, - req: &org.AddOrganizationRequest{ - Name: gofakeit.AppName(), - OrgId: gu.Ptr("custom-org-ID"), - }, - want: &org.AddOrganizationResponse{ - OrganizationId: "custom-org-ID", - CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{}, - }, - }, { name: "admin with init", ctx: CTX, - req: &org.AddOrganizationRequest{ + req: &v2beta_org.CreateOrganizationRequest{ Name: gofakeit.AppName(), - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*v2beta_org.CreateOrganizationRequest_Admin{ { - UserType: &org.AddOrganizationRequest_Admin_Human{ + UserType: &v2beta_org.CreateOrganizationRequest_Admin_Human{ Human: &user_v2beta.AddHumanUserRequest{ Profile: &user_v2beta.SetHumanProfile{ GivenName: "firstname", @@ -115,9 +109,9 @@ func TestServer_AddOrganization(t *testing.T) { }, }, }, - want: &org.AddOrganizationResponse{ - OrganizationId: integration.NotEmpty, - CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{ + want: &v2beta_org.CreateOrganizationResponse{ + Id: integration.NotEmpty, + CreatedAdmins: []*v2beta_org.CreatedAdmin{ { UserId: integration.NotEmpty, EmailCode: gu.Ptr(integration.NotEmpty), @@ -129,14 +123,14 @@ func TestServer_AddOrganization(t *testing.T) { { name: "existing user and new human with idp", ctx: CTX, - req: &org.AddOrganizationRequest{ + req: &v2beta_org.CreateOrganizationRequest{ Name: gofakeit.AppName(), - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*v2beta_org.CreateOrganizationRequest_Admin{ { - UserType: &org.AddOrganizationRequest_Admin_UserId{UserId: User.GetUserId()}, + UserType: &v2beta_org.CreateOrganizationRequest_Admin_UserId{UserId: User.GetUserId()}, }, { - UserType: &org.AddOrganizationRequest_Admin_Human{ + UserType: &v2beta_org.CreateOrganizationRequest_Admin_Human{ Human: &user_v2beta.AddHumanUserRequest{ Profile: &user_v2beta.SetHumanProfile{ GivenName: "firstname", @@ -160,8 +154,8 @@ func TestServer_AddOrganization(t *testing.T) { }, }, }, - want: &org.AddOrganizationResponse{ - CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{ + want: &v2beta_org.CreateOrganizationResponse{ + CreatedAdmins: []*v2beta_org.CreatedAdmin{ // a single admin is expected, because the first provided already exists { UserId: integration.NotEmpty, @@ -169,25 +163,36 @@ func TestServer_AddOrganization(t *testing.T) { }, }, }, + { + name: "create with ID", + ctx: CTX, + id: "custom_id", + req: &v2beta_org.CreateOrganizationRequest{ + Name: gofakeit.AppName(), + Id: gu.Ptr("custom_id"), + }, + want: &v2beta_org.CreateOrganizationResponse{}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Client.AddOrganization(tt.ctx, tt.req) + got, err := Client.CreateOrganization(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return } require.NoError(t, err) + if tt.id != "" { + require.Equal(t, tt.id, got.Id) + } + // check details - assert.NotZero(t, got.GetDetails().GetSequence()) - gotCD := got.GetDetails().GetChangeDate().AsTime() + gotCD := got.GetCreationDate().AsTime() now := time.Now() assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) - assert.NotEmpty(t, got.GetDetails().GetResourceOwner()) // organization id must be the same as the resourceOwner - assert.Equal(t, got.GetDetails().GetResourceOwner(), got.GetOrganizationId()) // check the admins require.Len(t, got.GetCreatedAdmins(), len(tt.want.GetCreatedAdmins())) @@ -199,7 +204,1739 @@ func TestServer_AddOrganization(t *testing.T) { } } -func assertCreatedAdmin(t *testing.T, expected, got *org.AddOrganizationResponse_CreatedAdmin) { +func TestServer_UpdateOrganization(t *testing.T) { + orgs, orgsName, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + orgName := orgsName[0] + + tests := []struct { + name string + ctx context.Context + req *v2beta_org.UpdateOrganizationRequest + want *v2beta_org.UpdateOrganizationResponse + wantErr bool + }{ + { + name: "update org with new name", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: orgId, + Name: "new org name", + }, + }, + { + name: "update org with same name", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: orgId, + Name: orgName, + }, + }, + { + name: "update org with non existent org id", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: "non existant org id", + // Name: "", + }, + wantErr: true, + }, + { + name: "update org with no id", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: "", + Name: orgName, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.UpdateOrganization(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + // check details + gotCD := got.GetChangeDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + }) + } +} + +func TestServer_ListOrganizations(t *testing.T) { + testStartTimestamp := time.Now() + ListOrgIinstance := integration.NewInstance(CTX) + listOrgIAmOwnerCtx := ListOrgIinstance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + listOrgClient := ListOrgIinstance.Client.OrgV2beta + + noOfOrgs := 3 + orgs, orgsName, err := createOrgs(listOrgIAmOwnerCtx, listOrgClient, noOfOrgs) + if err != nil { + require.NoError(t, err) + return + } + + // deactivat org[1] + _, err = listOrgClient.DeactivateOrganization(listOrgIAmOwnerCtx, &v2beta_org.DeactivateOrganizationRequest{ + Id: orgs[1].Id, + }) + require.NoError(t, err) + + tests := []struct { + name string + ctx context.Context + query []*v2beta_org.OrganizationSearchFilter + want []*v2beta_org.Organization + err error + }{ + { + name: "list organizations, without required permissions", + ctx: ListOrgIinstance.WithAuthorization(CTX, integration.UserTypeNoPermission), + err: errors.New("membership not found"), + }, + { + name: "list organizations happy path, no filter", + ctx: listOrgIAmOwnerCtx, + want: []*v2beta_org.Organization{ + { + // default org + Name: "testinstance", + }, + { + Id: orgs[0].Id, + Name: orgsName[0], + }, + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + { + Id: orgs[2].Id, + Name: orgsName[2], + }, + }, + }, + { + name: "list organizations by id happy path", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgs[1].Id, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations by state active", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_StateFilter{ + StateFilter: &v2beta_org.OrgStateFilter{ + State: v2beta_org.OrgState_ORG_STATE_ACTIVE, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + // default org + Name: "testinstance", + }, + { + Id: orgs[0].Id, + Name: orgsName[0], + }, + { + Id: orgs[2].Id, + Name: orgsName[2], + }, + }, + }, + { + name: "list organizations by state inactive", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_StateFilter{ + StateFilter: &v2beta_org.OrgStateFilter{ + State: v2beta_org.OrgState_ORG_STATE_INACTIVE, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations by id bad id", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: "bad id", + }, + }, + }, + }, + }, + { + name: "list organizations specify org name equals", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ + NameFilter: &v2beta_org.OrgNameFilter{ + Name: orgsName[1], + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify org name contains", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ + NameFilter: &v2beta_org.OrgNameFilter{ + Name: func() string { + return orgsName[1][1 : len(orgsName[1])-2] + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify org name contains IGNORE CASE", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ + NameFilter: &v2beta_org.OrgNameFilter{ + Name: func() string { + return strings.ToUpper(orgsName[1][1 : len(orgsName[1])-2]) + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify domain name equals", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &org.OrgDomainFilter{ + Domain: func() string { + listOrgRes, err := listOrgClient.ListOrganizations(listOrgIAmOwnerCtx, &v2beta_org.ListOrganizationsRequest{ + Filter: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgs[1].Id, + }, + }, + }, + }, + }) + require.NoError(t, err) + domain := listOrgRes.Organizations[0].PrimaryDomain + return domain + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify domain name contains", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &org.OrgDomainFilter{ + Domain: func() string { + domain := strings.ToLower(strings.ReplaceAll(orgsName[1][1:len(orgsName[1])-2], " ", "-")) + return domain + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify org name contains IGNORE CASE", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &org.OrgDomainFilter{ + Domain: func() string { + domain := strings.ToUpper(strings.ReplaceAll(orgsName[1][1:len(orgsName[1])-2], " ", "-")) + return domain + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := listOrgClient.ListOrganizations(tt.ctx, &v2beta_org.ListOrganizationsRequest{ + Filter: tt.query, + }) + if tt.err != nil { + require.ErrorContains(t, err, tt.err.Error()) + return + } + require.NoError(ttt, err) + + require.Equal(ttt, uint64(len(tt.want)), got.Pagination.GetTotalResult()) + + foundOrgs := 0 + for _, got := range got.Organizations { + for _, org := range tt.want { + + // created/chagned date + gotCD := got.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(ttt, gotCD, testStartTimestamp, now.Add(time.Minute)) + gotCD = got.GetChangedDate().AsTime() + assert.WithinRange(ttt, gotCD, testStartTimestamp, now.Add(time.Minute)) + + // default org + if org.Name == got.Name && got.Name == "testinstance" { + foundOrgs += 1 + continue + } + + if org.Name == got.Name && + org.Id == got.Id { + foundOrgs += 1 + } + } + } + require.Equal(ttt, len(tt.want), foundOrgs) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + }) + } +} + +func TestServer_DeleteOrganization(t *testing.T) { + tests := []struct { + name string + ctx context.Context + createOrgFunc func() string + req *v2beta_org.DeleteOrganizationRequest + want *v2beta_org.DeleteOrganizationResponse + dontCheckTime bool + err error + }{ + { + name: "delete org no permission", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + createOrgFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + return orgs[0].Id + }, + req: &v2beta_org.DeleteOrganizationRequest{}, + err: errors.New("membership not found"), + }, + { + name: "delete org happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + createOrgFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + return orgs[0].Id + }, + req: &v2beta_org.DeleteOrganizationRequest{}, + }, + { + name: "delete already deleted org", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + createOrgFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + // delete org + _, err = Client.DeleteOrganization(CTX, &v2beta_org.DeleteOrganizationRequest{Id: orgs[0].Id}) + require.NoError(t, err) + + return orgs[0].Id + }, + req: &v2beta_org.DeleteOrganizationRequest{}, + dontCheckTime: true, + }, + { + name: "delete non existent org", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.DeleteOrganizationRequest{ + Id: "non existent org id", + }, + dontCheckTime: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.createOrgFunc != nil { + tt.req.Id = tt.createOrgFunc() + } + + got, err := Client.DeleteOrganization(tt.ctx, tt.req) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + return + } + require.NoError(t, err) + + // check details + gotCD := got.GetDeletionDate().AsTime() + if !tt.dontCheckTime { + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + } + }) + } +} + +func TestServer_DeactivateReactivateNonExistentOrganization(t *testing.T) { + ctx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + // deactivate non existent organization + _, err := Client.DeactivateOrganization(ctx, &v2beta_org.DeactivateOrganizationRequest{ + Id: "non existent organization", + }) + require.Contains(t, err.Error(), "Organisation not found") + + // reactivate non existent organization + _, err = Client.ActivateOrganization(ctx, &v2beta_org.ActivateOrganizationRequest{ + Id: "non existent organization", + }) + require.Contains(t, err.Error(), "Organisation not found") +} + +func TestServer_ActivateOrganization(t *testing.T) { + tests := []struct { + name string + ctx context.Context + testFunc func() string + err error + }{ + { + name: "Activate, happy path", + ctx: CTX, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + + // 2. deactivate organization once + deactivate_res, err := Client.DeactivateOrganization(CTX, &v2beta_org.DeactivateOrganizationRequest{ + Id: orgId, + }) + require.NoError(t, err) + gotCD := deactivate_res.GetChangeDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 3. check organization state is deactivated + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + listOrgRes, err := Client.ListOrganizations(CTX, &v2beta_org.ListOrganizationsRequest{ + Filter: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgId, + }, + }, + }, + }, + }) + require.NoError(ttt, err) + require.Equal(ttt, v2beta_org.OrgState_ORG_STATE_INACTIVE, listOrgRes.Organizations[0].State) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + }, + { + name: "Activate, no permission", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + testFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + return orgId + }, + // BUG: this needs changing + err: errors.New("membership not found"), + }, + { + name: "Activate, not existing", + ctx: CTX, + testFunc: func() string { + return "non-existing-org-id" + }, + err: errors.New("Organisation not found"), + }, + { + name: "Activate, already activated", + ctx: CTX, + testFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + return orgId + }, + err: errors.New("Organisation is already active"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var orgId string + if tt.testFunc != nil { + orgId = tt.testFunc() + } + _, err := Client.ActivateOrganization(tt.ctx, &v2beta_org.ActivateOrganizationRequest{ + Id: orgId, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestServer_DeactivateOrganization(t *testing.T) { + tests := []struct { + name string + ctx context.Context + testFunc func() string + err error + }{ + { + name: "Deactivate, happy path", + ctx: CTX, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + + return orgId + }, + }, + { + name: "Deactivate, no permission", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + testFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + return orgId + }, + // BUG: this needs changing + err: errors.New("membership not found"), + }, + { + name: "Deactivate, not existing", + ctx: CTX, + testFunc: func() string { + return "non-existing-org-id" + }, + err: errors.New("Organisation not found"), + }, + { + name: "Deactivate, already deactivated", + ctx: CTX, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + + // 2. deactivate organization once + deactivate_res, err := Client.DeactivateOrganization(CTX, &v2beta_org.DeactivateOrganizationRequest{ + Id: orgId, + }) + require.NoError(t, err) + gotCD := deactivate_res.GetChangeDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 3. check organization state is deactivated + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + listOrgRes, err := Client.ListOrganizations(CTX, &v2beta_org.ListOrganizationsRequest{ + Filter: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgId, + }, + }, + }, + }, + }) + require.NoError(ttt, err) + require.Equal(ttt, v2beta_org.OrgState_ORG_STATE_INACTIVE, listOrgRes.Organizations[0].State) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + err: errors.New("Organisation is already deactivated"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var orgId string + orgId = tt.testFunc() + _, err := Client.DeactivateOrganization(tt.ctx, &v2beta_org.DeactivateOrganizationRequest{ + Id: orgId, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestServer_AddOrganizationDomain(t *testing.T) { + tests := []struct { + name string + ctx context.Context + domain string + testFunc func() string + err error + }{ + { + name: "add org domain, happy path", + domain: gofakeit.URL(), + testFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + return orgId + }, + }, + { + name: "add org domain, twice", + domain: gofakeit.URL(), + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + + domain := gofakeit.URL() + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // check domain added + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + }, + { + name: "add org domain to non existent org", + domain: gofakeit.URL(), + testFunc: func() string { + return "non-existing-org-id" + }, + // BUG: should return a error + err: nil, + }, + } + + for _, tt := range tests { + var orgId string + t.Run(tt.name, func(t *testing.T) { + orgId = tt.testFunc() + }) + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: tt.domain, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + } else { + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + } + } +} + +func TestServer_ListOrganizationDomains(t *testing.T) { + domain := gofakeit.URL() + tests := []struct { + name string + ctx context.Context + domain string + testFunc func() string + err error + }{ + { + name: "list org domain, happy path", + domain: domain, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + return orgId + }, + }, + } + + for _, tt := range tests { + var orgId string + t.Run(tt.name, func(t *testing.T) { + orgId = tt.testFunc() + }) + + var err error + var queryRes *v2beta_org.ListOrganizationDomainsResponse + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err = Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == tt.domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for adding domain") + + } +} + +func TestServer_DeleteOerganizationDomain(t *testing.T) { + domain := gofakeit.URL() + tests := []struct { + name string + ctx context.Context + domain string + testFunc func() string + err error + }{ + { + name: "delete org domain, happy path", + domain: domain, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // check domain added + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + }, + { + name: "delete org domain, twice", + domain: gofakeit.URL(), + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + + domain := gofakeit.URL() + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // check domain added + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + _, err = Client.DeleteOrganizationDomain(CTX, &v2beta_org.DeleteOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + + return orgId + }, + err: errors.New("Domain doesn't exist on organization"), + }, + { + name: "delete org domain to non existent org", + domain: gofakeit.URL(), + testFunc: func() string { + return "non-existing-org-id" + }, + // BUG: + err: errors.New("Domain doesn't exist on organization"), + }, + } + + for _, tt := range tests { + var orgId string + t.Run(tt.name, func(t *testing.T) { + orgId = tt.testFunc() + }) + + _, err := Client.DeleteOrganizationDomain(CTX, &v2beta_org.DeleteOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: tt.domain, + }) + + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + } else { + require.NoError(t, err) + } + } +} + +func TestServer_AddListDeleteOrganizationDomain(t *testing.T) { + tests := []struct { + name string + testFunc func() + }{ + { + name: "add org domain, re-add org domain", + testFunc: func() { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + // ctx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + domain := gofakeit.URL() + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 3. re-add domain + _, err = Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + // TODO remove error for adding already existing domain + // require.NoError(t, err) + require.Contains(t, err.Error(), "Errors.Already.Exists") + // check details + // gotCD = addOrgDomainRes.GetDetails().GetChangeDate().AsTime() + // now = time.Now() + // assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 4. check domain is added + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, + }, + { + name: "add org domain, delete org domain, re-delete org domain", + testFunc: func() { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + domain := gofakeit.URL() + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 2. delete organisation domain + deleteOrgDomainRes, err := Client.DeleteOrganizationDomain(CTX, &v2beta_org.DeleteOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD = deleteOrgDomainRes.GetDeletionDate().AsTime() + now = time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(t *assert.CollectT) { + // 3. check organization domain deleted + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.False(t, found, "deleted domain found") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + // 4. redelete organisation domain + _, err = Client.DeleteOrganizationDomain(CTX, &v2beta_org.DeleteOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + // TODO remove error for deleting org domain already deleted + // require.NoError(t, err) + require.Contains(t, err.Error(), "Domain doesn't exist on organization") + // check details + // gotCD = deleteOrgDomainRes.GetDetails().GetChangeDate().AsTime() + // now = time.Now() + // assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 5. check organization domain deleted + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.False(t, found, "deleted domain found") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.testFunc() + }) + } +} + +func TestServer_ValidateOrganizationDomain(t *testing.T) { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + _, err = Instance.Client.Admin.UpdateDomainPolicy(CTX, &admin.UpdateDomainPolicyRequest{ + ValidateOrgDomains: true, + }) + if err != nil && !strings.Contains(err.Error(), "Organisation is already deactivated") { + require.NoError(t, err) + } + + domain := gofakeit.URL() + _, err = Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + + tests := []struct { + name string + ctx context.Context + req *v2beta_org.GenerateOrganizationDomainValidationRequest + err error + }{ + { + name: "validate org http happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: orgId, + Domain: domain, + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + }, + }, + { + name: "validate org http non existnetn org id", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: "non existent org id", + Domain: domain, + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + }, + // BUG: this should be 'organization does not exist' + err: errors.New("Domain doesn't exist on organization"), + }, + { + name: "validate org dns happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: orgId, + Domain: domain, + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS, + }, + }, + { + name: "validate org dns non existnetn org id", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: "non existent org id", + Domain: domain, + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS, + }, + // BUG: this should be 'organization does not exist' + err: errors.New("Domain doesn't exist on organization"), + }, + { + name: "validate org non existnetn domain", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: orgId, + Domain: "non existent domain", + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + }, + err: errors.New("Domain doesn't exist on organization"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.GenerateOrganizationDomainValidation(tt.ctx, tt.req) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + return + } + require.NoError(t, err) + + require.NotEmpty(t, got.Token) + require.Contains(t, got.Url, domain) + }) + } +} + +func TestServer_SetOrganizationMetadata(t *testing.T) { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + tests := []struct { + name string + ctx context.Context + setupFunc func() + orgId string + key string + value string + err error + }{ + { + name: "set org metadata", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + orgId: orgId, + key: "key1", + value: "value1", + }, + { + name: "set org metadata on non existant org", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + orgId: "non existant orgid", + key: "key2", + value: "value2", + err: errors.New("Organisation not found"), + }, + { + name: "update org metadata", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key3", + Value: []byte("value3"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + key: "key4", + value: "value4", + }, + { + name: "update org metadata with same value", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key5", + Value: []byte("value5"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + key: "key5", + value: "value5", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setupFunc != nil { + tt.setupFunc() + } + got, err := Client.SetOrganizationMetadata(tt.ctx, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: tt.key, + Value: []byte(tt.value), + }, + }, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + return + } + require.NoError(t, err) + + // check details + gotCD := got.GetSetDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + // check metadata + listMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + foundMetadata := false + foundMetadataKeyCount := 0 + for _, res := range listMetadataRes.Metadata { + if res.Key == tt.key { + foundMetadataKeyCount += 1 + } + if res.Key == tt.key && + string(res.Value) == tt.value { + foundMetadata = true + } + } + require.True(ttt, foundMetadata, "unable to find added metadata") + require.Equal(ttt, 1, foundMetadataKeyCount, "same metadata key found multiple times") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + }) + } +} + +func TestServer_ListOrganizationMetadata(t *testing.T) { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + tests := []struct { + name string + ctx context.Context + setupFunc func() + orgId string + keyValuPars []struct { + key string + value string + } + }{ + { + name: "list org metadata happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key1", + Value: []byte("value1"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + keyValuPars: []struct{ key, value string }{ + { + key: "key1", + value: "value1", + }, + }, + }, + { + name: "list multiple org metadata happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key2", + Value: []byte("value2"), + }, + { + Key: "key3", + Value: []byte("value3"), + }, + { + Key: "key4", + Value: []byte("value4"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + keyValuPars: []struct{ key, value string }{ + { + key: "key2", + value: "value2", + }, + { + key: "key3", + value: "value3", + }, + { + key: "key4", + value: "value4", + }, + }, + }, + { + name: "list org metadata for non existent org", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + orgId: "non existent orgid", + keyValuPars: []struct{ key, value string }{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setupFunc != nil { + tt.setupFunc() + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + }) + require.NoError(t, err) + + foundMetadataCount := 0 + for _, kv := range tt.keyValuPars { + for _, res := range got.Metadata { + if res.Key == kv.key && + string(res.Value) == kv.value { + foundMetadataCount += 1 + } + } + } + require.Equal(t, len(tt.keyValuPars), foundMetadataCount) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + }) + } +} + +func TestServer_DeleteOrganizationMetadata(t *testing.T) { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + tests := []struct { + name string + ctx context.Context + setupFunc func() + orgId string + metadataToDelete []struct { + key string + value string + } + metadataToRemain []struct { + key string + value string + } + err error + }{ + { + name: "delete org metadata happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key1", + Value: []byte("value1"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + metadataToDelete: []struct{ key, value string }{ + { + key: "key1", + value: "value1", + }, + }, + }, + { + name: "delete multiple org metadata happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key2", + Value: []byte("value2"), + }, + { + Key: "key3", + Value: []byte("value3"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + metadataToDelete: []struct{ key, value string }{ + { + key: "key2", + value: "value2", + }, + { + key: "key3", + value: "value3", + }, + }, + }, + { + name: "delete some org metadata but not all", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key4", + Value: []byte("value4"), + }, + // key5 should not be deleted + { + Key: "key5", + Value: []byte("value5"), + }, + { + Key: "key6", + Value: []byte("value6"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + metadataToDelete: []struct{ key, value string }{ + { + key: "key4", + value: "value4", + }, + { + key: "key6", + value: "value6", + }, + }, + metadataToRemain: []struct{ key, value string }{ + { + key: "key5", + value: "value5", + }, + }, + }, + { + name: "delete org metadata that does not exist", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key88", + Value: []byte("value74"), + }, + { + Key: "key5888", + Value: []byte("value8885"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + // TODO: this error message needs to be either removed or changed + err: errors.New("Metadata list is empty"), + }, + { + name: "delete org metadata for org that does not exist", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key88", + Value: []byte("value74"), + }, + { + Key: "key5888", + Value: []byte("value8885"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: "non existant org id", + // TODO: this error message needs to be either removed or changed + err: errors.New("Metadata list is empty"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setupFunc != nil { + tt.setupFunc() + } + + // check metadata exists + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + listOrgMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + }) + require.NoError(ttt, err) + foundMetadataCount := 0 + for _, kv := range tt.metadataToDelete { + for _, res := range listOrgMetadataRes.Metadata { + if res.Key == kv.key && + string(res.Value) == kv.value { + foundMetadataCount += 1 + } + } + } + require.Equal(ttt, len(tt.metadataToDelete), foundMetadataCount) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + keys := make([]string, len(tt.metadataToDelete)) + for i, kvp := range tt.metadataToDelete { + keys[i] = kvp.key + } + + // run delete + _, err = Client.DeleteOrganizationMetadata(tt.ctx, &v2beta_org.DeleteOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + Keys: keys, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + return + } + require.NoError(t, err) + + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + // check metadata was definitely deleted + listOrgMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + }) + require.NoError(ttt, err) + foundMetadataCount := 0 + for _, kv := range tt.metadataToDelete { + for _, res := range listOrgMetadataRes.Metadata { + if res.Key == kv.key && + string(res.Value) == kv.value { + foundMetadataCount += 1 + } + } + } + require.Equal(ttt, foundMetadataCount, 0) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + // check metadata that should not be delted was not deleted + listOrgMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + }) + require.NoError(t, err) + foundMetadataCount := 0 + for _, kv := range tt.metadataToRemain { + for _, res := range listOrgMetadataRes.Metadata { + if res.Key == kv.key && + string(res.Value) == kv.value { + foundMetadataCount += 1 + } + } + } + require.Equal(t, len(tt.metadataToRemain), foundMetadataCount) + }) + } +} + +func createOrgs(ctx context.Context, client v2beta_org.OrganizationServiceClient, noOfOrgs int) ([]*v2beta_org.CreateOrganizationResponse, []string, error) { + var err error + orgs := make([]*v2beta_org.CreateOrganizationResponse, noOfOrgs) + orgsName := make([]string, noOfOrgs) + + for i := range noOfOrgs { + orgName := gofakeit.Name() + orgsName[i] = orgName + orgs[i], err = client.CreateOrganization(ctx, + &v2beta_org.CreateOrganizationRequest{ + Name: orgName, + }, + ) + if err != nil { + return nil, nil, err + } + } + + return orgs, orgsName, nil +} + +func assertCreatedAdmin(t *testing.T, expected, got *v2beta_org.CreatedAdmin) { if expected.GetUserId() != "" { assert.NotEmpty(t, got.GetUserId()) } else { diff --git a/internal/api/grpc/org/v2beta/org.go b/internal/api/grpc/org/v2beta/org.go index 39730f827e..66198757cb 100644 --- a/internal/api/grpc/org/v2beta/org.go +++ b/internal/api/grpc/org/v2beta/org.go @@ -2,16 +2,23 @@ package org import ( "context" + "errors" + "google.golang.org/protobuf/types/known/timestamppb" + + metadata "github.com/zitadel/zitadel/internal/api/grpc/metadata/v2beta" object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" user "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" ) -func (s *Server) AddOrganization(ctx context.Context, request *org.AddOrganizationRequest) (*org.AddOrganizationResponse, error) { - orgSetup, err := addOrganizationRequestToCommand(request) +func (s *Server) CreateOrganization(ctx context.Context, request *v2beta_org.CreateOrganizationRequest) (*v2beta_org.CreateOrganizationResponse, error) { + orgSetup, err := createOrganizationRequestToCommand(request) if err != nil { return nil, err } @@ -22,8 +29,182 @@ func (s *Server) AddOrganization(ctx context.Context, request *org.AddOrganizati return createdOrganizationToPb(createdOrg) } -func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*command.OrgSetup, error) { - admins, err := addOrganizationRequestAdminsToCommand(request.GetAdmins()) +func (s *Server) UpdateOrganization(ctx context.Context, request *v2beta_org.UpdateOrganizationRequest) (*v2beta_org.UpdateOrganizationResponse, error) { + org, err := s.command.ChangeOrg(ctx, request.Id, request.Name) + if err != nil { + return nil, err + } + + return &v2beta_org.UpdateOrganizationResponse{ + ChangeDate: timestamppb.New(org.EventDate), + }, nil +} + +func (s *Server) ListOrganizations(ctx context.Context, request *v2beta_org.ListOrganizationsRequest) (*v2beta_org.ListOrganizationsResponse, error) { + queries, err := listOrgRequestToModel(s.systemDefaults, request) + if err != nil { + return nil, err + } + orgs, err := s.query.SearchOrgs(ctx, queries, s.checkPermission) + if err != nil { + return nil, err + } + return &v2beta_org.ListOrganizationsResponse{ + Organizations: OrgViewsToPb(orgs.Orgs), + Pagination: &filter.PaginationResponse{ + TotalResult: orgs.Count, + AppliedLimit: uint64(request.GetPagination().GetLimit()), + }, + }, nil +} + +func (s *Server) DeleteOrganization(ctx context.Context, request *v2beta_org.DeleteOrganizationRequest) (*v2beta_org.DeleteOrganizationResponse, error) { + details, err := s.command.RemoveOrg(ctx, request.Id) + if err != nil { + var notFoundError *zerrors.NotFoundError + if errors.As(err, ¬FoundError) { + return &v2beta_org.DeleteOrganizationResponse{}, nil + } + return nil, err + } + return &v2beta_org.DeleteOrganizationResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }, nil +} + +func (s *Server) SetOrganizationMetadata(ctx context.Context, request *v2beta_org.SetOrganizationMetadataRequest) (*v2beta_org.SetOrganizationMetadataResponse, error) { + result, err := s.command.BulkSetOrgMetadata(ctx, request.OrganizationId, BulkSetOrgMetadataToDomain(request)...) + if err != nil { + return nil, err + } + return &org.SetOrganizationMetadataResponse{ + SetDate: timestamppb.New(result.EventDate), + }, nil +} + +func (s *Server) ListOrganizationMetadata(ctx context.Context, request *v2beta_org.ListOrganizationMetadataRequest) (*v2beta_org.ListOrganizationMetadataResponse, error) { + metadataQueries, err := ListOrgMetadataToDomain(s.systemDefaults, request) + if err != nil { + return nil, err + } + res, err := s.query.SearchOrgMetadata(ctx, true, request.OrganizationId, metadataQueries, false) + if err != nil { + return nil, err + } + return &v2beta_org.ListOrganizationMetadataResponse{ + Metadata: metadata.OrgMetadataListToPb(res.Metadata), + Pagination: &filter.PaginationResponse{ + TotalResult: res.Count, + AppliedLimit: uint64(request.GetPagination().GetLimit()), + }, + }, nil +} + +func (s *Server) DeleteOrganizationMetadata(ctx context.Context, request *v2beta_org.DeleteOrganizationMetadataRequest) (*v2beta_org.DeleteOrganizationMetadataResponse, error) { + result, err := s.command.BulkRemoveOrgMetadata(ctx, request.OrganizationId, request.Keys...) + if err != nil { + return nil, err + } + return &v2beta_org.DeleteOrganizationMetadataResponse{ + DeletionDate: timestamppb.New(result.EventDate), + }, nil +} + +func (s *Server) DeactivateOrganization(ctx context.Context, request *org.DeactivateOrganizationRequest) (*org.DeactivateOrganizationResponse, error) { + objectDetails, err := s.command.DeactivateOrg(ctx, request.Id) + if err != nil { + return nil, err + } + return &org.DeactivateOrganizationResponse{ + ChangeDate: timestamppb.New(objectDetails.EventDate), + }, nil +} + +func (s *Server) ActivateOrganization(ctx context.Context, request *org.ActivateOrganizationRequest) (*org.ActivateOrganizationResponse, error) { + objectDetails, err := s.command.ReactivateOrg(ctx, request.Id) + if err != nil { + return nil, err + } + return &org.ActivateOrganizationResponse{ + ChangeDate: timestamppb.New(objectDetails.EventDate), + }, err +} + +func (s *Server) AddOrganizationDomain(ctx context.Context, request *org.AddOrganizationDomainRequest) (*org.AddOrganizationDomainResponse, error) { + userIDs, err := s.getClaimedUserIDsOfOrgDomain(ctx, request.Domain, request.OrganizationId) + if err != nil { + return nil, err + } + details, err := s.command.AddOrgDomain(ctx, request.OrganizationId, request.Domain, userIDs) + if err != nil { + return nil, err + } + return &org.AddOrganizationDomainResponse{ + CreationDate: timestamppb.New(details.EventDate), + }, nil +} + +func (s *Server) ListOrganizationDomains(ctx context.Context, req *org.ListOrganizationDomainsRequest) (*org.ListOrganizationDomainsResponse, error) { + queries, err := ListOrgDomainsRequestToModel(s.systemDefaults, req) + if err != nil { + return nil, err + } + orgIDQuery, err := query.NewOrgDomainOrgIDSearchQuery(req.OrganizationId) + if err != nil { + return nil, err + } + queries.Queries = append(queries.Queries, orgIDQuery) + + domains, err := s.query.SearchOrgDomains(ctx, queries, false) + if err != nil { + return nil, err + } + return &org.ListOrganizationDomainsResponse{ + Domains: object.DomainsToPb(domains.Domains), + Pagination: &filter.PaginationResponse{ + TotalResult: domains.Count, + AppliedLimit: uint64(req.GetPagination().GetLimit()), + }, + }, nil +} + +func (s *Server) DeleteOrganizationDomain(ctx context.Context, req *org.DeleteOrganizationDomainRequest) (*org.DeleteOrganizationDomainResponse, error) { + details, err := s.command.RemoveOrgDomain(ctx, RemoveOrgDomainRequestToDomain(ctx, req)) + if err != nil { + return nil, err + } + return &org.DeleteOrganizationDomainResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }, err +} + +func (s *Server) GenerateOrganizationDomainValidation(ctx context.Context, req *org.GenerateOrganizationDomainValidationRequest) (*org.GenerateOrganizationDomainValidationResponse, error) { + token, url, err := s.command.GenerateOrgDomainValidation(ctx, GenerateOrgDomainValidationRequestToDomain(ctx, req)) + if err != nil { + return nil, err + } + return &org.GenerateOrganizationDomainValidationResponse{ + Token: token, + Url: url, + }, nil +} + +func (s *Server) VerifyOrganizationDomain(ctx context.Context, request *org.VerifyOrganizationDomainRequest) (*org.VerifyOrganizationDomainResponse, error) { + userIDs, err := s.getClaimedUserIDsOfOrgDomain(ctx, request.Domain, request.OrganizationId) + if err != nil { + return nil, err + } + details, err := s.command.ValidateOrgDomain(ctx, ValidateOrgDomainRequestToDomain(ctx, request), userIDs) + if err != nil { + return nil, err + } + return &org.VerifyOrganizationDomainResponse{ + ChangeDate: timestamppb.New(details.EventDate), + }, nil +} + +func createOrganizationRequestToCommand(request *v2beta_org.CreateOrganizationRequest) (*command.OrgSetup, error) { + admins, err := createOrganizationRequestAdminsToCommand(request.GetAdmins()) if err != nil { return nil, err } @@ -31,14 +212,14 @@ func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*comm Name: request.GetName(), CustomDomain: "", Admins: admins, - OrgID: request.GetOrgId(), + OrgID: request.GetId(), }, nil } -func addOrganizationRequestAdminsToCommand(requestAdmins []*org.AddOrganizationRequest_Admin) (admins []*command.OrgSetupAdmin, err error) { +func createOrganizationRequestAdminsToCommand(requestAdmins []*v2beta_org.CreateOrganizationRequest_Admin) (admins []*command.OrgSetupAdmin, err error) { admins = make([]*command.OrgSetupAdmin, len(requestAdmins)) for i, admin := range requestAdmins { - admins[i], err = addOrganizationRequestAdminToCommand(admin) + admins[i], err = createOrganizationRequestAdminToCommand(admin) if err != nil { return nil, err } @@ -46,14 +227,14 @@ func addOrganizationRequestAdminsToCommand(requestAdmins []*org.AddOrganizationR return admins, nil } -func addOrganizationRequestAdminToCommand(admin *org.AddOrganizationRequest_Admin) (*command.OrgSetupAdmin, error) { +func createOrganizationRequestAdminToCommand(admin *v2beta_org.CreateOrganizationRequest_Admin) (*command.OrgSetupAdmin, error) { switch a := admin.GetUserType().(type) { - case *org.AddOrganizationRequest_Admin_UserId: + case *v2beta_org.CreateOrganizationRequest_Admin_UserId: return &command.OrgSetupAdmin{ ID: a.UserId, Roles: admin.GetRoles(), }, nil - case *org.AddOrganizationRequest_Admin_Human: + case *v2beta_org.CreateOrganizationRequest_Admin_Human: human, err := user.AddUserRequestToAddHuman(a.Human) if err != nil { return nil, err @@ -63,22 +244,31 @@ func addOrganizationRequestAdminToCommand(admin *org.AddOrganizationRequest_Admi Roles: admin.GetRoles(), }, nil default: - return nil, zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", a) + return nil, zerrors.ThrowUnimplementedf(nil, "ORGv2-SL2r8", "userType oneOf %T in method AddOrganization not implemented", a) } } -func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *org.AddOrganizationResponse, err error) { - admins := make([]*org.AddOrganizationResponse_CreatedAdmin, len(createdOrg.CreatedAdmins)) - for i, admin := range createdOrg.CreatedAdmins { - admins[i] = &org.AddOrganizationResponse_CreatedAdmin{ - UserId: admin.ID, - EmailCode: admin.EmailCode, - PhoneCode: admin.PhoneCode, - } +func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain, orgID string) ([]string, error) { + queries := make([]query.SearchQuery, 0, 2) + loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+orgDomain, query.TextEndsWithIgnoreCase) + if err != nil { + return nil, err } - return &org.AddOrganizationResponse{ - Details: object.DomainToDetailsPb(createdOrg.ObjectDetails), - OrganizationId: createdOrg.ObjectDetails.ResourceOwner, - CreatedAdmins: admins, - }, nil + queries = append(queries, loginName) + if orgID != "" { + owner, err := query.NewUserResourceOwnerSearchQuery(orgID, query.TextNotEquals) + if err != nil { + return nil, err + } + queries = append(queries, owner) + } + users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: queries}, nil) + if err != nil { + return nil, err + } + userIDs := make([]string, len(users.Users)) + for i, user := range users.Users { + userIDs[i] = user.ID + } + return userIDs, nil } diff --git a/internal/api/grpc/org/v2beta/org_test.go b/internal/api/grpc/org/v2beta/org_test.go index 57ed05dfb2..2047f665a1 100644 --- a/internal/api/grpc/org/v2beta/org_test.go +++ b/internal/api/grpc/org/v2beta/org_test.go @@ -12,14 +12,13 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) -func Test_addOrganizationRequestToCommand(t *testing.T) { +func Test_createOrganizationRequestToCommand(t *testing.T) { type args struct { - request *org.AddOrganizationRequest + request *org.CreateOrganizationRequest } tests := []struct { name string @@ -30,21 +29,21 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { { name: "nil user", args: args{ - request: &org.AddOrganizationRequest{ + request: &org.CreateOrganizationRequest{ Name: "name", - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*org.CreateOrganizationRequest_Admin{ {}, }, }, }, - wantErr: zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", nil), + wantErr: zerrors.ThrowUnimplementedf(nil, "ORGv2-SL2r8", "userType oneOf %T in method AddOrganization not implemented", nil), }, { name: "custom org ID", args: args{ - request: &org.AddOrganizationRequest{ - Name: "custom org ID", - OrgId: gu.Ptr("org-ID"), + request: &org.CreateOrganizationRequest{ + Name: "custom org ID", + Id: gu.Ptr("org-ID"), }, }, want: &command.OrgSetup{ @@ -57,11 +56,11 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { { name: "user ID", args: args{ - request: &org.AddOrganizationRequest{ + request: &org.CreateOrganizationRequest{ Name: "name", - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*org.CreateOrganizationRequest_Admin{ { - UserType: &org.AddOrganizationRequest_Admin_UserId{ + UserType: &org.CreateOrganizationRequest_Admin_UserId{ UserId: "userID", }, Roles: nil, @@ -82,11 +81,11 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { { name: "human user", args: args{ - request: &org.AddOrganizationRequest{ + request: &org.CreateOrganizationRequest{ Name: "name", - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*org.CreateOrganizationRequest_Admin{ { - UserType: &org.AddOrganizationRequest_Admin_Human{ + UserType: &org.CreateOrganizationRequest_Admin_Human{ Human: &user.AddHumanUserRequest{ Profile: &user.SetHumanProfile{ GivenName: "firstname", @@ -124,7 +123,7 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := addOrganizationRequestToCommand(tt.args.request) + got, err := createOrganizationRequestToCommand(tt.args.request) require.ErrorIs(t, err, tt.wantErr) assert.Equal(t, tt.want, got) }) @@ -139,7 +138,7 @@ func Test_createdOrganizationToPb(t *testing.T) { tests := []struct { name string args args - want *org.AddOrganizationResponse + want *org.CreateOrganizationResponse wantErr error }{ { @@ -160,14 +159,10 @@ func Test_createdOrganizationToPb(t *testing.T) { }, }, }, - want: &org.AddOrganizationResponse{ - Details: &object.Details{ - Sequence: 1, - ChangeDate: timestamppb.New(now), - ResourceOwner: "orgID", - }, - OrganizationId: "orgID", - CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{ + want: &org.CreateOrganizationResponse{ + CreationDate: timestamppb.New(now), + Id: "orgID", + CreatedAdmins: []*org.CreatedAdmin{ { UserId: "id", EmailCode: gu.Ptr("emailCode"), diff --git a/internal/api/grpc/org/v2beta/server.go b/internal/api/grpc/org/v2beta/server.go index 89dba81702..b7e8d4994f 100644 --- a/internal/api/grpc/org/v2beta/server.go +++ b/internal/api/grpc/org/v2beta/server.go @@ -6,6 +6,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" @@ -15,6 +16,7 @@ var _ org.OrganizationServiceServer = (*Server)(nil) type Server struct { org.UnimplementedOrganizationServiceServer + systemDefaults systemdefaults.SystemDefaults command *command.Commands query *query.Queries checkPermission domain.PermissionCheck @@ -23,11 +25,13 @@ type Server struct { type Config struct{} func CreateServer( + systemDefaults systemdefaults.SystemDefaults, command *command.Commands, query *query.Queries, checkPermission domain.PermissionCheck, ) *Server { return &Server{ + systemDefaults: systemDefaults, command: command, query: query, checkPermission: checkPermission, diff --git a/internal/query/org_metadata.go b/internal/query/org_metadata.go index 84b204de2b..fe61ad51d9 100644 --- a/internal/query/org_metadata.go +++ b/internal/query/org_metadata.go @@ -194,7 +194,6 @@ func prepareOrgMetadataQuery() (sq.SelectBuilder, func(*sql.Row) (*OrgMetadata, &m.Key, &m.Value, ) - if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, zerrors.ThrowNotFound(err, "QUERY-Rph32", "Errors.Metadata.NotFound") diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 1e7f3b7407..d8c88d540b 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -307,7 +307,6 @@ service AdminService { }; } - // Deprecated: Use [ListCustomDomains](apis/resources/instance_service_v2/instance-service-list-custom-domains.api.mdx) instead to list custom domains rpc ListInstanceDomains(ListInstanceDomainsRequest) returns (ListInstanceDomainsResponse) { option (google.api.http) = { post: "/domains/_search"; @@ -320,12 +319,10 @@ service AdminService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Instance"; summary: "List Instance Domains"; - description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are the URLs where ZITADEL is running."; - deprecated: true; + description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are the URLs where ZITADEL is running." }; } - // Deprecated: Use [ListTrustedDomains](apis/resources/instance_service_v2/instance-service-list-trusted-domains.api.mdx) instead to list trusted domains rpc ListInstanceTrustedDomains(ListInstanceTrustedDomainsRequest) returns (ListInstanceTrustedDomainsResponse) { option (google.api.http) = { post: "/trusted_domains/_search"; @@ -338,12 +335,10 @@ service AdminService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Instance"; summary: "List Instance Trusted Domains"; - description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts."; - deprecated: true; + description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts." }; } - // Deprecated: Use [AddTrustedDomain](apis/resources/instance_service_v2/instance-service-add-trusted-domain.api.mdx) instead to add a trusted domain rpc AddInstanceTrustedDomain(AddInstanceTrustedDomainRequest) returns (AddInstanceTrustedDomainResponse) { option (google.api.http) = { post: "/trusted_domains"; @@ -357,12 +352,10 @@ service AdminService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Instance"; summary: "Add an Instance Trusted Domain"; - description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts."; - deprecated: true; + description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts." }; } - // Deprecated: Use [RemoveTrustedDomain](apis/resources/instance_service_v2/instance-service-remove-trusted-domain.api.mdx) instead to remove a trusted domain rpc RemoveInstanceTrustedDomain(RemoveInstanceTrustedDomainRequest) returns (RemoveInstanceTrustedDomainResponse) { option (google.api.http) = { delete: "/trusted_domains/{domain}"; @@ -375,8 +368,7 @@ service AdminService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Instance"; summary: "Remove an Instance Trusted Domain"; - description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts."; - deprecated: true; + description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts." }; } @@ -1245,6 +1237,7 @@ service AdminService { }; } + // Deprecated: use ListOrganization [apis/resources/org_service_v2beta/organization-service-list-organizations.api.mdx] API instead rpc ListOrgs(ListOrgsRequest) returns (ListOrgsResponse) { option (google.api.http) = { post: "/orgs/_search"; @@ -1264,7 +1257,8 @@ service AdminService { value: { description: "list of organizations matching the query"; }; - }; + } + deprecated: true; responses: { key: "400"; value: { @@ -1279,6 +1273,7 @@ service AdminService { }; } + // Deprecated: use CreateOrganization [apis/resources/org_service_v2beta/organization-service-create-organization.api.mdx] API instead rpc SetUpOrg(SetUpOrgRequest) returns (SetUpOrgResponse) { option (google.api.http) = { post: "/orgs/_setup"; @@ -1298,7 +1293,8 @@ service AdminService { value: { description: "org, user and user membership were created successfully"; }; - }; + } + deprecated: true; responses: { key: "400"; value: { @@ -1313,6 +1309,7 @@ service AdminService { }; } + // Deprecated: use DeleteOrganization [apis/resources/org_service_v2beta/organization-service-delete-organization.api.mdx] API instead rpc RemoveOrg(RemoveOrgRequest) returns (RemoveOrgResponse) { option (google.api.http) = { delete: "/orgs/{org_id}" @@ -1330,7 +1327,8 @@ service AdminService { value: { description: "org removed successfully"; }; - }; + } + deprecated: true; responses: { key: "400"; value: { diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 3018ebe600..34a8384d39 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -2119,6 +2119,7 @@ service ManagementService { }; } + // Deprecated: use CreateOrganization [apis/resources/org_service_v2beta/organization-service-create-organization.api.mdx] API instead rpc AddOrg(AddOrgRequest) returns (AddOrgResponse) { option (google.api.http) = { post: "/orgs" @@ -2133,6 +2134,7 @@ service ManagementService { tags: "Organizations"; summary: "Create Organization"; description: "Create a new organization. Based on the given name a domain will be generated to be able to identify users within an organization." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2144,6 +2146,7 @@ service ManagementService { }; } + // Deprecated: use UpdateOrganization [apis/resources/org_service_v2beta/organization-service-update-organization.api.mdx] API instead rpc UpdateOrg(UpdateOrgRequest) returns (UpdateOrgResponse) { option (google.api.http) = { put: "/orgs/me" @@ -2158,6 +2161,7 @@ service ManagementService { tags: "Organizations"; summary: "Update Organization"; description: "Change the name of the organization." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2169,6 +2173,7 @@ service ManagementService { }; } + // Deprecated: use DeactivateOrganization [apis/resources/org_service_v2beta/organization-service-deactivate-organization.api.mdx] API instead rpc DeactivateOrg(DeactivateOrgRequest) returns (DeactivateOrgResponse) { option (google.api.http) = { post: "/orgs/me/_deactivate" @@ -2183,6 +2188,7 @@ service ManagementService { tags: "Organizations"; summary: "Deactivate Organization"; description: "Sets the state of my organization to deactivated. Users of this organization will not be able to log in." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2194,6 +2200,7 @@ service ManagementService { }; } + // Deprecated: use ActivateOrganization [apis/resources/org_service_v2beta/organization-service-activate-organization.api.mdx] API instead rpc ReactivateOrg(ReactivateOrgRequest) returns (ReactivateOrgResponse) { option (google.api.http) = { post: "/orgs/me/_reactivate" @@ -2208,6 +2215,7 @@ service ManagementService { tags: "Organizations"; summary: "Reactivate Organization"; description: "Set the state of my organization to active. The state of the organization has to be deactivated to perform the request. Users of this organization will be able to log in again." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2219,6 +2227,7 @@ service ManagementService { }; } + // Deprecated: use DeleteOrganization [apis/resources/org_service_v2beta/organization-service-delete-organization.api.mdx] API instead rpc RemoveOrg(RemoveOrgRequest) returns (RemoveOrgResponse) { option (google.api.http) = { delete: "/orgs/me" @@ -2232,6 +2241,7 @@ service ManagementService { tags: "Organizations"; summary: "Delete Organization"; description: "Deletes my organization and all its resources (Users, Projects, Grants to and from the org). Users of this organization will not be able to log in." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2243,6 +2253,7 @@ service ManagementService { }; } + // Deprecated: use SetOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-set-organization-metadata.api.mdx] API instead rpc SetOrgMetadata(SetOrgMetadataRequest) returns (SetOrgMetadataResponse) { option (google.api.http) = { post: "/metadata/{key}" @@ -2258,6 +2269,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Set Organization Metadata"; description: "This endpoint either adds or updates a metadata value for the requested key. Make sure the value is base64 encoded." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2269,6 +2281,7 @@ service ManagementService { }; } + // Deprecated: use SetOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-set-organization-metadata.api.mdx] API instead rpc BulkSetOrgMetadata(BulkSetOrgMetadataRequest) returns (BulkSetOrgMetadataResponse) { option (google.api.http) = { post: "/metadata/_bulk" @@ -2284,6 +2297,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Bulk Set Organization Metadata"; description: "This endpoint sets a list of metadata to the organization. Make sure the values are base64 encoded." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2295,6 +2309,7 @@ service ManagementService { }; } + // Deprecated: use ListOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-list-organization-metadata.api.mdx] API instead rpc ListOrgMetadata(ListOrgMetadataRequest) returns (ListOrgMetadataResponse) { option (google.api.http) = { post: "/metadata/_search" @@ -2310,6 +2325,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Search Organization Metadata"; description: "Get the metadata of an organization filtered by your query." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2321,6 +2337,7 @@ service ManagementService { }; } + // Deprecated: use ListOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-list-organization-metadata.api.mdx] API instead rpc GetOrgMetadata(GetOrgMetadataRequest) returns (GetOrgMetadataResponse) { option (google.api.http) = { get: "/metadata/{key}" @@ -2335,6 +2352,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Get Organization Metadata By Key"; description: "Get a metadata object from an organization by a specific key." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2346,6 +2364,7 @@ service ManagementService { }; } + // Deprecated: use DeleteOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-delete-organization-metadata.api.mdx] API instead rpc RemoveOrgMetadata(RemoveOrgMetadataRequest) returns (RemoveOrgMetadataResponse) { option (google.api.http) = { delete: "/metadata/{key}" @@ -2360,6 +2379,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Delete Organization Metadata By Key"; description: "Remove a metadata object from an organization with a specific key." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2371,6 +2391,7 @@ service ManagementService { }; } + // Deprecated: use DeleteOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-delete-organization-metadata.api.mdx] API instead rpc BulkRemoveOrgMetadata(BulkRemoveOrgMetadataRequest) returns (BulkRemoveOrgMetadataResponse) { option (google.api.http) = { delete: "/metadata/_bulk" @@ -2384,6 +2405,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Organizations"; tags: "Organization Metadata"; + deprecated: true summary: "Bulk Delete Metadata"; description: "Remove a list of metadata objects from an organization with a list of keys." parameters: { @@ -2397,31 +2419,7 @@ service ManagementService { }; } - rpc ListOrgDomains(ListOrgDomainsRequest) returns (ListOrgDomainsResponse) { - option (google.api.http) = { - post: "/orgs/me/domains/_search" - body: "*" - }; - - option (zitadel.v1.auth_option) = { - permission: "org.read" - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Organizations"; - summary: "Search Domains"; - description: "Returns the list of registered domains of an organization. The domains are used to identify to which organization a user belongs." - parameters: { - headers: { - name: "x-zitadel-orgid"; - description: "The default is always the organization of the requesting user. If you like to get/set a result of another organization include the header. Make sure the user has permission to access the requested data."; - type: STRING, - required: false; - }; - }; - }; - } - + // Deprecated: use AddOrganizationDomain [apis/resources/org_service_v2beta/organization-service-add-organization-domain.api.mdx] API instead rpc AddOrgDomain(AddOrgDomainRequest) returns (AddOrgDomainResponse) { option (google.api.http) = { post: "/orgs/me/domains" @@ -2436,6 +2434,7 @@ service ManagementService { tags: "Organizations"; summary: "Add Domain"; description: "Add a new domain to an organization. The domains are used to identify to which organization a user belongs." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2447,6 +2446,34 @@ service ManagementService { }; } + // Deprecated: use ListOrganizationDomains [apis/resources/org_service_v2beta/organization-service-list-organization-domains.api.mdx] API instead + rpc ListOrgDomains(ListOrgDomainsRequest) returns (ListOrgDomainsResponse) { + option (google.api.http) = { + post: "/orgs/me/domains/_search" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.read" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Organizations"; + summary: "Search Domains"; + description: "Returns the list of registered domains of an organization. The domains are used to identify to which organization a user belongs." + deprecated: true + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to get/set a result of another organization include the header. Make sure the user has permission to access the requested data."; + type: STRING, + required: false; + }; + }; + }; + } + + // Deprecated: use DeleteOrganizationDomain [apis/resources/org_service_v2beta/organization-service-delete-organization-domain.api.mdx] API instead rpc RemoveOrgDomain(RemoveOrgDomainRequest) returns (RemoveOrgDomainResponse) { option (google.api.http) = { delete: "/orgs/me/domains/{domain}" @@ -2460,6 +2487,7 @@ service ManagementService { tags: "Organizations"; summary: "Remove Domain"; description: "Delete a new domain from an organization. The domains are used to identify to which organization a user belongs. If the uses use the domain for login, this will not be possible afterwards. They have to use another domain instead." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2471,6 +2499,7 @@ service ManagementService { }; } + // Deprecated: use GenerateOrganizationDomainValidation [apis/resources/org_service_v2beta/organization-service-generate-organization-domain-validation.api.mdx] API instead rpc GenerateOrgDomainValidation(GenerateOrgDomainValidationRequest) returns (GenerateOrgDomainValidationResponse) { option (google.api.http) = { post: "/orgs/me/domains/{domain}/validation/_generate" @@ -2485,6 +2514,7 @@ service ManagementService { tags: "Organizations"; summary: "Generate Domain Verification"; description: "Generate a new file to be able to verify your domain with DNS or HTTP challenge." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2496,6 +2526,7 @@ service ManagementService { }; } + // Deprecated: use VerifyOrganizationDomain [apis/resources/org_service_v2beta/organization-service-verify-organization-domain.api.mdx] API instead rpc ValidateOrgDomain(ValidateOrgDomainRequest) returns (ValidateOrgDomainResponse) { option (google.api.http) = { post: "/orgs/me/domains/{domain}/validation/_validate" @@ -2510,6 +2541,7 @@ service ManagementService { tags: "Organizations"; summary: "Verify Domain"; description: "Make sure you have added the required verification to your domain, depending on the method you have chosen (HTTP or DNS challenge). ZITADEL will check it and set the domain as verified if it was successful. A verify domain has to be unique." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2678,11 +2710,6 @@ service ManagementService { }; } - // Get Project By ID - // - // Deprecated: [Get Project](apis/resources/project_service_v2/project-service-get-project.api.mdx) to get project by ID. - // - // Returns a project owned by the organization (no granted projects). A Project is a vessel for different applications sharing the same role context. rpc GetProjectByID(GetProjectByIDRequest) returns (GetProjectByIDResponse) { option (google.api.http) = { get: "/projects/{id}" @@ -2695,7 +2722,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Get Project By ID"; + description: "Returns a project owned by the organization (no granted projects). A Project is a vessel for different applications sharing the same role context." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2707,11 +2735,6 @@ service ManagementService { }; } - // Get Granted Project By ID - // - // Deprecated: [List Projects](apis/resources/project_service_v2/project-service-list-projects.api.mdx) to get granted projects. - // - // Returns a project owned by another organization and granted to my organization. A Project is a vessel for different applications sharing the same role context. rpc GetGrantedProjectByID(GetGrantedProjectByIDRequest) returns (GetGrantedProjectByIDResponse) { option (google.api.http) = { get: "/granted_projects/{project_id}/grants/{grant_id}" @@ -2724,7 +2747,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Get Granted Project By ID"; + description: "Returns a project owned by another organization and granted to my organization. A Project is a vessel for different applications sharing the same role context." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2736,11 +2760,6 @@ service ManagementService { }; } - // List Projects - // - // Deprecated: [List Projects](apis/resources/project_service_v2/project-service-list-projects.api.mdx) to list all projects and granted projects. - // - // Lists projects my organization is the owner of (no granted projects). A Project is a vessel for different applications sharing the same role context. rpc ListProjects(ListProjectsRequest) returns (ListProjectsResponse) { option (google.api.http) = { post: "/projects/_search" @@ -2753,7 +2772,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Search Project"; + description: "Lists projects my organization is the owner of (no granted projects). A Project is a vessel for different applications sharing the same role context." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2765,11 +2785,6 @@ service ManagementService { }; } - // List Granted Projects - // - // Deprecated: [List Projects](apis/resources/project_service_v2/project-service-list-projects.api.mdx) to list all projects and granted projects. - // - // Lists projects my organization got granted from another organization. A Project is a vessel for different applications sharing the same role context. rpc ListGrantedProjects(ListGrantedProjectsRequest) returns (ListGrantedProjectsResponse) { option (google.api.http) = { post: "/granted_projects/_search" @@ -2782,7 +2797,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Search Granted Project"; + description: "Lists projects my organization got granted from another organization. A Project is a vessel for different applications sharing the same role context." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2844,11 +2860,6 @@ service ManagementService { }; } - // Create Project - // - // Deprecated: [Create Project](apis/resources/project_service_v2/project-service-create-project.api.mdx) to create a project. - // - // Create a new project. A Project is a vessel for different applications sharing the same role context. rpc AddProject(AddProjectRequest) returns (AddProjectResponse) { option (google.api.http) = { post: "/projects" @@ -2861,7 +2872,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Create Project"; + description: "Create a new project. A Project is a vessel for different applications sharing the same role context." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2873,11 +2885,6 @@ service ManagementService { }; } - // Update Project - // - // Deprecated: [Update Project](apis/resources/project_service_v2/project-service-update-project.api.mdx) to update a project. - // - // Update a project and its settings. A Project is a vessel for different applications sharing the same role context. rpc UpdateProject(UpdateProjectRequest) returns (UpdateProjectResponse) { option (google.api.http) = { put: "/projects/{id}" @@ -2891,7 +2898,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Update Project"; + description: "Update a project and its settings. A Project is a vessel for different applications sharing the same role context." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2903,11 +2911,6 @@ service ManagementService { }; } - // Deactivate Project - // - // Deprecated: [Deactivate Project](apis/resources/project_service_v2/project-service-deactivate-project.api.mdx) to deactivate a project. - // - // Set the state of a project to deactivated. Request returns an error if the project is already deactivated. rpc DeactivateProject(DeactivateProjectRequest) returns (DeactivateProjectResponse) { option (google.api.http) = { post: "/projects/{id}/_deactivate" @@ -2921,7 +2924,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Deactivate Project"; + description: "Set the state of a project to deactivated. Request returns an error if the project is already deactivated." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2933,11 +2937,6 @@ service ManagementService { }; } - // Activate Project - // - // Deprecated: [Activate Project](apis/resources/project_service_v2/project-service-activate-project.api.mdx) to activate a project. - // - // Set the state of a project to active. Request returns an error if the project is not deactivated. rpc ReactivateProject(ReactivateProjectRequest) returns (ReactivateProjectResponse) { option (google.api.http) = { post: "/projects/{id}/_reactivate" @@ -2951,7 +2950,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Reactivate Project"; + description: "Set the state of a project to active. Request returns an error if the project is not deactivated." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2963,11 +2963,6 @@ service ManagementService { }; } - // Remove Project - // - // Deprecated: [Delete Project](apis/resources/project_service_v2/project-service-delete-project.api.mdx) to remove a project. - // - // Project and all its sub-resources like project grants, applications, roles and user grants will be removed. rpc RemoveProject(RemoveProjectRequest) returns (RemoveProjectResponse) { option (google.api.http) = { delete: "/projects/{id}" @@ -2980,7 +2975,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Remove Project"; + description: "Project and all its sub-resources like project grants, applications, roles and user grants will be removed." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2992,11 +2988,6 @@ service ManagementService { }; } - // Search Project Roles - // - // Deprecated: [List Project Roles](apis/resources/project_service_v2/project-service-list-project-roles.api.mdx) to get project roles. - // - // Returns all roles of a project matching the search query. rpc ListProjectRoles(ListProjectRolesRequest) returns (ListProjectRolesResponse) { option (google.api.http) = { post: "/projects/{project_id}/roles/_search" @@ -3010,7 +3001,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Roles"; - deprecated: true; + summary: "Search Project Roles"; + description: "Returns all roles of a project matching the search query." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3022,11 +3014,6 @@ service ManagementService { }; } - // Add Project Role - // - // Deprecated: [Add Project Role](apis/resources/project_service_v2/project-service-add-project-role.api.mdx) to add a project role. - // - // Add a new project role to a project. The key must be unique within the project.\n\nDeprecated: please use user service v2 AddProjectRole. rpc AddProjectRole(AddProjectRoleRequest) returns (AddProjectRoleResponse) { option (google.api.http) = { post: "/projects/{project_id}/roles" @@ -3040,7 +3027,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Roles"; - deprecated: true; + summary: "Add Project Role"; + description: "Add a new project role to a project. The key must be unique within the project." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3052,11 +3040,6 @@ service ManagementService { }; } - // Bulk add Project Role - // - // Deprecated: [Add Project Role](apis/resources/project_service_v2/project-service-add-project-role.api.mdx) to add a project role. - // - // Add a list of roles to a project. The keys must be unique within the project. rpc BulkAddProjectRoles(BulkAddProjectRolesRequest) returns (BulkAddProjectRolesResponse) { option (google.api.http) = { post: "/projects/{project_id}/roles/_bulk" @@ -3070,7 +3053,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Roles"; - deprecated: true; + summary: "Bulk Add Project Role"; + description: "Add a list of roles to a project. The keys must be unique within the project." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3082,11 +3066,6 @@ service ManagementService { }; } - // Update Project Role - // - // Deprecated: [Update Project Role](apis/resources/project_service_v2/project-service-update-project-role.api.mdx) to update a project role. - // - // Change a project role. The key is not editable. If a key should change, remove the role and create a new one. rpc UpdateProjectRole(UpdateProjectRoleRequest) returns (UpdateProjectRoleResponse) { option (google.api.http) = { put: "/projects/{project_id}/roles/{role_key}" @@ -3100,7 +3079,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Roles"; - deprecated: true; + summary: "Change Project Role"; + description: "Change a project role. The key is not editable. If a key should change, remove the role and create a new one." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3112,11 +3092,6 @@ service ManagementService { }; } - // Remove Project Role - // - // Deprecated: [Delete Project Role](apis/resources/project_service_v2/project-service-update-project-role.api.mdx) to remove a project role. - // - // Removes the role from the project and on every resource it has a dependency. This includes project grants and user grants. rpc RemoveProjectRole(RemoveProjectRoleRequest) returns (RemoveProjectRoleResponse) { option (google.api.http) = { delete: "/projects/{project_id}/roles/{role_key}" @@ -3129,7 +3104,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Roles"; - deprecated: true; + summary: "Remove Project Role"; + description: "Removes the role from the project and on every resource it has a dependency. This includes project grants and user grants." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3793,11 +3769,6 @@ service ManagementService { }; } - // Get Project Grant By ID - // - // Deprecated: [List Project Grants](apis/resources/project_service_v2/project-service-list-project-grants.api.mdx) to get a project grant. - // - // Returns a project grant. A project grant is when the organization grants its project to another organization. rpc GetProjectGrantByID(GetProjectGrantByIDRequest) returns (GetProjectGrantByIDResponse) { option (google.api.http) = { get: "/projects/{project_id}/grants/{grant_id}" @@ -3809,7 +3780,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Project Grant By ID"; + description: "Returns a project grant. A project grant is when the organization grants its project to another organization." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3821,11 +3793,6 @@ service ManagementService { }; } - // List Project Grants - // - // Deprecated: [List Project Grants](apis/resources/project_service_v2/project-service-list-project-grants.api.mdx) to list project grants. - // - // Returns a list of project grants for a specific project. A project grant is when the organization grants its project to another organization. rpc ListProjectGrants(ListProjectGrantsRequest) returns (ListProjectGrantsResponse) { option (google.api.http) = { post: "/projects/{project_id}/grants/_search" @@ -3839,7 +3806,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Search Project Grants from Project"; + description: "Returns a list of project grants for a specific project. A project grant is when the organization grants its project to another organization." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3851,11 +3819,6 @@ service ManagementService { }; } - // Search Project Grants - // - // Deprecated: [List Project Grants](apis/resources/project_service_v2/project-service-list-project-grants.api.mdx) to list project grants. - // - // Returns a list of project grants. A project grant is when the organization grants its project to another organization. rpc ListAllProjectGrants(ListAllProjectGrantsRequest) returns (ListAllProjectGrantsResponse) { option (google.api.http) = { post: "/projectgrants/_search" @@ -3868,7 +3831,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Search Project Grants"; + description: "Returns a list of project grants. A project grant is when the organization grants its project to another organization." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3880,11 +3844,6 @@ service ManagementService { }; } - // Add Project Grant - // - // Deprecated: [Create Project Grant](apis/resources/project_service_v2/project-service-create-project-grant.api.mdx) to add a project grant. - // - // Grant a project to another organization. The project grant will allow the granted organization to access the project and manage the authorizations for its users. Project Grant will be listed in the granted project of the granted organization. rpc AddProjectGrant(AddProjectGrantRequest) returns (AddProjectGrantResponse) { option (google.api.http) = { post: "/projects/{project_id}/grants" @@ -3897,7 +3856,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Add Project Grant"; + description: "Grant a project to another organization. The project grant will allow the granted organization to access the project and manage the authorizations for its users. Project Grant will be listed in the granted project of the granted organization" parameters: { headers: { name: "x-zitadel-orgid"; @@ -3909,11 +3869,6 @@ service ManagementService { }; } - // Update Project Grant - // - // Deprecated: [Update Project Grant](apis/resources/project_service_v2/project-service-update-project-grant.api.mdx) to update a project grant. - // - // Change the roles of the project that is granted to another organization. The project grant will allow the granted organization to access the project and manage the authorizations for its users. Project Grant will be listed in the granted project of the granted organization. rpc UpdateProjectGrant(UpdateProjectGrantRequest) returns (UpdateProjectGrantResponse) { option (google.api.http) = { put: "/projects/{project_id}/grants/{grant_id}" @@ -3926,7 +3881,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Change Project Grant"; + description: "Change the roles of the project that is granted to another organization. The project grant will allow the granted organization to access the project and manage the authorizations for its users. Project Grant will be listed in the granted project of the granted organization" parameters: { headers: { name: "x-zitadel-orgid"; @@ -3938,11 +3894,6 @@ service ManagementService { }; } - // Deactivate Project Grant - // - // Deprecated: [Deactivate Project Grant](apis/resources/project_service_v2/project-service-deactivate-project-grant.api.mdx) to deactivate a project grant. - // - // Set the state of the project grant to deactivated. The grant has to be active to be able to deactivate. rpc DeactivateProjectGrant(DeactivateProjectGrantRequest) returns (DeactivateProjectGrantResponse) { option (google.api.http) = { post: "/projects/{project_id}/grants/{grant_id}/_deactivate" @@ -3955,7 +3906,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Deactivate Project Grant"; + description: "Set the state of the project grant to deactivated. The grant has to be active to be able to deactivate." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3967,11 +3919,6 @@ service ManagementService { }; } - // Reactivate Project Grant - // - // Deprecated: [Activate Project Grant](apis/resources/project_service_v2/project-service-activate-project-grant.api.mdx) to activate a project grant. - // - // Set the state of the project grant to active. The grant has to be deactivated to be able to reactivate. rpc ReactivateProjectGrant(ReactivateProjectGrantRequest) returns (ReactivateProjectGrantResponse) { option (google.api.http) = { post: "/projects/{project_id}/grants/{grant_id}/_reactivate" @@ -3984,7 +3931,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Reactivate Project Grant"; + description: "Set the state of the project grant to active. The grant has to be deactivated to be able to reactivate." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3996,11 +3944,6 @@ service ManagementService { }; } - // Remove Project Grant - // - // Deprecated: [Delete Project Grant](apis/resources/project_service_v2/project-service-delete-project-grant.api.mdx) to remove a project grant. - // - // Remove a project grant. All user grants for this project grant will also be removed. A user will not have access to the project afterward (if permissions are checked). rpc RemoveProjectGrant(RemoveProjectGrantRequest) returns (RemoveProjectGrantResponse) { option (google.api.http) = { delete: "/projects/{project_id}/grants/{grant_id}" @@ -4012,7 +3955,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Remove Project Grant"; + description: "Remove a project grant. All user grants for this project grant will also be removed. A user will not have access to the project afterward (if permissions are checked)." parameters: { headers: { name: "x-zitadel-orgid"; diff --git a/proto/zitadel/metadata/v2beta/metadata.proto b/proto/zitadel/metadata/v2beta/metadata.proto new file mode 100644 index 0000000000..87fcc51869 --- /dev/null +++ b/proto/zitadel/metadata/v2beta/metadata.proto @@ -0,0 +1,57 @@ +syntax = "proto3"; + +import "zitadel/object/v2beta/object.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +package zitadel.metadata.v2beta; + +option go_package ="github.com/zitadel/zitadel/pkg/grpc/metadata/v2beta"; + +message Metadata { + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + google.protobuf.Timestamp change_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + string key = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "metadata key", + example: "\"key1\""; + } + ]; + bytes value = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "metadata value is base64 encoded, make sure to decode to get the value", + example: "\"VGhpcyBpcyBteSBmaXJzdCB2YWx1ZQ==\""; + } + ]; +} + +message MetadataQuery { + oneof query { + option (validate.required) = true; + MetadataKeyQuery key_query = 1; + } +} + +message MetadataKeyQuery { + string key = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"key\"" + } + ]; + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines which text equality method is used"; + } + ]; +} diff --git a/proto/zitadel/org/v2beta/org.proto b/proto/zitadel/org/v2beta/org.proto new file mode 100644 index 0000000000..08cf47e820 --- /dev/null +++ b/proto/zitadel/org/v2beta/org.proto @@ -0,0 +1,169 @@ +syntax = "proto3"; + +package zitadel.org.v2beta; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/org/v2beta;org"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/object/v2beta/object.proto"; +import "google/protobuf/timestamp.proto"; + +message Organization { + // Unique identifier of the organization. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + + // The timestamp of the organization was created. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + // The timestamp of the verification of the organization domain. + google.protobuf.Timestamp changed_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + + // Current state of the organization, for example active, inactive and deleted. + OrgState state = 4; + + // Name of the organization. + string name = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\""; + } + ]; + // Primary domain used in the organization. + string primary_domain = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.cloud\""; + } + ]; +} + +enum OrgState { + ORG_STATE_UNSPECIFIED = 0; + ORG_STATE_ACTIVE = 1; + ORG_STATE_INACTIVE = 2; + ORG_STATE_REMOVED = 3; +} + +enum OrgFieldName { + ORG_FIELD_NAME_UNSPECIFIED = 0; + ORG_FIELD_NAME_NAME = 1; + ORG_FIELD_NAME_CREATION_DATE = 2; +} + +message OrganizationSearchFilter{ + oneof filter { + option (validate.required) = true; + + OrgNameFilter name_filter = 1; + OrgDomainFilter domain_filter = 2; + OrgStateFilter state_filter = 3; + OrgIDFilter id_filter = 4; + } +} +message OrgNameFilter { + // Organization name. + string name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\""; + } + ]; + // Defines which text equality method is used. + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message OrgDomainFilter { + // The domain. + string domain = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.cloud\""; + } + ]; + // Defines which text equality method is used. + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message OrgStateFilter { + // Current state of the organization. + OrgState state = 1 [ + (validate.rules).enum.defined_only = true + ]; +} + +message OrgIDFilter { + // The Organization id. + string id = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; +} + +// from proto/zitadel/org.proto +message DomainSearchFilter { + oneof filter { + option (validate.required) = true; + DomainNameFilter domain_name_filter = 1; + } +} + +// from proto/zitadel/org.proto +message DomainNameFilter { + // The domain. + string name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.cloud\""; + } + ]; + // Defines which text equality method is used. + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +// from proto/zitadel/org.proto +message Domain { + // The Organization id. + string organization_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + // The domain name. + string domain_name = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.com\""; + } + ]; + // Defines if the domain is verified. + bool is_verified = 3; + // Defines if the domain is the primary domain. + bool is_primary = 4; + // Defines the protocol the domain was validated with. + DomainValidationType validation_type = 5; +} + +// from proto/zitadel/org.proto +enum DomainValidationType { + DOMAIN_VALIDATION_TYPE_UNSPECIFIED = 0; + DOMAIN_VALIDATION_TYPE_HTTP = 1; + DOMAIN_VALIDATION_TYPE_DNS = 2; +} diff --git a/proto/zitadel/org/v2beta/org_service.proto b/proto/zitadel/org/v2beta/org_service.proto index e303b676d7..28c823a89b 100644 --- a/proto/zitadel/org/v2beta/org_service.proto +++ b/proto/zitadel/org/v2beta/org_service.proto @@ -6,24 +6,22 @@ package zitadel.org.v2beta; import "zitadel/object/v2beta/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; import "zitadel/user/v2beta/auth.proto"; -import "zitadel/user/v2beta/email.proto"; -import "zitadel/user/v2beta/phone.proto"; -import "zitadel/user/v2beta/idp.proto"; -import "zitadel/user/v2beta/password.proto"; -import "zitadel/user/v2beta/user.proto"; +import "zitadel/org/v2beta/org.proto"; +import "zitadel/metadata/v2beta/metadata.proto"; import "zitadel/user/v2beta/user_service.proto"; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; -import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/filter/v2beta/filter.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/org/v2beta;org"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { - title: "User Service"; + title: "Organization Service (Beta)"; version: "2.0-beta"; description: "This API is intended to manage organizations in a ZITADEL instance. This project is in beta state. It can AND will continue breaking until the services provide the same functionality as the current login."; contact:{ @@ -111,8 +109,13 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { service OrganizationService { - // Create a new organization and grant the user(s) permission to manage it - rpc AddOrganization(AddOrganizationRequest) returns (AddOrganizationResponse) { + // Create Organization + // + // Create a new organization with an administrative user. If no specific roles are sent for the users, they will be granted the role ORG_OWNER. + // + // Required permission: + // - `org.create` + rpc CreateOrganization(CreateOrganizationRequest) returns (CreateOrganizationResponse) { option (google.api.http) = { post: "/v2beta/organizations" body: "*" @@ -122,34 +125,411 @@ service OrganizationService { auth_option: { permission: "org.create" } - http_response: { - success_code: 201 - } }; - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create an Organization"; - description: "Create a new organization with an administrative user. If no specific roles are sent for the users, they will be granted the role ORG_OWNER." + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { responses: { - key: "200" + key: "200"; value: { - description: "OK"; + description: "Organization created successfully"; + }; + }; + responses: { + key: "409" + value: { + description: "The organization to create already exists."; } }; }; } + + // Update Organization + // + // Change the name of the organization. + // + // Required permission: + // - `org.write` + rpc UpdateOrganization(UpdateOrganizationRequest) returns (UpdateOrganizationResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Organization created successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "Organisation's not found"; + } + }; + responses: { + key: "409" + value: { + description: "Organisation's name already taken"; + } + }; + }; + + } + + // List Organizations + // + // Returns a list of organizations that match the requesting filters. All filters are applied with an AND condition. + // + // Required permission: + // - `iam.read` + rpc ListOrganizations(ListOrganizationsRequest) returns (ListOrganizationsResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/search"; + body: "*"; + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.read"; + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + } + + // Delete Organization + // + // Deletes the organization and all its resources (Users, Projects, Grants to and from the org). Users of this organization will not be able to log in. + // + // Required permission: + // - `org.delete` + rpc DeleteOrganization(DeleteOrganizationRequest) returns (DeleteOrganizationResponse) { + option (google.api.http) = { + delete: "/v2beta/organizations/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.delete"; + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Organization created successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "Organisation's not found"; + } + }; + }; + } + + // Set Organization Metadata + // + // Adds or updates a metadata value for the requested key. Make sure the value is base64 encoded. + // + // Required permission: + // - `org.write` + rpc SetOrganizationMetadata(SetOrganizationMetadataRequest) returns (SetOrganizationMetadataResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/metadata" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + responses: { + // TODO This needs to chagne to 404 + key: "400" + value: { + description: "Organisation's not found"; + } + }; + }; + } + + // List Organization Metadata + // + // List metadata of an organization filtered by query. + // + // Required permission: + // - `org.read` + rpc ListOrganizationMetadata(ListOrganizationMetadataRequest) returns (ListOrganizationMetadataResponse ) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/metadata/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { + permission: "org.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Delete Organization Metadata + // + // Delete metadata objects from an organization with a specific key. + // + // Required permission: + // - `org.write` + rpc DeleteOrganizationMetadata(DeleteOrganizationMetadataRequest) returns (DeleteOrganizationMetadataResponse) { + option (google.api.http) = { + delete: "/v2beta/organizations/{organization_id}/metadata" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Add Organization Domain + // + // Add a new domain to an organization. The domains are used to identify to which organization a user belongs. + // + // Required permission: + // - `org.write` + rpc AddOrganizationDomain(AddOrganizationDomainRequest) returns (AddOrganizationDomainResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/domains" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + responses: { + key: "409" + value: { + description: "Domain already exists"; + } + }; + }; + + } + + // List Organization Domains + // + // Returns the list of registered domains of an organization. The domains are used to identify to which organization a user belongs. + // + // Required permission: + // - `org.read` + rpc ListOrganizationDomains(ListOrganizationDomainsRequest) returns (ListOrganizationDomainsResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/domains/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Delete Organization Domain + // + // Delete a new domain from an organization. The domains are used to identify to which organization a user belongs. If the uses use the domain for login, this will not be possible afterwards. They have to use another domain instead. + // + // Required permission: + // - `org.write` + rpc DeleteOrganizationDomain(DeleteOrganizationDomainRequest) returns (DeleteOrganizationDomainResponse) { + option (google.api.http) = { + delete: "/v2beta/organizations/{organization_id}/domains" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Generate Organization Domain Validation + // + // Generate a new file to be able to verify your domain with DNS or HTTP challenge. + // + // Required permission: + // - `org.write` + rpc GenerateOrganizationDomainValidation(GenerateOrganizationDomainValidationRequest) returns (GenerateOrganizationDomainValidationResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/domains/validation/generate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + responses: { + key: "404" + value: { + description: "Domain doesn't exist on organization"; + } + }; + }; + } + + // Verify Organization Domain + // + // Make sure you have added the required verification to your domain, depending on the method you have chosen (HTTP or DNS challenge). ZITADEL will check it and set the domain as verified if it was successful. A verify domain has to be unique. + // + // Required permission: + // - `org.write` + rpc VerifyOrganizationDomain(VerifyOrganizationDomainRequest) returns (VerifyOrganizationDomainResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/domains/validation/verify" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + } + + // Deactivate Organization + // + // Sets the state of my organization to deactivated. Users of this organization will not be able to log in. + // + // Required permission: + // - `org.write` + rpc DeactivateOrganization(DeactivateOrganizationRequest) returns (DeactivateOrganizationResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{id}/deactivate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Activate Organization + // + // Set the state of my organization to active. The state of the organization has to be deactivated to perform the request. Users of this organization will be able to log in again. + // + // Required permission: + // - `org.write` + rpc ActivateOrganization(ActivateOrganizationRequest) returns (ActivateOrganizationResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{id}/activate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + } -message AddOrganizationRequest{ +message CreateOrganizationRequest{ + // The Admin for the newly created Organization. message Admin { oneof user_type{ string user_id = 1; zitadel.user.v2beta.AddHumanUserRequest human = 2; } - // specify Org Member Roles for the provided user (default is ORG_OWNER if roles are empty) + // specify Organization Member Roles for the provided user (default is ORG_OWNER if roles are empty) repeated string roles = 3; } + // name of the Organization to be created. string name = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, @@ -159,24 +539,403 @@ message AddOrganizationRequest{ example: "\"ZITADEL\""; } ]; - repeated Admin admins = 2; - // optionally set your own id unique for the organization. - optional string org_id = 3 [ - (validate.rules).string = {min_len: 1, max_len: 200}, + // Optionally set your own id unique for the organization. + optional string id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200 }, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; max_length: 200; - example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + example: "\"69629012906488334\""; + } + ]; + // Additional Admins for the Organization. + repeated Admin admins = 3; +} + +message CreatedAdmin { + string user_id = 1; + optional string email_code = 2; + optional string phone_code = 3; +} + +message CreateOrganizationResponse{ + // The timestamp of the organization was created. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + // Organization ID of the newly created organization. + string id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + + // The admins created for the Organization + repeated CreatedAdmin created_admins = 3; +} + +message UpdateOrganizationRequest { + // Organization Id for the Organization to be updated + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + + // New Name for the Organization to be updated + string name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Customer 1\""; } ]; } -message AddOrganizationResponse{ - message CreatedAdmin { - string user_id = 1; - optional string email_code = 2; - optional string phone_code = 3; - } - zitadel.object.v2beta.Details details = 1; - string organization_id = 2; - repeated CreatedAdmin created_admins = 3; +message UpdateOrganizationResponse { + // The timestamp of the update to the organization. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListOrganizationsRequest { + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 1; + // the field the result is sorted + zitadel.org.v2beta.OrgFieldName sorting_column = 2; + // Define the criteria to query for. + // repeated ProjectRoleQuery filters = 4; + repeated zitadel.org.v2beta.OrganizationSearchFilter filter = 3; +} + +message ListOrganizationsResponse { + // Pagination of the Organizations results + zitadel.filter.v2beta.PaginationResponse pagination = 1; + // The Organizations requested + repeated zitadel.org.v2beta.Organization organizations = 2; +} + +message DeleteOrganizationRequest { + + // Organization Id for the Organization to be deleted + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + min_length: 1; + max_length: 200; + } + ]; +} + +message DeleteOrganizationResponse { + // The timestamp of the deletion of the organization. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeactivateOrganizationRequest { + // Organization Id for the Organization to be deactivated + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + min_length: 1; + max_length: 200; + } + ]; +} + +message DeactivateOrganizationResponse { + // The timestamp of the deactivation of the organization. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ActivateOrganizationRequest { + // Organization Id for the Organization to be activated + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + min_length: 1; + max_length: 200; + } + ]; +} + +message ActivateOrganizationResponse { + // The timestamp of the activation of the organization. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message AddOrganizationDomainRequest { + // Organization Id for the Organization for which the domain is to be added to. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + // The domain you want to add to the organization. + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"testdomain.com\""; + } + ]; +} + +message AddOrganizationDomainResponse { + // The timestamp of the organization was created. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message ListOrganizationDomainsRequest { + // Organization Id for the Organization which domains are to be listed. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 2; + // Define the criteria to query for. + repeated DomainSearchFilter filters = 3; +} + +message ListOrganizationDomainsResponse { + // Pagination of the Organizations domain results. + zitadel.filter.v2beta.PaginationResponse pagination = 1; + // The domains requested. + repeated Domain domains = 2; +} + +message DeleteOrganizationDomainRequest { + // Organization Id for the Organization which domain is to be deleted. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"testdomain.com\""; + } + ]; +} + +message DeleteOrganizationDomainResponse { + // The timestamp of the deletion of the organization domain. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message GenerateOrganizationDomainValidationRequest { + // Organization Id for the Organization which doman to be validated. + string organization_id = 1 [ + + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + // The domain which to be deleted. + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"testdomain.com\""; + } + ]; + DomainValidationType type = 3 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; +} + +message GenerateOrganizationDomainValidationResponse { + // The token verify domain. + string token = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ofSBHsSAVHAoTIE4Iv2gwhaYhTjcY5QX\""; + } + ]; + // URL used to verify the domain. + string url = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://testdomain.com/.well-known/zitadel-challenge/ofSBHsSAVHAoTIE4Iv2gwhaYhTjcY5QX\""; + } + ]; +} + +message VerifyOrganizationDomainRequest { + // Organization Id for the Organization doman to be verified. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + // Organization Id for the Organization doman to be verified. + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"testdomain.com\""; + } + ]; +} + +message VerifyOrganizationDomainResponse { + // The timestamp of the verification of the organization domain. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message Metadata { + // Key in the metadata key/value pair. + string key = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + // Value in the metadata key/value pair. + bytes value = 2 [(validate.rules).bytes = {min_len: 1, max_len: 500000}]; +} +message SetOrganizationMetadataRequest{ + // Organization Id for the Organization doman to be verified. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + // Metadata to set. + repeated Metadata metadata = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + title: "Medata (Key/Value)" + description: "The values have to be base64 encoded."; + example: "[{\"key\": \"test1\", \"value\": \"VGhpcyBpcyBteSBmaXJzdCB2YWx1ZQ==\"}, {\"key\": \"test2\", \"value\": \"VGhpcyBpcyBteSBzZWNvbmQgdmFsdWU=\"}]" + } + ]; +} + +message SetOrganizationMetadataResponse{ + // The timestamp of the update of the organization metadata. + google.protobuf.Timestamp set_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListOrganizationMetadataRequest { + // Organization ID of Orgalization which metadata is to be listed. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 2; + // Define the criteria to query for. + repeated zitadel.metadata.v2beta.MetadataQuery filter = 3; +} + +message ListOrganizationMetadataResponse { + // Pagination of the Organizations metadata results. + zitadel.filter.v2beta.PaginationResponse pagination = 1; + // The Organization metadata requested. + repeated zitadel.metadata.v2beta.Metadata metadata = 2; +} + +message DeleteOrganizationMetadataRequest { + // Organization ID of Orgalization which metadata is to be deleted is stored on. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + // The keys for the Organization metadata to be deleted. + repeated string keys = 2 [(validate.rules).repeated.items.string = {min_len: 1, max_len: 200}]; +} + +message DeleteOrganizationMetadataResponse{ + // The timestamp of the deletiion of the organization metadata. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; } From 15902f5bc79047dd1bf6083e60047a4acccad353 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Tue, 3 Jun 2025 14:48:15 +0200 Subject: [PATCH 15/35] fix(cache): prevent org cache overwrite by other instances (#10012) # Which Problems Are Solved A customer reported that randomly certain login flows, such as automatic redirect to the only configured IdP would not work. During the investigation it was discovered that they used that same primary domain on two different instances. As they used the domain for preselecting the organization, one would always overwrite the other in the cache. Since The organization and especially it's policies could not be retrieved on the other instance, it would fallback to the default organization settings, where the external login and the corresponding IdP were not configured. # How the Problems Are Solved Include the instance id in the cache key for organizations to prevent overwrites. # Additional Changes None # Additional Context - found because of a support request - requires backport to 2.70.x, 2.71.x and 3.x --- internal/query/org.go | 22 +++++++++++++++++----- internal/query/org_test.go | 4 ++++ internal/v2/readmodel/org.go | 2 ++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/internal/query/org.go b/internal/query/org.go index dfe90ad9f8..e2d9e205da 100644 --- a/internal/query/org.go +++ b/internal/query/org.go @@ -12,6 +12,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" domain_pkg "github.com/zitadel/zitadel/internal/domain" + es "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/feature" "github.com/zitadel/zitadel/internal/query/projection" @@ -77,6 +78,8 @@ type Org struct { ResourceOwner string State domain_pkg.OrgState Sequence uint64 + // instanceID is used to create a unique cache key for the org + instanceID string Name string Domain string @@ -122,7 +125,7 @@ func (q *Queries) OrgByID(ctx context.Context, shouldTriggerBulk bool, id string ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if org, ok := q.caches.org.Get(ctx, orgIndexByID, id); ok { + if org, ok := q.caches.org.Get(ctx, orgIndexByID, orgCacheKey(authz.GetInstance(ctx).InstanceID(), id)); ok { return org, nil } defer func() { @@ -159,6 +162,7 @@ func (q *Queries) OrgByID(ctx context.Context, shouldTriggerBulk bool, id string ResourceOwner: foundOrg.Owner, State: domain_pkg.OrgState(foundOrg.State.State), Sequence: uint64(foundOrg.Sequence), + instanceID: foundOrg.InstanceID, Name: foundOrg.Name, Domain: foundOrg.PrimaryDomain.Domain, }, nil @@ -195,7 +199,7 @@ func (q *Queries) OrgByPrimaryDomain(ctx context.Context, domain string) (org *O ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - org, ok := q.caches.org.Get(ctx, orgIndexByPrimaryDomain, domain) + org, ok := q.caches.org.Get(ctx, orgIndexByPrimaryDomain, orgCacheKey(authz.GetInstance(ctx).InstanceID(), domain)) if ok { return org, nil } @@ -430,6 +434,7 @@ func prepareOrgQuery() (sq.SelectBuilder, func(*sql.Row) (*Org, error)) { OrgColumnResourceOwner.identifier(), OrgColumnState.identifier(), OrgColumnSequence.identifier(), + OrgColumnInstanceID.identifier(), OrgColumnName.identifier(), OrgColumnDomain.identifier(), ). @@ -444,6 +449,7 @@ func prepareOrgQuery() (sq.SelectBuilder, func(*sql.Row) (*Org, error)) { &o.ResourceOwner, &o.State, &o.Sequence, + &o.instanceID, &o.Name, &o.Domain, ) @@ -521,15 +527,21 @@ const ( func (o *Org) Keys(index orgIndex) []string { switch index { case orgIndexByID: - return []string{o.ID} + return []string{orgCacheKey(o.instanceID, o.ID)} case orgIndexByPrimaryDomain: - return []string{o.Domain} + return []string{orgCacheKey(o.instanceID, o.Domain)} case orgIndexUnspecified: } return nil } +func orgCacheKey(instanceID, key string) string { + return instanceID + "-" + key +} + func (c *Caches) registerOrgInvalidation() { - invalidate := cacheInvalidationFunc(c.org, orgIndexByID, getAggregateID) + invalidate := cacheInvalidationFunc(c.org, orgIndexByID, func(aggregate *es.Aggregate) string { + return orgCacheKey(aggregate.InstanceID, aggregate.ID) + }) projection.OrgProjection.RegisterCacheInvalidation(invalidate) } diff --git a/internal/query/org_test.go b/internal/query/org_test.go index d704d2901a..635594e7fd 100644 --- a/internal/query/org_test.go +++ b/internal/query/org_test.go @@ -50,6 +50,7 @@ var ( ` projections.orgs1.resource_owner,` + ` projections.orgs1.org_state,` + ` projections.orgs1.sequence,` + + ` projections.orgs1.instance_id,` + ` projections.orgs1.name,` + ` projections.orgs1.primary_domain` + ` FROM projections.orgs1` @@ -60,6 +61,7 @@ var ( "resource_owner", "org_state", "sequence", + "instance_id", "name", "primary_domain", } @@ -242,6 +244,7 @@ func Test_OrgPrepares(t *testing.T) { "ro", domain.OrgStateActive, uint64(20211108), + "instance-id", "org-name", "zitadel.ch", }, @@ -254,6 +257,7 @@ func Test_OrgPrepares(t *testing.T) { ResourceOwner: "ro", State: domain.OrgStateActive, Sequence: 20211108, + instanceID: "instance-id", Name: "org-name", Domain: "zitadel.ch", }, diff --git a/internal/v2/readmodel/org.go b/internal/v2/readmodel/org.go index 94bcb21537..ce61ef69b0 100644 --- a/internal/v2/readmodel/org.go +++ b/internal/v2/readmodel/org.go @@ -18,6 +18,7 @@ type Org struct { CreationDate time.Time ChangeDate time.Time Owner string + InstanceID string } func NewOrg(id string) *Org { @@ -60,6 +61,7 @@ func (rm *Org) Reduce(events ...*eventstore.StorageEvent) error { } rm.Sequence = event.Sequence rm.ChangeDate = event.CreatedAt + rm.InstanceID = event.Aggregate.Instance } if err := rm.State.Reduce(events...); err != nil { return err From b8ff83454e8cf08f8969db94266c44a0ee158b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabienne=20B=C3=BChler?= Date: Tue, 3 Jun 2025 15:44:04 +0200 Subject: [PATCH 16/35] docs: product roadmap and zitadel versions (#9838) # Which Problems Are Solved The current public roadmap can be hard to understand for customers and it doesn't show the timelines for the different versions. which results in a lot of requests. It only outlines what is already fixed on the timeline, but doesn't give any possibilities to outline future topics / features, which not yet have a timeline # How the Problems Are Solved A new roadmap page is added - Outline for each version when it will have which state - Outline different zitadel versions with its features, deprecations, breaking changes, etc. - Show future topics, which are not yet on the roadmap --- docs/docs/apis/introduction.mdx | 2 +- docs/docs/concepts/architecture/software.md | 2 +- docs/docs/guides/integrate/actions/usage.md | 2 +- .../guides/integrate/login-ui/device-auth.mdx | 4 +- .../integrate/login-ui/oidc-standard.mdx | 2 +- .../integrate/login-ui/saml-standard.mdx | 2 +- .../guides/integrate/login/login-users.mdx | 4 +- .../guides/integrate/login/oidc/webkeys.md | 2 +- docs/docs/guides/manage/customize/branding.md | 2 +- docs/docs/product/_beta-ga.mdx | 1 + docs/docs/product/_breaking-changes.mdx | 1 + docs/docs/product/_deprecated.mdx | 1 + docs/docs/product/_new-feature.mdx | 1 + docs/docs/product/_sdk_v3.mdx | 32 + docs/docs/product/release-cycle.mdx | 63 ++ docs/docs/product/roadmap.mdx | 714 ++++++++++++++++++ docs/sidebars.js | 15 +- docs/src/css/custom.css | 4 + docs/static/img/product/release-cycle.png | Bin 0 -> 102080 bytes 19 files changed, 842 insertions(+), 12 deletions(-) create mode 100644 docs/docs/product/_beta-ga.mdx create mode 100644 docs/docs/product/_breaking-changes.mdx create mode 100644 docs/docs/product/_deprecated.mdx create mode 100644 docs/docs/product/_new-feature.mdx create mode 100644 docs/docs/product/_sdk_v3.mdx create mode 100644 docs/docs/product/release-cycle.mdx create mode 100644 docs/docs/product/roadmap.mdx create mode 100644 docs/static/img/product/release-cycle.png diff --git a/docs/docs/apis/introduction.mdx b/docs/docs/apis/introduction.mdx index e05a7e84b3..905adfc0fb 100644 --- a/docs/docs/apis/introduction.mdx +++ b/docs/docs/apis/introduction.mdx @@ -45,7 +45,7 @@ The [OIDC Playground](https://zitadel.com/playgrounds/oidc) is for testing OpenI ### Custom -ZITADEL allows to authenticate users by creating a session with the [Session API](/docs/apis/resources/session_service_v2), get OIDC authentication request details with the [OIDC service API](/docs/apis/resources/oidc_service) or get SAML request details with the [SAML service API](/docs/apis/resources/saml_service). +ZITADEL allows to authenticate users by creating a session with the [Session API](/docs/apis/resources/session_service_v2), get OIDC authentication request details with the [OIDC service API](/docs/apis/resources/oidc_service_v2) or get SAML request details with the [SAML service API](/docs/apis/resources/saml_service_v2). User authorizations can be [retrieved as roles from our APIs](/docs/guides/integrate/retrieve-user-roles). Refer to our guide to learn how to [build your own login UI](/docs/guides/integrate/login-ui) diff --git a/docs/docs/concepts/architecture/software.md b/docs/docs/concepts/architecture/software.md index dc6f2b56c7..01bacfe12d 100644 --- a/docs/docs/concepts/architecture/software.md +++ b/docs/docs/concepts/architecture/software.md @@ -147,5 +147,5 @@ Zitadel currently supports PostgreSQL. Make sure to read our [Production Guide](/docs/self-hosting/manage/production#prefer-postgresql) before you decide on using one of them. :::info -Zitadel v2 supported CockroachDB and PostgreSQL. Zitadel v3 only supports PostgreSQL. Please refer to [the mirror guide](cli/mirror) to migrate to PostgreSQL. +Zitadel v2 supported CockroachDB and PostgreSQL. Zitadel v3 only supports PostgreSQL. Please refer to [the mirror guide](/docs/self-hosting/manage/cli/mirror) to migrate to PostgreSQL. ::: \ No newline at end of file diff --git a/docs/docs/guides/integrate/actions/usage.md b/docs/docs/guides/integrate/actions/usage.md index ba512ae549..e21fb4935d 100644 --- a/docs/docs/guides/integrate/actions/usage.md +++ b/docs/docs/guides/integrate/actions/usage.md @@ -371,7 +371,7 @@ The API documentation to create a target can be found [here](/apis/resources/act To ensure the integrity of request content, each call includes a 'ZITADEL-Signature' in the headers. This header contains an HMAC value computed from the request content and a timestamp, which can be used to time out requests. The logic for this process is provided in 'pkg/actions/signing.go'. The goal is to verify that the HMAC value in the header matches the HMAC value computed by the Target, ensuring that the sent and received requests are identical. Each Target resource now contains also a Signing Key, which gets generated and returned when a Target is [created](/apis/resources/action_service_v2/action-service-create-target), -and can also be newly generated when a Target is [patched](/apis/resources/action_service_v2/action-service-patch-target). +and can also be newly generated when a Target is [patched](/apis/resources/action_service_v2/action-service-update-target). For an example on how to check the signature, [refer to the example](/guides/integrate/actions/testing-request-signature). diff --git a/docs/docs/guides/integrate/login-ui/device-auth.mdx b/docs/docs/guides/integrate/login-ui/device-auth.mdx index f60fad1310..32984a17ff 100644 --- a/docs/docs/guides/integrate/login-ui/device-auth.mdx +++ b/docs/docs/guides/integrate/login-ui/device-auth.mdx @@ -102,7 +102,7 @@ Present the user with the information of the device authorization request and al ### Perform Login After you have initialized the OIDC flow you can implement the login. -Implement all the steps you like the user the go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service/session-service-set-session) the user-session. +Implement all the steps you like the user the go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service_v2/session-service-set-session) the user-session. Read the following resources for more information about the different checks: - [Username and Password](./username-password) @@ -117,7 +117,7 @@ On the create and update user session request you will always get a session toke The latest session token has to be sent to the following request: -Read more about the [Authorize or Deny Device Authorization Request Documentation](/docs/apis/resources/oidc_service_v2/oidc-service-authorize-device-authorization) +Read more about the [Authorize or Deny Device Authorization Request Documentation](/docs/apis/resources/oidc_service_v2/oidc-service-authorize-or-deny-device-authorization) Make sure that the authorization header is from an account which is permitted to finalize the Auth Request through the `IAM_LOGIN_CLIENT` role. ```bash diff --git a/docs/docs/guides/integrate/login-ui/oidc-standard.mdx b/docs/docs/guides/integrate/login-ui/oidc-standard.mdx index c96338fbf0..92068e5116 100644 --- a/docs/docs/guides/integrate/login-ui/oidc-standard.mdx +++ b/docs/docs/guides/integrate/login-ui/oidc-standard.mdx @@ -80,7 +80,7 @@ Response Example: ### Perform Login After you have initialized the OIDC flow you can implement the login. -Implement all the steps you like the user the go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service/session-service-set-session) the user-session. +Implement all the steps you like the user the go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service_v2/session-service-set-session) the user-session. Read the following resources for more information about the different checks: - [Username and Password](./username-password) diff --git a/docs/docs/guides/integrate/login-ui/saml-standard.mdx b/docs/docs/guides/integrate/login-ui/saml-standard.mdx index 8114350d5d..5196f6c81a 100644 --- a/docs/docs/guides/integrate/login-ui/saml-standard.mdx +++ b/docs/docs/guides/integrate/login-ui/saml-standard.mdx @@ -77,7 +77,7 @@ Response Example: ### Perform Login After you have initialized the SAML flow you can implement the login. -Implement all the steps you like the user to go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service/session-service-set-session) the user-session. +Implement all the steps you like the user to go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service_v2/session-service-set-session) the user-session. Read the following resources for more information about the different checks: - [Username and Password](./username-password) diff --git a/docs/docs/guides/integrate/login/login-users.mdx b/docs/docs/guides/integrate/login/login-users.mdx index de1dacfe24..13439ac1db 100644 --- a/docs/docs/guides/integrate/login/login-users.mdx +++ b/docs/docs/guides/integrate/login/login-users.mdx @@ -25,7 +25,7 @@ The identity provider is not part of the original application, but a standalone The user will authenticate using their credentials. After successful authentication, the user will be redirected back to the original application. -If you want to read more about authenticating with OIDC, head over to our comprehensive [OpenID Connect Guide](/docs/integrate/login/oidc). +If you want to read more about authenticating with OIDC, head over to our comprehensive [OpenID Connect Guide](/docs/guides/integrate/login/oidc). ### Authenticate users with SAML @@ -54,7 +54,7 @@ Note that SAML might not be suitable for mobile applications. In case you want to integrate a mobile application, use OpenID Connect or our Session API. There are more [differences between SAML and OIDC](https://zitadel.com/blog/saml-vs-oidc) that you might want to consider. -If you want to read more about authenticating with SAML, head over to our comprehensive [SAML Guide](/docs/integrate/login/saml). +If you want to read more about authenticating with SAML, head over to our comprehensive [SAML Guide](/docs/guides/integrate/login/saml). ## ZITADEL's Session API diff --git a/docs/docs/guides/integrate/login/oidc/webkeys.md b/docs/docs/guides/integrate/login/oidc/webkeys.md index a66cae61a9..62f62a90e0 100644 --- a/docs/docs/guides/integrate/login/oidc/webkeys.md +++ b/docs/docs/guides/integrate/login/oidc/webkeys.md @@ -85,7 +85,7 @@ The same counts for [zitadel/oidc](https://github.com/zitadel/oidc) Go library. ## Web Key management -ZITADEL provides a resource based [web keys API](/docs/apis/resources/webkey_service_v3). +ZITADEL provides a resource based [web keys API](/docs/apis/resources/webkey_service_v2). The API allows the creation, activation, deletion and listing of web keys. All public keys that are stored for an instance are served on the [JWKS endpoint](#json-web-key-set). Applications need public keys for token verification and not all applications are capable of on-demand diff --git a/docs/docs/guides/manage/customize/branding.md b/docs/docs/guides/manage/customize/branding.md index 14c18705f6..c5ec0a8838 100644 --- a/docs/docs/guides/manage/customize/branding.md +++ b/docs/docs/guides/manage/customize/branding.md @@ -46,7 +46,7 @@ If you like to trigger your settings for your applications you have different po Send a [reserved scope](/apis/openidoauth/scopes) with your [authorization request](../../integrate/login/oidc/login-users#auth-request) to trigger your organization. The primary domain scope will restrict the login to your organization, so only users of your own organization will be able to login. -You can use our [OpenID Authentication Request Playground](/oidc-playground) to learn more about how to trigger an [organization's policies and branding](/oidc-playground#organization-policies-and-branding). +You can use our [OpenID Authentication Request Playground](https://zitadel.com/playgrounds/oidc) to learn more about how to trigger an [organization's policies and branding](https://zitadel.com/playgrounds/oidc#organization-policies-and-branding). ### 2. Setting on your Project diff --git a/docs/docs/product/_beta-ga.mdx b/docs/docs/product/_beta-ga.mdx new file mode 100644 index 0000000000..229f94cdf5 --- /dev/null +++ b/docs/docs/product/_beta-ga.mdx @@ -0,0 +1 @@ +This describes the progression of features from a limited, pre-release testing phase (Beta) to their official, stable, and publicly available version (General Availability), ready for widespread use, with the specific transitions listed below. \ No newline at end of file diff --git a/docs/docs/product/_breaking-changes.mdx b/docs/docs/product/_breaking-changes.mdx new file mode 100644 index 0000000000..dd903a0f2e --- /dev/null +++ b/docs/docs/product/_breaking-changes.mdx @@ -0,0 +1 @@ +These are modifications to existing functionalities that may require users to alter their current implementation or usage to ensure continued compatibility; see the list below for specifics. \ No newline at end of file diff --git a/docs/docs/product/_deprecated.mdx b/docs/docs/product/_deprecated.mdx new file mode 100644 index 0000000000..e5848d68f0 --- /dev/null +++ b/docs/docs/product/_deprecated.mdx @@ -0,0 +1 @@ +This announces that specific existing features are being phased out and are scheduled for future removal, often because they have become outdated or are being replaced by an improved alternative; please see the deprecated items listed below. \ No newline at end of file diff --git a/docs/docs/product/_new-feature.mdx b/docs/docs/product/_new-feature.mdx new file mode 100644 index 0000000000..1877a1d690 --- /dev/null +++ b/docs/docs/product/_new-feature.mdx @@ -0,0 +1 @@ +These introduce brand-new functionalities or capabilities, expanding the product's offerings and value to users, as detailed below. \ No newline at end of file diff --git a/docs/docs/product/_sdk_v3.mdx b/docs/docs/product/_sdk_v3.mdx new file mode 100644 index 0000000000..76040640a2 --- /dev/null +++ b/docs/docs/product/_sdk_v3.mdx @@ -0,0 +1,32 @@ +import NewFeature from './_new-feature.mdx'; + +An initial version of our Software Development Kit (SDK) will be published. +To better align our versioning with the [ZITADEL core](#zitadel-core), the SDK will be released as version 3.x. +This strategic versioning will ensure a more consistent and intuitive development experience across our entire ecosystem. + +
+ New Features + + + +
+ Machine User Authentication Methods + + This feature introduces robust and standardized authentication methods for your machine users, enabling secure automated access to your resources. + + Choose from the following authentication methods: + - **Private Key JWT Authentication**: Enhance security by using asymmetric cryptography. A client with a registered public key can generate and sign a JSON Web Token (JWT) with its private key to authenticate. + - **Client Credentials Grant**: A simple and direct method for machine-to-machine authentication where the client confidentially provides its credentials to the authorization server in exchange for an access token. + - **Personal Access Tokens (PATs)**: Ideal for individual developers or specific scripts, PATs offer a convenient way to create long-lived, revocable tokens with specific scopes, acting as a substitute for a password. + +
+ +
+ Zitadel APIs Wrapper + + This SDK provides a convenient client for interacting with the ZITADEL APIs, simplifying how you manage resources within your instance. + + Currently, the client is tailored for machine-to-machine communication, enabling machine users to authenticate and manage ZITADEL resources programmatically. + Please note that this initial version is focused on API calls for automated tasks and does not yet include support for human user authentication flows like OAuth or OIDC. +
+
diff --git a/docs/docs/product/release-cycle.mdx b/docs/docs/product/release-cycle.mdx new file mode 100644 index 0000000000..f38c81a36d --- /dev/null +++ b/docs/docs/product/release-cycle.mdx @@ -0,0 +1,63 @@ +--- +title: Release Cycle +sidebar_label: Release Cycle +--- + +We release a new major version of our software every three months. This predictable schedule allows us to introduce significant features and enhancements in a structured way. + +This cadence provides enough time for thorough development and rigorous testing. Before each stable release, we engage with our community and customers to test and stabilize the new version. This ensures high quality and reliability. For our customers, this approach creates a clear and manageable upgrade path. + +While major changes are reserved for these three-month releases, we address urgent needs by backporting smaller updates, such as critical bug and security fixes, to earlier versions. This allows us to provide essential updates without altering the predictable rhythm of our major release cycle. + + +![Release Cycle](/img/product/release-cycle.png) + +## Preparation + +The first quarter of our cycle is for Preparation and Planning, where we create the blueprint for the upcoming major release. +During this time, we define the core architecture, map out the implementation strategy, and finalize the design for the new features. + + +## Implementation + +The second month is the Implementation and Development Phase, where our engineers build the features defined in the planning stage. + +During this period, we focus on writing the code for the new enhancements. +We also integrate accepted contributions from our community and create the necessary documentation alongside the development work. +This phase concludes when the new version is feature-complete and ready to enter the testing phase. + +## Release Candidate (RC) + +The first month of the third quarter is for the Release Candidate (RC) and Stabilization Phase. +At the beginning of this month, we publish a Release Candidate version. +This is a feature-complete version that we believe is ready for public release, made available to our customers and community for widespread testing. + +This phase is critical for ensuring the quality of the final release. We have two main objectives: +- **Community Feedback and Bug Fixing**: This is when we rely on your feedback. By testing the RC in your own environments, you help us find and fix bugs and other issues we may have missed. Your active participation is crucial for stabilizing the new version. +- **Enhanced Internal Testing**: While the community provides feedback, our internal teams conduct enhanced quality assurance. This includes in-depth feature validation, rigorous testing of upgrade paths from previous versions, and comprehensive performance and benchmark testing. + +The goal of this phase is to use both community feedback and internal testing to ensure the new release is robust, bug-free, and performs well, so our customers can upgrade with confidence. + +## General Availability (GA) / Stable + +Following the month-long Release Candidate and Stabilization phase, we publish the official General Availability (GA) / Stable Release. +This is the final, production-ready version of our software that has been thoroughly tested by both our internal teams and the community. + +This release is available to everyone, and we recommend that customers begin reviewing the official upgrade path for their production environments. +The deployment of this new major version to our cloud services also happens at this time. + +**Ongoing Maintenance: Minor and Patch Releases** +Once a major version becomes stable, we provide ongoing support through back-porting. This means we carefully select and apply critical updates from our main development track to the stable release, ensuring it remains secure and reliable. These updates are delivered in two ways: + +- Minor Releases: These include simple features and enhancements from the next release cycle that are safe to add, requiring no major refactoring or large database migrations. +- Patch Releases: These are focused exclusively on high-priority bug and security fixes to address critical issues promptly. + +This process ensures that you can benefit from the stability of a major release while still receiving important updates and fixes in a timely manner. + +## Deprecated + +Each major version is actively supported for a full release cycle after its launch. This means that approximately six months after its initial stable release, a version enters its deprecation period. + +Once a version is deprecated, we strongly encourage all self-hosted customers to upgrade to a newer version as soon as possible to continue receiving the latest features, improvements, and bug fixes. + +For our enterprise customers, we may offer extended support by providing critical security fixes for a deprecated version beyond the standard six-month lifecycle. This extended support is evaluated on a case-by-case basis to ensure a secure and manageable transition for large-scale deployments. \ No newline at end of file diff --git a/docs/docs/product/roadmap.mdx b/docs/docs/product/roadmap.mdx new file mode 100644 index 0000000000..b83efedb10 --- /dev/null +++ b/docs/docs/product/roadmap.mdx @@ -0,0 +1,714 @@ +--- +title: Zitadel Release Versions and Roadmap +sidebar_label: Release Versions and Roadmap +--- + + +import NewFeature from './_new-feature.mdx'; +import BreakingChanges from './_breaking-changes.mdx'; +import Deprecated from './_deprecated.mdx'; +import BetaToGA from './_beta-ga.mdx'; +import SDKv3 from './_sdk_v3.mdx'; + + +## Timeline and Overview + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
20252026
Q1Q2Q3Q4Q1Q2Q3Q4
JanFebMarAprMayJunJulAugSepOctNovDecJanFebMarAprMayJunJulAugSepOctNovDec
Zitadel Versions
[v2.x](/docs/product/roadmap#v2x)GA / Stable Deprecated
[v3.x](/docs/product/roadmap#v3x)ImplementationRCGA / Stable Deprecated
[v4.x](/docs/product/roadmap#v4x)ImplementationRCGA / Stable Deprecated
[v5.x](/docs/product/roadmap#v5x)ImplementationRCGA / Stable Deprecated
+ +For more detailed description about the different stages and the release cycle check out the following Page: [Release Cycle](/docs/product/release-cycle) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
25-Q125-Q225-Q325-Q4
Zitadel Core
+ [v2.x](/docs/product/roadmap#v2x) + + [v3.x](/docs/product/roadmap#v3x) +
    +
  • + Actions V2 +
  • +
  • + Removed CockroachDB Support +
  • +
  • + License Change +
  • +
  • + Login v2 +
      +
    • + Initial Release +
    • +
    • + All standard authentication methods +
    • +
    • + OIDC & SAML +
    • +
    +
  • +
+
+ [v4.x](/docs/product/roadmap#v4x) +
    +
  • Resource API
  • +
  • + Login v2 as default + +
      +
    • + Device Authorization Flow +
    • +
    • + LDAP IDP +
    • +
    • + JWT IDP +
    • +
    • + Custom Login UI Texts +
    • +
    +
  • +
+ +
+ [v5.x](/docs/product/roadmap#v5x) +
    +
  • Analytics
  • +
  • User Groups
  • +
  • User Uniqueness on Instance Level
  • +
  • Remove Required Fields from User
  • +
+
Zitadel SDKs
+ + + + +
    +
  • + [Initial Version of PHP SDK](/docs/product/roadmap#v3x-1) +
  • +
  • + [Initial Version of Java SDK](/docs/product/roadmap#v3x-2) +
  • +
  • + [Initial Version of Ruby SDK](/docs/product/roadmap#v3x-3) +
  • +
  • + [Initial Version of Python SDK](/docs/product/roadmap#v3x-4) +
  • +
+
+
+ +## Zitadel Core + +Check out all [Zitadel Release Versions](https://github.com/zitadel/zitadel/releases) + +### v2.x + +**Current State**: General Availability / Stable + +**Release**: [v2.x](https://github.com/zitadel/zitadel/releases?q=v2.&expanded=true) + +In Zitadel versions 2.x and earlier, new releases were deployed with a minimum frequency of every two weeks. +This practice resulted in a significant number of individual versions. +To review the features and bug fixes for these releases, please consult the linked release information provided above. + +### v3.x + +ZITADEL v3 is here, bringing key changes designed to empower your identity management experience. +This release transitions our licensing to AGPLv3, reinforcing our commitment to open and collaborative development. +We've streamlined our database support by removing CockroachDB. +Excitingly, v3 introduces the foundational elements for Actions V2, opening up a world of possibilities for tailoring and extending ZITADEL to perfectly fit your unique use cases. + +**Current State**: General Availability / Stable + +**Release**: [v3.x](https://github.com/zitadel/zitadel/releases?q=v3.&expanded=true) + +**Blog**: [Zitadel v3: AGPL License, Streamlined Releases, and Platform Updates](https://zitadel.com/blog/zitadel-v3-announcement) + + +
+ New Features + + + +
+ Actions V2 + + Zitadel Actions V2 empowers you to customize Zitadel's workflows by executing your own logic at specific points. You define external Endpoints containing your code and configure Targets and Executions within Zitadel to trigger them based on various conditions and events. + + Why we built it: To provide greater flexibility and control, allowing you to implement custom business rules, automate tasks, enrich user data, control access, and integrate with other systems seamlessly. Actions V2 enables you to tailor Zitadel precisely to your unique needs. + + Read more in our [documentation](https://zitadel.com/docs/concepts/features/actions_v2) +
+ +
+ License Change Apache 2.0 to AGPL3 + + Zitadel is switching to the AGPL 3.0 license to ensure the project's sustainability and encourage community contributions from commercial users, while keeping the core free and open source. + + Read more about our [decision](https://zitadel.com/blog/apache-to-agpl) +
+
+ +
+ Breaking Changes + + + +
+ CockroachDB Support removed + + After careful consideration, we have made the decision to discontinue support for CockroachDB in Zitadel v3 and beyond. + While CockroachDB is an excellent distributed SQL database, supporting multiple database backends has increased our maintenance burden and complicated our testing matrix. + Check out our [migration guide](https://zitadel.com/docs/self-hosting/manage/cli/mirror) to migrate from CockroachDB to PostgreSQL. + + More details can be found [here](https://github.com/zitadel/zitadel/issues/9414) +
+ +
+ Actions API v3 alpha removed + + With the current release we have published the Actions V2 API as a beta version, and got rid of the previously published alpha API. + Check out the [new API](http://localhost:3000/docs/apis/resources/action_service_v2) + +
+
+ +### v4.x + +**Current State**: Implementation + + +
+ New Features + + + +
+ Resource API (v2) + + We are revamping our APIs to improve the developer experience. + Currently, our use-case-based APIs are complex and inconsistent, causing confusion and slowing down integration. + To fix this, we're shifting to a resource-based approach. + This means developers will use consistent endpoints (e.g., /users) to manage resources, regardless of their own role. + This change, along with standardized naming and improved documentation, will simplify integration, accelerate development, and create a more intuitive experience for our customers and community. + + Resources integrated in this release: + - Instances + - Organizations + - Projects + - Users + + For more details read the [Github Issue](https://github.com/zitadel/zitadel/issues/6305) +
+ +
+ Login V2 + + Our new login UI has been enhanced with additional features, bringing it to feature parity with Version 1. + +
+ Device Authorization Flow + + The Device Authorization Grant is an OAuth 2.0 flow designed for devices that have limited input capabilities (like smart TVs, gaming consoles, or IoT devices) or lack a browser. + + Read our docs about how to integrate your application using the [Device Authorization Flow](https://zitadel.com/docs/guides/integrate/login/oidc/device-authorization) +
+ +
+ LDAP IDP + + This feature enables users to log in using their existing LDAP (Lightweight Directory Access Protocol) credentials. + It integrates your system with an LDAP directory, allowing it to act as an Identity Provider (IdP) solely for authentication purposes. + This means users can securely access the service with their familiar LDAP username and password, streamlining the login process. +
+ +
+ JWT IDP + + This "JSON Web Token Identity Provider (JWT IdP)" feature allows you to use an existing JSON Web Token (JWT) from another system (like a Web Application Firewall managing a session) as a federated identity for authentication in new applications managed by ZITADEL. + + Essentially, it enables session reuse by letting ZITADEL trust and validate a JWT issued by an external source. This allows users already authenticated in an existing system to seamlessly access new applications without re-logging in. + + Read more in our docs about how to login users with [JWT IDP](https://zitadel.com/docs/guides/integrate/identity-providers/jwt_idp) +
+ +
+ Custom Login UI Texts + + This feature provides customers with the flexibility to personalize the user experience by customizing various text elements across different screens of the login UI. Administrators can modify default messages, labels, and instructions to align with their branding, provide specific guidance, or cater to unique regional or organizational needs, ensuring a more tailored and intuitive authentication process for their users. +
+
+
+ + +
+ General Availability + + + +
+ Hosted Login v2 + + We're officially moving our new Login UI v2 from beta to General Availability. + Starting now, it will be the default login experience for all new customers. + With this release, 8.0we are also focused on implementing previously missing features, such as device authorization and LDAP IDP support, to make the new UI fully feature-complete. + + - [Hosted Login V2](http://localhost:3000/docs/guides/integrate/login/hosted-login#hosted-login-version-2-beta) +
+ +
+ Web Keys + + Web Keys in ZITADEL are used to sign and verify JSON Web Tokens (JWT). + ID tokens are created, signed and returned by ZITADEL when a OpenID connect (OIDC) or OAuth2 authorization flow completes and a user is authenticated. + Based on customer and community feedback, we've updated our key management system. You now have full manual control over key generation and rotation, instead of the previous automatic process. + + Read the full description about Web Keys in our [Documentation](https://zitadel.com/docs/guides/integrate/login/oidc/webkeys). +
+ +
+ SCIM 2.0 Server - User Resource + + The Zitadel SCIM v2 service provider interface enables seamless integration of identity and access management (IAM) systems with Zitadel, following the System for Cross-domain Identity Management (SCIM) v2.0 specification. + This interface allows standardized management of IAM resources, making it easier to automate user provisioning and deprovisioning. + + - [SCIM 2.0 API](https://zitadel.com/docs/apis/scim2) + - [Manage Users Guide](https://zitadel.com/docs/guides/manage/user/scim2) +
+ + +
+ Token Exchange (Impersonation) + + The Token Exchange grant implements [RFC 8693, OAuth 2.0 Token Exchange](https://www.rfc-editor.org/rfc/rfc8693) and can be used to exchange tokens to a different scope, audience or subject. + Changing the subject of an authenticated token is called impersonation or delegation. + Read more in our [Impersonation and delegation using Token Exchange](https://zitadel.com/docs/guides/integrate/token-exchange) Guide +
+ +
+ Caches + + ZITADEL supports the use of a caches to speed up the lookup of frequently needed objects. + As opposed to HTTP caches which might reside between ZITADEL and end-user applications, the cache build into ZITADEL uses active invalidation when an object gets updated. + Another difference is that HTTP caches only cache the result of a complete request and the built-in cache stores objects needed for the internal business logic. + For example, each request made to ZITADEL needs to retrieve and set instance information in middleware. + + Read more about Zitadel Caches [here](https://zitadel.com/docs/self-hosting/manage/cache) +
+
+ +### v5.x + +**Current State**: Planning + +
+ New Features + + + +
+ Analytics + + We provide comprehensive and insightful analytics capabilities that empower you with the information needed to understand platform usage, monitor system health, and make data-driven decisions. + +
+ Daily Active Users (DAU) & Monthly Active Users (MAU) + + Administrators need to track user activity to understand platform usage and identify trends. + This feature provides basic metrics for daily and monthly active users, allowing for filtering by date range and scope (instance-wide or within a specific organization). + The metrics should ensure that each user is counted only once per day or month, respectively, regardless of how many actions they performed. + This minimal feature serves as a foundation for future expansion into more detailed analytics. + + For more details track our [github issue](https://github.com/zitadel/zitadel/issues/7506). +
+ +
+ Resource Count Metrics + + To effectively manage a Zitadel instance, administrators need to understand resource utilization. + This feature provides metrics for resource counts, including organizations, users (with filtering options), projects, applications, and authorizations. + For users, we will offer filters to retrieve the total count, counts per organization, and counts by user type (human or machine). + These metrics will provide administrators with valuable insights into the scale and complexity of their Zitadel instance. + + + For more details track our [github issue](https://github.com/zitadel/zitadel/issues/9709). +
+ +
+ Operational Metrics + + To empower customers to better manage and optimize their Zitadel instances, we will provide access to detailed operational metrics. + This data will help customers identify potential issues, optimize performance, and ensure the stability of their deployments. + The provided data will encompass basic system information, infrastructure details, configuration settings, error reports, and the health status of various Zitadel components, accessible via a user interface or an API. + + + For more details track our [github issue](https://github.com/zitadel/zitadel/issues/9476). + +
+
+ +
+ User Groups + + Administrators will be able to define groups within an organization and assign users to these groups. + More details about the feature can be found [here](https://github.com/zitadel/zitadel/issues/9702) +
+ +
+ User Uniqueness on Organization Level + + Administrators will be able to define weather users should be unique across the instance or within an organization. + This allows managing users independently and avoids conflicts due to shared user identifiers. + Example: The user with the username user@gmail.com can be created in the Organization "Customer A" and "Customer B" if uniqueness is defined on the organization level. + + Stay updated on the progress and details on our [GitHub Issue](https://github.com/zitadel/zitadel/issues/9535) +
+ + +
+ Remove Required Fields + + Currently, the user creation process requires several fields, such as email, first name, and last name, which can be restrictive in certain scenarios. This feature allows administrators to create users with only a username, making other fields optional. + This provides flexibility for systems that don't require complete user profiles upon initial creation for example simplified onboarding flows. + + For more details check out our [GitHub Issue](https://github.com/zitadel/zitadel/issues/4386) +
+
+ +
+ Feature Deprecation + + + +
+ Actions V1 +
+
+ +
+ Breaking Changes + + + +
+ Hosted Login v1 will be removed +
+ + +
+ Zitadel APIs v1 will be removed +
+
+ + +### v6.x + +
+ New Features + + + +
+ Basic Threat Detection Framework + + This initial version of our Threat Detection Framework is designed to enhance the security of your account by identifying and challenging potentially anomalous user behavior. + When the system detects unusual activity, it will present a challenge, such as a reCAPTCHA, to verify that the user is legitimate and not a bot or malicious actor. + Security administrators will also have the ability to revoke user sessions based on the output of the threat detection model, providing a crucial tool to mitigate potential security risks in real-time. + + We are beginning with a straightforward reCAPTCHA-style challenge to build and refine the core framework. + This foundational step will allow us to gather insights into how the system performs and how it can be improved. + Future iterations will build upon this groundwork to incorporate more sophisticated detection methods and a wider range of challenge and response mechanisms, ensuring an increasingly robust and intelligent security posture for all users. + + More details can be found in the (GitHub Issue](https://github.com/zitadel/zitadel/issues/9707) +
+ +
+ SCIM Outbound + + Automate user provisioning to your external applications with our new SCIM Client. + This feature ensures users are automatically created in downstream systems before their first SSO login, preventing access issues and streamlining onboarding. + + It also synchronizes user lifecycle events, so changes like deactivations or deletions are instantly reflected across all connected applications for consistent and secure access management. + The initial release will focus on provisioning the user resource. + + More details can be found in the (GitHub Issue](https://github.com/zitadel/zitadel/issues/6601) +
+ +
+ Analytics + + We provide comprehensive and insightful analytics capabilities that empower you with the information needed to understand platform usage, monitor system health, and make data-driven decisions. + +
+ Login Insights: Successful and Failed Login Metrics + + To enhance security monitoring and gain insights into user authentication patterns, administrators need access to login metrics. + This feature provides data on successful and failed login attempts, allowing for filtering by time range and level (overall instance, within a specific organization, or for a particular application). + This will enable administrators to detect suspicious login activity, analyze authentication trends, and proactively address potential security concerns. + + For more details track our [GitHub issue](https://github.com/zitadel/zitadel/issues/9711). +
+
+ +
+ Impersonation: External Token Exchange + + This feature expands our existing impersonation capabilities to support seamless and secure integration with external, third-party applications. + Currently, our platform supports impersonation for internal use cases, allowing administrators or support staff to obtain a temporary token for an end-user to troubleshoot issues or provide assistance within applications that already use ZITADEL for authentication. (You can find more details in our [existing documentation](/docs/guides/integrate/token-exchange)). + + The next evolution of this feature will focus on external applications. + This enables scenarios where a user, already authenticated in a third-party system (like their primary e-banking portal), can seamlessly access a connected application that is secured by ZITADEL without needing to log in again. + + For example, a user in their e-banking app could click to open an integrated "Budget Planning" tool that relies on ZITADEL for access. + Using a secure token exchange, the budget app will grant the user a valid session on their behalf, creating a smooth, uninterrupted user experience while maintaining a high level of security. + This enhancement bridges the authentication gap between external platforms and ZITADEL-powered applications. +
+
+ +### Future Vision / Upcoming Features + +#### Fine Grained Authorization + +We're planning the future of Zitadel and fine-grained authorization is high on our list. +While Zitadel already offers strong role-based access (RBAC), we know many of you need more granular control. + +**What is Fine-Grained Authorization?** + +It's about moving beyond broad roles to define precise access based on: + +- Attributes (ABAC): User details (department, location), resource characteristics (sensitivity), or context (time of day). +- Relationships (ReBAC): Connections between users and resources (e.g., "owner" of a document, "manager" of a team). +- Policies (PBAC): Explicit rules combining attributes and relationships. + +**Why Explore This?** + +Fine-grained authorization can offer: +- Tighter Security: Minimize access to only what's essential. +- Greater Flexibility: Adapt to complex and dynamic business rules. +- Easier Compliance: Meet strict regulatory demands. +- Scalable Permissions: Manage access effectively as you grow. + +**We Need Your Input!** 🗣️ + +As we explore the best way to bring this to Zitadel, tell us: +- Your Use Cases: Where do you need more detailed access control than standard roles provide? +- Preferred Models: Are you thinking attribute-based, relationship-based, or something else? +- Integration Preferences: + - A fully integrated solution within Zitadel? + - Or integration with existing authorization vendors (e.g. openFGA, cerbos, etc.)? + +Your feedback is crucial for shaping our roadmap. + +🔗 Share your thoughts and needs in our [discussion forum](https://discord.com/channels/927474939156643850/1368861057669533736) + +#### Threat Detection + +We're taking the next step in securing your applications by exploring a new Threat Detection framework for Zitadel. +Our goal is to proactively identify and stop malicious activity in real-time. + +**Our First Step: A Modern reCAPTCHA Alternative** +We will begin by building a system to detect and mitigate malicious bots, serving as a smart, privacy-focused alternative to CAPTCHA. +This initial use case will help us combat credential stuffing, spam registrations, and other automated attacks, forming the foundation of our larger framework. + +**How We Envision It** + +Our exploration is focused on creating an intelligent system that: +- **Analyzes Signals**: Gathers data points like IP reputation, device characteristics, and user behavior to spot suspicious activity. +- **Uses AI/**: Trains models to distinguish between legitimate users and bots, reducing friction for real users. +- **Mitigates Threats**: Enables flexible responses when a threat is detected, such as blocking the attempt, requiring MFA, or sending an alert. + +**Help Us Shape the Future** 🤝 + +As we design this framework, we need to know: +- What are your biggest security threats today? +- What kind of automated responses (e.g., block, notification) would be most useful for you? +- What are your key privacy or compliance concerns regarding threat detection? + +Your feedback will directly influence our development and ensure we build a solution that truly meets your needs. + +🔗 Join the conversation and share your insights [here](https://discord.com/channels/927474939156643850/1375383775164235806) + + +#### The Role of AI in Zitadel + +As we look to the future, we believe Artificial Intelligence will be a critical tool for enhancing both user experience and security within Zitadel. +Our vision for AI is focused on two key areas: providing intelligent, contextual assistance and building a collective defense against emerging threats. + +1. **AI-Powered Support** + + We want you to get fast, accurate answers to your questions without ever having to leave your workflow. + To achieve this, we are integrating an AI-powered support assistant trained on our knowledge base, including our documentation, tutorials, and community discussions. + + Our rollout is planned in phases to ensure we deliver a helpful experience: + - **Phase 1 (Happening Now)**: We are currently testing a preliminary version of our AI bot within our [community channels](https://discord.com/channels/927474939156643850/1357076488825995477). This allows us to gather real-world questions and answers, refining the AI's accuracy and helpfulness based on direct feedback. + - **Phase 2 (Next Steps)**: Once we are confident in its capabilities, we will integrate this AI assistant directly into our documentation. You'll be able to ask complex questions and get immediate, well-sourced answers. + - **Phase 3 (The Ultimate Goal)**: The final step is to embed the assistant directly into the Zitadel Console/Customer Portal. Imagine getting help based on the exact context of what you're doing—whether you're configuring an action, setting up a new organization, or integrating social login. + +2. **Decentralized AI for Threat Detection** + + Security threats are constantly evolving. + A threat vector that targets one customer today might target another tomorrow. + We believe in the power of collective intelligence to provide proactive security for everyone. + + This leads to our second major AI initiative: **decentralized model training** for our Threat Detection framework. + + Here’s how it would work: + - **Collective Data, Anonymously**: Customers across our cloud and self-hosted environments experience different user behaviors and threat vectors. We plan to offer an opt-in system where anonymized, non-sensitive data (like behavioral patterns and threat signals) can be collected from participating instances. + - **Centralized Training**: This collective, anonymized data will be used to train powerful, next-generation AI security models. With a much larger and more diverse dataset, these models can learn to identify subtle and emerging threats far more effectively than a model trained on a single instance's data. + - **Shared Protection**: These constantly improving models would then be distributed to all participating Zitadel instances. + + The result is a powerful security network effect. You could receive protection from a threat vector you haven't even experienced yet, simply because the system learned from an attack on another member of the community. + + + +## Zitadel Ecosystem + +### PHP SDK + +GitHub Repository: [PHP SDK](https://github.com/zitadel/client-php) + +#### v3.x + + + +### Java SDK + +GitHub Repository: [Java SDK](https://github.com/zitadel/client-java) + +#### v3.x + + + +### Ruby SDK + +GitHub Repository: [Ruby SDK](https://github.com/zitadel/client-ruby) + +#### v3.x + + + +### Python SDK + +GitHub Repository: [Python SDK](https://github.com/zitadel/client-python) + +#### v3.x + + diff --git a/docs/sidebars.js b/docs/sidebars.js index f9b97703e5..1bd53ed1b3 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -183,7 +183,6 @@ module.exports = { items: [ "guides/manage/user/reg-create-user", "guides/manage/customize/user-metadata", - "guides/manage/customize/user-schema", "guides/manage/user/scim2", ], }, @@ -611,6 +610,20 @@ module.exports = { }, ], }, + { + type: "category", + label: "Product Information", + collapsed: true, + items: [ + "product/roadmap", + "product/release-cycle", + { + type: "link", + label: "Changelog", + href: "https://zitadel.com/changelog", + }, + ], + }, { type: "category", label: "Support", diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index 50035f2541..11d4208695 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -641,3 +641,7 @@ p strong { .zitadel-lifecycle-deprecated { text-decoration: line-through; } + +table#zitadel-versions td { + vertical-align: top; +} \ No newline at end of file diff --git a/docs/static/img/product/release-cycle.png b/docs/static/img/product/release-cycle.png new file mode 100644 index 0000000000000000000000000000000000000000..7017566ab7087d14af651d9e94f4b99caecac804 GIT binary patch literal 102080 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 literal 0 HcmV?d00001 From b9c1cdf4adb8b4a7737b8f8d5b6a5abd270ad5e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 3 Jun 2025 17:15:30 +0300 Subject: [PATCH 17/35] feat(projections): resource counters (#9979) # Which Problems Are Solved Add the ability to keep track of the current counts of projection resources. We want to prevent calling `SELECT COUNT(*)` on tables, as that forces a full scan and sudden spikes of DB resource uses. # How the Problems Are Solved - A resource_counts table is added - Triggers that increment and decrement the counted values on inserts and deletes - Triggers that delete all counts of a table when the source table is TRUNCATEd. This is not in the business logic, but prevents wrong counts in case someone want to force a re-projection. - Triggers that delete all counts if the parent resource is deleted - Script to pre-populate the resource_counts table when a new source table is added. The triggers are reusable for any type of resource, in case we choose to add more in the future. Counts are aggregated by a given parent. Currently only `instance` and `organization` are defined as possible parent. This can later be extended to other types, such as `project`, should the need arise. I deliberately chose to use `parent_id` to distinguish from the de-factor `resource_owner` which is usually an organization ID. For example: - For users the parent is an organization and the `parent_id` matches `resource_owner`. - For organizations the parent is an instance, but the `resource_owner` is the `org_id`. In this case the `parent_id` is the `instance_id`. - Applications would have a similar problem, where the parent is a project, but the `resource_owner` is the `org_id` # Additional Context Closes https://github.com/zitadel/zitadel/issues/9957 --- cmd/setup/57.go | 27 ++ cmd/setup/57.sql | 106 ++++++++ cmd/setup/config.go | 1 + cmd/setup/setup.go | 3 + cmd/setup/trigger_steps.go | 125 +++++++++ internal/domain/count_trigger.go | 9 + internal/domain/countparenttype_enumer.go | 109 ++++++++ internal/domain/secretgeneratortype_enumer.go | 93 +++++-- internal/migration/count_trigger.sql | 43 +++ .../delete_parent_counts_trigger.sql | 13 + internal/migration/migration.go | 5 +- internal/migration/trigger.go | 127 +++++++++ internal/migration/trigger_test.go | 253 ++++++++++++++++++ internal/query/resource_counts.go | 61 +++++ internal/query/resource_counts_list.sql | 12 + internal/query/resource_counts_test.go | 109 ++++++++ 16 files changed, 1080 insertions(+), 16 deletions(-) create mode 100644 cmd/setup/57.go create mode 100644 cmd/setup/57.sql create mode 100644 cmd/setup/trigger_steps.go create mode 100644 internal/domain/count_trigger.go create mode 100644 internal/domain/countparenttype_enumer.go create mode 100644 internal/migration/count_trigger.sql create mode 100644 internal/migration/delete_parent_counts_trigger.sql create mode 100644 internal/migration/trigger.go create mode 100644 internal/migration/trigger_test.go create mode 100644 internal/query/resource_counts.go create mode 100644 internal/query/resource_counts_list.sql create mode 100644 internal/query/resource_counts_test.go diff --git a/cmd/setup/57.go b/cmd/setup/57.go new file mode 100644 index 0000000000..4c52018f1e --- /dev/null +++ b/cmd/setup/57.go @@ -0,0 +1,27 @@ +package setup + +import ( + "context" + _ "embed" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" +) + +var ( + //go:embed 57.sql + createResourceCounts string +) + +type CreateResourceCounts struct { + dbClient *database.DB +} + +func (mig *CreateResourceCounts) Execute(ctx context.Context, _ eventstore.Event) error { + _, err := mig.dbClient.ExecContext(ctx, createResourceCounts) + return err +} + +func (mig *CreateResourceCounts) String() string { + return "57_create_resource_counts" +} diff --git a/cmd/setup/57.sql b/cmd/setup/57.sql new file mode 100644 index 0000000000..f2f0a40202 --- /dev/null +++ b/cmd/setup/57.sql @@ -0,0 +1,106 @@ +CREATE TABLE IF NOT EXISTS projections.resource_counts +( + id SERIAL PRIMARY KEY, -- allows for easy pagination + instance_id TEXT NOT NULL, + table_name TEXT NOT NULL, -- needed for trigger matching, not in reports + parent_type TEXT NOT NULL, + parent_id TEXT NOT NULL, + resource_name TEXT NOT NULL, -- friendly name for reporting + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + amount INTEGER NOT NULL DEFAULT 1 CHECK (amount >= 0), + + UNIQUE (instance_id, parent_type, parent_id, table_name) +); + +-- count_resource is a trigger function which increases or decreases the count of a resource. +-- When creating the trigger the following required arguments (TG_ARGV) can be passed: +-- 1. The type of the parent +-- 2. The column name of the instance id +-- 3. The column name of the owner id +-- 4. The name of the resource +CREATE OR REPLACE FUNCTION projections.count_resource() + RETURNS trigger + LANGUAGE 'plpgsql' VOLATILE +AS $$ +DECLARE + -- trigger variables + tg_table_name TEXT := TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME; + tg_parent_type TEXT := TG_ARGV[0]; + tg_instance_id_column TEXT := TG_ARGV[1]; + tg_parent_id_column TEXT := TG_ARGV[2]; + tg_resource_name TEXT := TG_ARGV[3]; + + tg_instance_id TEXT; + tg_parent_id TEXT; + + select_ids TEXT := format('SELECT ($1).%I, ($1).%I', tg_instance_id_column, tg_parent_id_column); +BEGIN + IF (TG_OP = 'INSERT') THEN + EXECUTE select_ids INTO tg_instance_id, tg_parent_id USING NEW; + + INSERT INTO projections.resource_counts(instance_id, table_name, parent_type, parent_id, resource_name) + VALUES (tg_instance_id, tg_table_name, tg_parent_type, tg_parent_id, tg_resource_name) + ON CONFLICT (instance_id, table_name, parent_type, parent_id) DO + UPDATE SET updated_at = now(), amount = projections.resource_counts.amount + 1; + + RETURN NEW; + ELSEIF (TG_OP = 'DELETE') THEN + EXECUTE select_ids INTO tg_instance_id, tg_parent_id USING OLD; + + UPDATE projections.resource_counts + SET updated_at = now(), amount = amount - 1 + WHERE instance_id = tg_instance_id + AND table_name = tg_table_name + AND parent_type = tg_parent_type + AND parent_id = tg_parent_id + AND resource_name = tg_resource_name + AND amount > 0; -- prevent check failure on negative amount. + + RETURN OLD; + END IF; +END +$$; + +-- delete_table_counts removes all resource counts for a TRUNCATED table. +CREATE OR REPLACE FUNCTION projections.delete_table_counts() + RETURNS trigger + LANGUAGE 'plpgsql' +AS $$ +DECLARE + -- trigger variables + tg_table_name TEXT := TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME; +BEGIN + DELETE FROM projections.resource_counts + WHERE table_name = tg_table_name; +END +$$; + +-- delete_parent_counts removes all resource counts for a deleted parent. +-- 1. The type of the parent +-- 2. The column name of the instance id +-- 3. The column name of the owner id +CREATE OR REPLACE FUNCTION projections.delete_parent_counts() + RETURNS trigger + LANGUAGE 'plpgsql' +AS $$ +DECLARE + -- trigger variables + tg_parent_type TEXT := TG_ARGV[0]; + tg_instance_id_column TEXT := TG_ARGV[1]; + tg_parent_id_column TEXT := TG_ARGV[2]; + + tg_instance_id TEXT; + tg_parent_id TEXT; + + select_ids TEXT := format('SELECT ($1).%I, ($1).%I', tg_instance_id_column, tg_parent_id_column); +BEGIN + EXECUTE select_ids INTO tg_instance_id, tg_parent_id USING OLD; + + DELETE FROM projections.resource_counts + WHERE instance_id = tg_instance_id + AND parent_type = tg_parent_type + AND parent_id = tg_parent_id; + + RETURN OLD; +END +$$; diff --git a/cmd/setup/config.go b/cmd/setup/config.go index bd2abde9ea..dd59ba3f07 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -153,6 +153,7 @@ type Steps struct { s54InstancePositionIndex *InstancePositionIndex s55ExecutionHandlerStart *ExecutionHandlerStart s56IDPTemplate6SAMLFederatedLogout *IDPTemplate6SAMLFederatedLogout + s57CreateResourceCounts *CreateResourceCounts } func MustNewSteps(v *viper.Viper) *Steps { diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index c84976f282..1465180a6b 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -215,6 +215,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s54InstancePositionIndex = &InstancePositionIndex{dbClient: dbClient} steps.s55ExecutionHandlerStart = &ExecutionHandlerStart{dbClient: dbClient} steps.s56IDPTemplate6SAMLFederatedLogout = &IDPTemplate6SAMLFederatedLogout{dbClient: dbClient} + steps.s57CreateResourceCounts = &CreateResourceCounts{dbClient: dbClient} err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil) logging.OnError(err).Fatal("unable to start projections") @@ -260,6 +261,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s54InstancePositionIndex, steps.s55ExecutionHandlerStart, steps.s56IDPTemplate6SAMLFederatedLogout, + steps.s57CreateResourceCounts, } { setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed") if setupErr != nil { @@ -296,6 +298,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) client: dbClient, }, } + repeatableSteps = append(repeatableSteps, triggerSteps(dbClient)...) for _, repeatableStep := range repeatableSteps { setupErr = executeMigration(ctx, eventstoreClient, repeatableStep, "unable to migrate repeatable step") diff --git a/cmd/setup/trigger_steps.go b/cmd/setup/trigger_steps.go new file mode 100644 index 0000000000..163a8fdb59 --- /dev/null +++ b/cmd/setup/trigger_steps.go @@ -0,0 +1,125 @@ +package setup + +import ( + "fmt" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/migration" + "github.com/zitadel/zitadel/internal/query/projection" +) + +// triggerSteps defines the repeatable migrations that set up triggers +// for counting resources in the database. +func triggerSteps(db *database.DB) []migration.RepeatableMigration { + return []migration.RepeatableMigration{ + // Delete parent count triggers for instances and organizations + migration.DeleteParentCountsTrigger(db, + projection.InstanceProjectionTable, + domain.CountParentTypeInstance, + projection.InstanceColumnID, + projection.InstanceColumnID, + "instance", + ), + migration.DeleteParentCountsTrigger(db, + projection.OrgProjectionTable, + domain.CountParentTypeOrganization, + projection.OrgColumnInstanceID, + projection.OrgColumnID, + "organization", + ), + + // Count triggers for all the resources + migration.CountTrigger(db, + projection.OrgProjectionTable, + domain.CountParentTypeInstance, + projection.OrgColumnInstanceID, + projection.OrgColumnInstanceID, + "organization", + ), + migration.CountTrigger(db, + projection.ProjectProjectionTable, + domain.CountParentTypeOrganization, + projection.ProjectColumnInstanceID, + projection.ProjectColumnResourceOwner, + "project", + ), + migration.CountTrigger(db, + projection.UserTable, + domain.CountParentTypeOrganization, + projection.UserInstanceIDCol, + projection.UserResourceOwnerCol, + "user", + ), + migration.CountTrigger(db, + projection.InstanceMemberProjectionTable, + domain.CountParentTypeInstance, + projection.MemberInstanceID, + projection.MemberResourceOwner, + "iam_admin", + ), + migration.CountTrigger(db, + projection.IDPTable, + domain.CountParentTypeInstance, + projection.IDPInstanceIDCol, + projection.IDPInstanceIDCol, + "identity_provider", + ), + migration.CountTrigger(db, + projection.IDPTemplateLDAPTable, + domain.CountParentTypeInstance, + projection.LDAPInstanceIDCol, + projection.LDAPInstanceIDCol, + "identity_provider_ldap", + ), + migration.CountTrigger(db, + projection.ActionTable, + domain.CountParentTypeInstance, + projection.ActionInstanceIDCol, + projection.ActionInstanceIDCol, + "action_v1", + ), + migration.CountTrigger(db, + projection.ExecutionTable, + domain.CountParentTypeInstance, + projection.ExecutionInstanceIDCol, + projection.ExecutionInstanceIDCol, + "execution", + ), + migration.CountTrigger(db, + fmt.Sprintf("%s_%s", projection.ExecutionTable, projection.ExecutionTargetSuffix), + domain.CountParentTypeInstance, + projection.ExecutionTargetInstanceIDCol, + projection.ExecutionTargetInstanceIDCol, + "execution_target", + ), + migration.CountTrigger(db, + projection.LoginPolicyTable, + domain.CountParentTypeInstance, + projection.LoginPolicyInstanceIDCol, + projection.LoginPolicyInstanceIDCol, + "login_policy", + ), + migration.CountTrigger(db, + projection.PasswordComplexityTable, + domain.CountParentTypeInstance, + projection.ComplexityPolicyInstanceIDCol, + projection.ComplexityPolicyInstanceIDCol, + "password_complexity_policy", + ), + migration.CountTrigger(db, + projection.PasswordAgeTable, + domain.CountParentTypeInstance, + projection.AgePolicyInstanceIDCol, + projection.AgePolicyInstanceIDCol, + "password_expiry_policy", + ), + migration.CountTrigger(db, + projection.LockoutPolicyTable, + domain.CountParentTypeInstance, + projection.LockoutPolicyInstanceIDCol, + projection.LockoutPolicyInstanceIDCol, + "lockout_policy", + ), + } +} diff --git a/internal/domain/count_trigger.go b/internal/domain/count_trigger.go new file mode 100644 index 0000000000..a29d125fe9 --- /dev/null +++ b/internal/domain/count_trigger.go @@ -0,0 +1,9 @@ +package domain + +//go:generate enumer -type CountParentType -transform lower -trimprefix CountParentType -sql +type CountParentType int + +const ( + CountParentTypeInstance CountParentType = iota + CountParentTypeOrganization +) diff --git a/internal/domain/countparenttype_enumer.go b/internal/domain/countparenttype_enumer.go new file mode 100644 index 0000000000..8691d97e62 --- /dev/null +++ b/internal/domain/countparenttype_enumer.go @@ -0,0 +1,109 @@ +// Code generated by "enumer -type CountParentType -transform lower -trimprefix CountParentType -sql"; DO NOT EDIT. + +package domain + +import ( + "database/sql/driver" + "fmt" + "strings" +) + +const _CountParentTypeName = "instanceorganization" + +var _CountParentTypeIndex = [...]uint8{0, 8, 20} + +const _CountParentTypeLowerName = "instanceorganization" + +func (i CountParentType) String() string { + if i < 0 || i >= CountParentType(len(_CountParentTypeIndex)-1) { + return fmt.Sprintf("CountParentType(%d)", i) + } + return _CountParentTypeName[_CountParentTypeIndex[i]:_CountParentTypeIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _CountParentTypeNoOp() { + var x [1]struct{} + _ = x[CountParentTypeInstance-(0)] + _ = x[CountParentTypeOrganization-(1)] +} + +var _CountParentTypeValues = []CountParentType{CountParentTypeInstance, CountParentTypeOrganization} + +var _CountParentTypeNameToValueMap = map[string]CountParentType{ + _CountParentTypeName[0:8]: CountParentTypeInstance, + _CountParentTypeLowerName[0:8]: CountParentTypeInstance, + _CountParentTypeName[8:20]: CountParentTypeOrganization, + _CountParentTypeLowerName[8:20]: CountParentTypeOrganization, +} + +var _CountParentTypeNames = []string{ + _CountParentTypeName[0:8], + _CountParentTypeName[8:20], +} + +// CountParentTypeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func CountParentTypeString(s string) (CountParentType, error) { + if val, ok := _CountParentTypeNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _CountParentTypeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to CountParentType values", s) +} + +// CountParentTypeValues returns all values of the enum +func CountParentTypeValues() []CountParentType { + return _CountParentTypeValues +} + +// CountParentTypeStrings returns a slice of all String values of the enum +func CountParentTypeStrings() []string { + strs := make([]string, len(_CountParentTypeNames)) + copy(strs, _CountParentTypeNames) + return strs +} + +// IsACountParentType returns "true" if the value is listed in the enum definition. "false" otherwise +func (i CountParentType) IsACountParentType() bool { + for _, v := range _CountParentTypeValues { + if i == v { + return true + } + } + return false +} + +func (i CountParentType) Value() (driver.Value, error) { + return i.String(), nil +} + +func (i *CountParentType) Scan(value interface{}) error { + if value == nil { + return nil + } + + var str string + switch v := value.(type) { + case []byte: + str = string(v) + case string: + str = v + case fmt.Stringer: + str = v.String() + default: + return fmt.Errorf("invalid value of CountParentType: %[1]T(%[1]v)", value) + } + + val, err := CountParentTypeString(str) + if err != nil { + return err + } + + *i = val + return nil +} diff --git a/internal/domain/secretgeneratortype_enumer.go b/internal/domain/secretgeneratortype_enumer.go index f819bafc1f..db66715670 100644 --- a/internal/domain/secretgeneratortype_enumer.go +++ b/internal/domain/secretgeneratortype_enumer.go @@ -4,11 +4,14 @@ package domain import ( "fmt" + "strings" ) -const _SecretGeneratorTypeName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailinvite_codesecret_generator_type_count" +const _SecretGeneratorTypeName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailinvite_codesigning_keysecret_generator_type_count" -var _SecretGeneratorTypeIndex = [...]uint8{0, 11, 20, 37, 54, 67, 86, 108, 118, 124, 133, 144, 171} +var _SecretGeneratorTypeIndex = [...]uint8{0, 11, 20, 37, 54, 67, 86, 108, 118, 124, 133, 144, 155, 182} + +const _SecretGeneratorTypeLowerName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailinvite_codesigning_keysecret_generator_type_count" func (i SecretGeneratorType) String() string { if i < 0 || i >= SecretGeneratorType(len(_SecretGeneratorTypeIndex)-1) { @@ -17,21 +20,70 @@ func (i SecretGeneratorType) String() string { return _SecretGeneratorTypeName[_SecretGeneratorTypeIndex[i]:_SecretGeneratorTypeIndex[i+1]] } -var _SecretGeneratorTypeValues = []SecretGeneratorType{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _SecretGeneratorTypeNoOp() { + var x [1]struct{} + _ = x[SecretGeneratorTypeUnspecified-(0)] + _ = x[SecretGeneratorTypeInitCode-(1)] + _ = x[SecretGeneratorTypeVerifyEmailCode-(2)] + _ = x[SecretGeneratorTypeVerifyPhoneCode-(3)] + _ = x[SecretGeneratorTypeVerifyDomain-(4)] + _ = x[SecretGeneratorTypePasswordResetCode-(5)] + _ = x[SecretGeneratorTypePasswordlessInitCode-(6)] + _ = x[SecretGeneratorTypeAppSecret-(7)] + _ = x[SecretGeneratorTypeOTPSMS-(8)] + _ = x[SecretGeneratorTypeOTPEmail-(9)] + _ = x[SecretGeneratorTypeInviteCode-(10)] + _ = x[SecretGeneratorTypeSigningKey-(11)] + _ = x[secretGeneratorTypeCount-(12)] +} + +var _SecretGeneratorTypeValues = []SecretGeneratorType{SecretGeneratorTypeUnspecified, SecretGeneratorTypeInitCode, SecretGeneratorTypeVerifyEmailCode, SecretGeneratorTypeVerifyPhoneCode, SecretGeneratorTypeVerifyDomain, SecretGeneratorTypePasswordResetCode, SecretGeneratorTypePasswordlessInitCode, SecretGeneratorTypeAppSecret, SecretGeneratorTypeOTPSMS, SecretGeneratorTypeOTPEmail, SecretGeneratorTypeInviteCode, SecretGeneratorTypeSigningKey, secretGeneratorTypeCount} var _SecretGeneratorTypeNameToValueMap = map[string]SecretGeneratorType{ - _SecretGeneratorTypeName[0:11]: 0, - _SecretGeneratorTypeName[11:20]: 1, - _SecretGeneratorTypeName[20:37]: 2, - _SecretGeneratorTypeName[37:54]: 3, - _SecretGeneratorTypeName[54:67]: 4, - _SecretGeneratorTypeName[67:86]: 5, - _SecretGeneratorTypeName[86:108]: 6, - _SecretGeneratorTypeName[108:118]: 7, - _SecretGeneratorTypeName[118:124]: 8, - _SecretGeneratorTypeName[124:133]: 9, - _SecretGeneratorTypeName[133:144]: 10, - _SecretGeneratorTypeName[144:171]: 11, + _SecretGeneratorTypeName[0:11]: SecretGeneratorTypeUnspecified, + _SecretGeneratorTypeLowerName[0:11]: SecretGeneratorTypeUnspecified, + _SecretGeneratorTypeName[11:20]: SecretGeneratorTypeInitCode, + _SecretGeneratorTypeLowerName[11:20]: SecretGeneratorTypeInitCode, + _SecretGeneratorTypeName[20:37]: SecretGeneratorTypeVerifyEmailCode, + _SecretGeneratorTypeLowerName[20:37]: SecretGeneratorTypeVerifyEmailCode, + _SecretGeneratorTypeName[37:54]: SecretGeneratorTypeVerifyPhoneCode, + _SecretGeneratorTypeLowerName[37:54]: SecretGeneratorTypeVerifyPhoneCode, + _SecretGeneratorTypeName[54:67]: SecretGeneratorTypeVerifyDomain, + _SecretGeneratorTypeLowerName[54:67]: SecretGeneratorTypeVerifyDomain, + _SecretGeneratorTypeName[67:86]: SecretGeneratorTypePasswordResetCode, + _SecretGeneratorTypeLowerName[67:86]: SecretGeneratorTypePasswordResetCode, + _SecretGeneratorTypeName[86:108]: SecretGeneratorTypePasswordlessInitCode, + _SecretGeneratorTypeLowerName[86:108]: SecretGeneratorTypePasswordlessInitCode, + _SecretGeneratorTypeName[108:118]: SecretGeneratorTypeAppSecret, + _SecretGeneratorTypeLowerName[108:118]: SecretGeneratorTypeAppSecret, + _SecretGeneratorTypeName[118:124]: SecretGeneratorTypeOTPSMS, + _SecretGeneratorTypeLowerName[118:124]: SecretGeneratorTypeOTPSMS, + _SecretGeneratorTypeName[124:133]: SecretGeneratorTypeOTPEmail, + _SecretGeneratorTypeLowerName[124:133]: SecretGeneratorTypeOTPEmail, + _SecretGeneratorTypeName[133:144]: SecretGeneratorTypeInviteCode, + _SecretGeneratorTypeLowerName[133:144]: SecretGeneratorTypeInviteCode, + _SecretGeneratorTypeName[144:155]: SecretGeneratorTypeSigningKey, + _SecretGeneratorTypeLowerName[144:155]: SecretGeneratorTypeSigningKey, + _SecretGeneratorTypeName[155:182]: secretGeneratorTypeCount, + _SecretGeneratorTypeLowerName[155:182]: secretGeneratorTypeCount, +} + +var _SecretGeneratorTypeNames = []string{ + _SecretGeneratorTypeName[0:11], + _SecretGeneratorTypeName[11:20], + _SecretGeneratorTypeName[20:37], + _SecretGeneratorTypeName[37:54], + _SecretGeneratorTypeName[54:67], + _SecretGeneratorTypeName[67:86], + _SecretGeneratorTypeName[86:108], + _SecretGeneratorTypeName[108:118], + _SecretGeneratorTypeName[118:124], + _SecretGeneratorTypeName[124:133], + _SecretGeneratorTypeName[133:144], + _SecretGeneratorTypeName[144:155], + _SecretGeneratorTypeName[155:182], } // SecretGeneratorTypeString retrieves an enum value from the enum constants string name. @@ -40,6 +92,10 @@ func SecretGeneratorTypeString(s string) (SecretGeneratorType, error) { if val, ok := _SecretGeneratorTypeNameToValueMap[s]; ok { return val, nil } + + if val, ok := _SecretGeneratorTypeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } return 0, fmt.Errorf("%s does not belong to SecretGeneratorType values", s) } @@ -48,6 +104,13 @@ func SecretGeneratorTypeValues() []SecretGeneratorType { return _SecretGeneratorTypeValues } +// SecretGeneratorTypeStrings returns a slice of all String values of the enum +func SecretGeneratorTypeStrings() []string { + strs := make([]string, len(_SecretGeneratorTypeNames)) + copy(strs, _SecretGeneratorTypeNames) + return strs +} + // IsASecretGeneratorType returns "true" if the value is listed in the enum definition. "false" otherwise func (i SecretGeneratorType) IsASecretGeneratorType() bool { for _, v := range _SecretGeneratorTypeValues { diff --git a/internal/migration/count_trigger.sql b/internal/migration/count_trigger.sql new file mode 100644 index 0000000000..4b521094ab --- /dev/null +++ b/internal/migration/count_trigger.sql @@ -0,0 +1,43 @@ +{{ define "count_trigger" -}} +CREATE OR REPLACE TRIGGER count_{{ .Resource }} + AFTER INSERT OR DELETE + ON {{ .Table }} + FOR EACH ROW + EXECUTE FUNCTION projections.count_resource( + '{{ .ParentType }}', + '{{ .InstanceIDColumn }}', + '{{ .ParentIDColumn }}', + '{{ .Resource }}' + ); + +CREATE OR REPLACE TRIGGER truncate_{{ .Resource }}_counts + AFTER TRUNCATE + ON {{ .Table }} + FOR EACH STATEMENT + EXECUTE FUNCTION projections.delete_table_counts(); + +-- Prevent inserts and deletes while we populate the counts. +LOCK TABLE {{ .Table }} IN SHARE MODE; + +-- Populate the resource counts for the existing data in the table. +INSERT INTO projections.resource_counts( + instance_id, + table_name, + parent_type, + parent_id, + resource_name, + amount +) +SELECT + {{ .InstanceIDColumn }}, + '{{ .Table }}', + '{{ .ParentType }}', + {{ .ParentIDColumn }}, + '{{ .Resource }}', + COUNT(*) AS amount +FROM {{ .Table }} +GROUP BY ({{ .InstanceIDColumn }}, {{ .ParentIDColumn }}) +ON CONFLICT (instance_id, table_name, parent_type, parent_id) DO +UPDATE SET updated_at = now(), amount = EXCLUDED.amount; + +{{- end -}} diff --git a/internal/migration/delete_parent_counts_trigger.sql b/internal/migration/delete_parent_counts_trigger.sql new file mode 100644 index 0000000000..a2e9df6626 --- /dev/null +++ b/internal/migration/delete_parent_counts_trigger.sql @@ -0,0 +1,13 @@ +{{ define "delete_parent_counts_trigger" -}} + +CREATE OR REPLACE TRIGGER delete_parent_counts_trigger + AFTER DELETE + ON {{ .Table }} + FOR EACH ROW + EXECUTE FUNCTION projections.delete_parent_counts( + '{{ .ParentType }}', + '{{ .InstanceIDColumn }}', + '{{ .ParentIDColumn }}' + ); + +{{- end -}} diff --git a/internal/migration/migration.go b/internal/migration/migration.go index a2224340a7..3aeb2f0612 100644 --- a/internal/migration/migration.go +++ b/internal/migration/migration.go @@ -36,7 +36,10 @@ type errCheckerMigration interface { type RepeatableMigration interface { Migration - Check(lastRun map[string]interface{}) bool + + // Check if the migration should be executed again. + // True will repeat the migration, false will not. + Check(lastRun map[string]any) bool } func Migrate(ctx context.Context, es *eventstore.Eventstore, migration Migration) (err error) { diff --git a/internal/migration/trigger.go b/internal/migration/trigger.go new file mode 100644 index 0000000000..bd06afd5c5 --- /dev/null +++ b/internal/migration/trigger.go @@ -0,0 +1,127 @@ +package migration + +import ( + "context" + "embed" + "fmt" + "strings" + "text/template" + + "github.com/mitchellh/mapstructure" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ( + countTriggerTmpl = "count_trigger" + deleteParentCountsTmpl = "delete_parent_counts_trigger" +) + +var ( + //go:embed *.sql + templateFS embed.FS + templates = template.Must(template.ParseFS(templateFS, "*.sql")) +) + +// CountTrigger registers the existing projections.count_trigger function. +// The trigger than takes care of keeping count of existing +// rows in the source table. +// It also pre-populates the projections.resource_counts table with +// the counts for the given table. +// +// During the population of the resource_counts table, +// the source table is share-locked to prevent concurrent modifications. +// Projection handlers will be halted until the lock is released. +// SELECT statements are not blocked by the lock. +// +// This migration repeats when any of the arguments are changed, +// such as renaming of a projection table. +func CountTrigger( + db *database.DB, + table string, + parentType domain.CountParentType, + instanceIDColumn string, + parentIDColumn string, + resource string, +) RepeatableMigration { + return &triggerMigration{ + triggerConfig: triggerConfig{ + Table: table, + ParentType: parentType.String(), + InstanceIDColumn: instanceIDColumn, + ParentIDColumn: parentIDColumn, + Resource: resource, + }, + db: db, + templateName: countTriggerTmpl, + } +} + +// DeleteParentCountsTrigger +// +// This migration repeats when any of the arguments are changed, +// such as renaming of a projection table. +func DeleteParentCountsTrigger( + db *database.DB, + table string, + parentType domain.CountParentType, + instanceIDColumn string, + parentIDColumn string, + resource string, +) RepeatableMigration { + return &triggerMigration{ + triggerConfig: triggerConfig{ + Table: table, + ParentType: parentType.String(), + InstanceIDColumn: instanceIDColumn, + ParentIDColumn: parentIDColumn, + Resource: resource, + }, + db: db, + templateName: deleteParentCountsTmpl, + } +} + +type triggerMigration struct { + triggerConfig + db *database.DB + templateName string +} + +// String implements [Migration] and [fmt.Stringer]. +func (m *triggerMigration) String() string { + return fmt.Sprintf("repeatable_%s_%s", m.Resource, m.templateName) +} + +// Execute implements [Migration] +func (m *triggerMigration) Execute(ctx context.Context, _ eventstore.Event) error { + var query strings.Builder + err := templates.ExecuteTemplate(&query, m.templateName, m.triggerConfig) + if err != nil { + return fmt.Errorf("%s: execute trigger template: %w", m, err) + } + _, err = m.db.ExecContext(ctx, query.String()) + if err != nil { + return fmt.Errorf("%s: exec trigger query: %w", m, err) + } + return nil +} + +type triggerConfig struct { + Table string `json:"table,omitempty" mapstructure:"table"` + ParentType string `json:"parent_type,omitempty" mapstructure:"parent_type"` + InstanceIDColumn string `json:"instance_id_column,omitempty" mapstructure:"instance_id_column"` + ParentIDColumn string `json:"parent_id_column,omitempty" mapstructure:"parent_id_column"` + Resource string `json:"resource,omitempty" mapstructure:"resource"` +} + +// Check implements [RepeatableMigration]. +func (c *triggerConfig) Check(lastRun map[string]any) bool { + var dst triggerConfig + if err := mapstructure.Decode(lastRun, &dst); err != nil { + panic(err) + } + return dst != *c +} diff --git a/internal/migration/trigger_test.go b/internal/migration/trigger_test.go new file mode 100644 index 0000000000..5799526428 --- /dev/null +++ b/internal/migration/trigger_test.go @@ -0,0 +1,253 @@ +package migration + +import ( + "context" + "regexp" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/database" +) + +const ( + expCountTriggerQuery = `CREATE OR REPLACE TRIGGER count_resource + AFTER INSERT OR DELETE + ON table + FOR EACH ROW + EXECUTE FUNCTION projections.count_resource( + 'instance', + 'instance_id', + 'parent_id', + 'resource' + ); + +CREATE OR REPLACE TRIGGER truncate_resource_counts + AFTER TRUNCATE + ON table + FOR EACH STATEMENT + EXECUTE FUNCTION projections.delete_table_counts(); + +-- Prevent inserts and deletes while we populate the counts. +LOCK TABLE table IN SHARE MODE; + +-- Populate the resource counts for the existing data in the table. +INSERT INTO projections.resource_counts( + instance_id, + table_name, + parent_type, + parent_id, + resource_name, + amount +) +SELECT + instance_id, + 'table', + 'instance', + parent_id, + 'resource', + COUNT(*) AS amount +FROM table +GROUP BY (instance_id, parent_id) +ON CONFLICT (instance_id, table_name, parent_type, parent_id) DO +UPDATE SET updated_at = now(), amount = EXCLUDED.amount;` + + expDeleteParentCountsQuery = `CREATE OR REPLACE TRIGGER delete_parent_counts_trigger + AFTER DELETE + ON table + FOR EACH ROW + EXECUTE FUNCTION projections.delete_parent_counts( + 'instance', + 'instance_id', + 'parent_id' + );` +) + +func Test_triggerMigration_Execute(t *testing.T) { + type fields struct { + triggerConfig triggerConfig + templateName string + } + tests := []struct { + name string + fields fields + expects func(sqlmock.Sqlmock) + wantErr bool + }{ + { + name: "template error", + fields: fields{ + triggerConfig: triggerConfig{ + Table: "table", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "resource", + }, + templateName: "foo", + }, + expects: func(_ sqlmock.Sqlmock) {}, + wantErr: true, + }, + { + name: "db error", + fields: fields{ + triggerConfig: triggerConfig{ + Table: "table", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "resource", + }, + templateName: countTriggerTmpl, + }, + expects: func(mock sqlmock.Sqlmock) { + mock.ExpectExec(regexp.QuoteMeta(expCountTriggerQuery)). + WillReturnError(assert.AnError) + }, + wantErr: true, + }, + { + name: "count trigger", + fields: fields{ + triggerConfig: triggerConfig{ + Table: "table", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "resource", + }, + templateName: countTriggerTmpl, + }, + expects: func(mock sqlmock.Sqlmock) { + mock.ExpectExec(regexp.QuoteMeta(expCountTriggerQuery)). + WithoutArgs(). + WillReturnResult( + sqlmock.NewResult(1, 1), + ) + }, + }, + { + name: "count trigger", + fields: fields{ + triggerConfig: triggerConfig{ + Table: "table", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "resource", + }, + templateName: deleteParentCountsTmpl, + }, + expects: func(mock sqlmock.Sqlmock) { + mock.ExpectExec(regexp.QuoteMeta(expDeleteParentCountsQuery)). + WithoutArgs(). + WillReturnResult( + sqlmock.NewResult(1, 1), + ) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer func() { + err := mock.ExpectationsWereMet() + require.NoError(t, err) + }() + defer db.Close() + tt.expects(mock) + mock.ExpectClose() + + m := &triggerMigration{ + db: &database.DB{ + DB: db, + }, + triggerConfig: tt.fields.triggerConfig, + templateName: tt.fields.templateName, + } + err = m.Execute(context.Background(), nil) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func Test_triggerConfig_Check(t *testing.T) { + type fields struct { + Table string + ParentType string + InstanceIDColumn string + ParentIDColumn string + Resource string + } + type args struct { + lastRun map[string]any + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "should", + fields: fields{ + Table: "users2", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "user", + }, + args: args{ + lastRun: map[string]any{ + "table": "users1", + "parent_type": "instance", + "instance_id_column": "instance_id", + "parent_id_column": "parent_id", + "resource": "user", + }, + }, + want: true, + }, + { + name: "should not", + fields: fields{ + Table: "users1", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "user", + }, + args: args{ + lastRun: map[string]any{ + "table": "users1", + "parent_type": "instance", + "instance_id_column": "instance_id", + "parent_id_column": "parent_id", + "resource": "user", + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &triggerConfig{ + Table: tt.fields.Table, + ParentType: tt.fields.ParentType, + InstanceIDColumn: tt.fields.InstanceIDColumn, + ParentIDColumn: tt.fields.ParentIDColumn, + Resource: tt.fields.Resource, + } + got := c.Check(tt.args.lastRun) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/query/resource_counts.go b/internal/query/resource_counts.go new file mode 100644 index 0000000000..9d486e0b90 --- /dev/null +++ b/internal/query/resource_counts.go @@ -0,0 +1,61 @@ +package query + +import ( + "context" + "database/sql" + _ "embed" + "time" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +var ( + //go:embed resource_counts_list.sql + resourceCountsListQuery string +) + +type ResourceCount struct { + ID int // Primary key, used for pagination + InstanceID string + TableName string + ParentType domain.CountParentType + ParentID string + Resource string + UpdatedAt time.Time + Amount int +} + +// ListResourceCounts retrieves all resource counts. +// It supports pagination using lastID and limit parameters. +// +// TODO: Currently only a proof of concept, filters may be implemented later if required. +func (q *Queries) ListResourceCounts(ctx context.Context, lastID, limit int) (result []ResourceCount, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { + for rows.Next() { + var count ResourceCount + err := rows.Scan( + &count.ID, + &count.InstanceID, + &count.TableName, + &count.ParentType, + &count.ParentID, + &count.Resource, + &count.UpdatedAt, + &count.Amount) + if err != nil { + return zerrors.ThrowInternal(err, "QUERY-2f4g5", "Errors.Internal") + } + result = append(result, count) + } + return nil + }, resourceCountsListQuery, lastID, limit) + if err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-3f4g5", "Errors.Internal") + } + return result, nil +} diff --git a/internal/query/resource_counts_list.sql b/internal/query/resource_counts_list.sql new file mode 100644 index 0000000000..0d4abf87eb --- /dev/null +++ b/internal/query/resource_counts_list.sql @@ -0,0 +1,12 @@ +SELECT id, + instance_id, + table_name, + parent_type, + parent_id, + resource_name, + updated_at, + amount +FROM projections.resource_counts +WHERE id > $1 +ORDER BY id +LIMIT $2; diff --git a/internal/query/resource_counts_test.go b/internal/query/resource_counts_test.go new file mode 100644 index 0000000000..2829a660ef --- /dev/null +++ b/internal/query/resource_counts_test.go @@ -0,0 +1,109 @@ +package query + +import ( + "context" + _ "embed" + "regexp" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" +) + +func TestQueries_ListResourceCounts(t *testing.T) { + columns := []string{"id", "instance_id", "table_name", "parent_type", "parent_id", "resource_name", "updated_at", "amount"} + type args struct { + lastID int + limit int + } + tests := []struct { + name string + args args + expects func(sqlmock.Sqlmock) + wantResult []ResourceCount + wantErr bool + }{ + { + name: "query error", + args: args{ + lastID: 0, + limit: 10, + }, + expects: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(regexp.QuoteMeta(resourceCountsListQuery)). + WithArgs(0, 10). + WillReturnError(assert.AnError) + }, + wantErr: true, + }, + { + name: "success", + args: args{ + lastID: 0, + limit: 10, + }, + expects: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(regexp.QuoteMeta(resourceCountsListQuery)). + WithArgs(0, 10). + WillReturnRows( + sqlmock.NewRows(columns). + AddRow(1, "instance_1", "table", "instance", "parent_1", "resource_name", time.Unix(1, 2), 5). + AddRow(2, "instance_2", "table", "instance", "parent_2", "resource_name", time.Unix(1, 2), 6), + ) + }, + wantResult: []ResourceCount{ + { + ID: 1, + InstanceID: "instance_1", + TableName: "table", + ParentType: domain.CountParentTypeInstance, + ParentID: "parent_1", + Resource: "resource_name", + UpdatedAt: time.Unix(1, 2), + Amount: 5, + }, + { + ID: 2, + InstanceID: "instance_2", + TableName: "table", + ParentType: domain.CountParentTypeInstance, + ParentID: "parent_2", + Resource: "resource_name", + UpdatedAt: time.Unix(1, 2), + Amount: 6, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer func() { + err := mock.ExpectationsWereMet() + require.NoError(t, err) + }() + defer db.Close() + tt.expects(mock) + mock.ExpectClose() + q := &Queries{ + client: &database.DB{ + DB: db, + }, + } + + gotResult, err := q.ListResourceCounts(context.Background(), tt.args.lastID, tt.args.limit) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantResult, gotResult, "ListResourceCounts() result mismatch") + }) + } +} From 1e5ffd41c9a624df49d2f743ca199330b9463c91 Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:18:16 +0200 Subject: [PATCH 18/35] docs(10016): improve understanding of output (#10014) # Which Problems Are Solved The output of the sql statement of tech advisory was unclear on how the data should be compared # How the Problems Are Solved An additional column is added to the output to show the effective difference of the old and new position. --- docs/docs/support/advisory/a10016.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/docs/support/advisory/a10016.md b/docs/docs/support/advisory/a10016.md index 38d73e6078..6272eaa81d 100644 --- a/docs/docs/support/advisory/a10016.md +++ b/docs/docs/support/advisory/a10016.md @@ -87,12 +87,13 @@ select b.aggregate_type, b.sequence, b.old_position, - b.new_position + b.new_position, + b.old_position - b.new_position difference from broken b; ``` -If the output from the above looks reasonable, for example not a huge difference between `old_position` and `new_position`, commit the transaction: +If the output from the above looks reasonable, for example not a huge number in the `difference` column, commit the transaction: ```sql commit; From e2a61a60029783f9a29bf7b71f2ac3d8fd39bb78 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Wed, 4 Jun 2025 08:41:10 +0200 Subject: [PATCH 19/35] docs(api): remove unreleased services from api reference (#10015) # Which Problems Are Solved As we migrate resources to the new API, whenever a an implementation got merged, the API reference was added to the docs sidenav. As these new services and their implementation are not yet released, it can be confusing for developers as the corresponding endpoints return 404 or unimplemented errors. # How the Problems Are Solved Currently we just remove it from the sidenav and will add it once they're released. We're looking into a proper solution for the API references. # Additional Changes None # Additional Context None --- docs/sidebars.js | 41 ----------------------------------------- 1 file changed, 41 deletions(-) diff --git a/docs/sidebars.js b/docs/sidebars.js index 1bd53ed1b3..d7ebb80f5b 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -805,18 +805,6 @@ module.exports = { }, items: sidebar_api_org_service_v2, }, - { - type: "category", - label: "Organization (Beta)", - link: { - type: "generated-index", - title: "Organization Service beta API", - slug: "/apis/resources/org_service/v2beta", - description: - "This API is intended to manage organizations for ZITADEL. \n", - }, - items: sidebar_api_org_service_v2beta, - }, { type: "category", label: "Identity Provider", @@ -868,35 +856,6 @@ module.exports = { }, items: sidebar_api_actions_v2, }, - { - type: "category", - label: "Project (Beta)", - link: { - type: "generated-index", - title: "Project Service API (Beta)", - slug: "/apis/resources/project_service_v2", - description: - "This API is intended to manage projects and subresources for ZITADEL. \n"+ - "\n" + - "This service is in beta state. It can AND will continue breaking until a stable version is released.", - }, - items: sidebar_api_project_service_v2, - label: "Instance (Beta)", - link: { - type: "generated-index", - title: "Instance Service API (Beta)", - slug: "/apis/resources/instance_service_v2", - description: - "This API is intended to manage instances, custom domains and trusted domains in ZITADEL.\n" + - "\n" + - "This service is in beta state. It can AND will continue breaking until a stable version is released.\n"+ - "\n" + - "This v2 of the API provides the same functionalities as the v1, but organised on a per resource basis.\n" + - "The whole functionality related to domains (custom and trusted) has been moved under this instance API." - , - }, - items: sidebar_api_instance_service_v2, - }, ], }, { From 8fc11a7366dcaf24a11d3c4fd26e86f5e61d4d1f Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Wed, 4 Jun 2025 09:17:23 +0200 Subject: [PATCH 20/35] feat: user api requests to resource API (#9794) # Which Problems Are Solved This pull request addresses a significant gap in the user service v2 API, which currently lacks methods for managing machine users. # How the Problems Are Solved This PR adds new API endpoints to the user service v2 to manage machine users including their secret, keys and personal access tokens. Additionally, there's now a CreateUser and UpdateUser endpoints which allow to create either a human or machine user and update them. The existing `CreateHumanUser` endpoint has been deprecated along the corresponding management service endpoints. For details check the additional context section. # Additional Context - Closes https://github.com/zitadel/zitadel/issues/9349 ## More details - API changes: https://github.com/zitadel/zitadel/pull/9680 - Implementation: https://github.com/zitadel/zitadel/pull/9763 - Tests: https://github.com/zitadel/zitadel/pull/9771 ## Follow-ups - Metadata: support managing user metadata using resource API https://github.com/zitadel/zitadel/pull/10005 - Machine token type: support managing the machine token type (migrate to new enum with zero value unspecified?) --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Livio Spring --- API_DESIGN.md | 4 +- cmd/start/start.go | 2 +- go.mod | 1 + internal/api/grpc/admin/import.go | 2 +- internal/api/grpc/filter/v2/converter.go | 50 + .../grpc/management/project_application.go | 2 +- internal/api/grpc/management/user.go | 13 +- .../project/v2beta/integration/query_test.go | 148 +- internal/api/grpc/project/v2beta/query.go | 23 +- internal/api/grpc/user/v2/human.go | 187 ++ internal/api/grpc/user/v2/human_test.go | 254 +++ .../user/v2/integration_test/email_test.go | 4 +- .../grpc/user/v2/integration_test/key_test.go | 659 +++++++ .../user/v2/integration_test/password_test.go | 2 +- .../grpc/user/v2/integration_test/pat_test.go | 615 ++++++ .../user/v2/integration_test/phone_test.go | 4 +- .../user/v2/integration_test/secret_test.go | 347 ++++ .../user/v2/integration_test/user_test.go | 1711 ++++++++++++++++- internal/api/grpc/user/v2/key.go | 62 + internal/api/grpc/user/v2/key_query.go | 124 ++ internal/api/grpc/user/v2/machine.go | 58 + internal/api/grpc/user/v2/machine_test.go | 62 + internal/api/grpc/user/v2/pat.go | 56 + internal/api/grpc/user/v2/pat_query.go | 123 ++ internal/api/grpc/user/v2/secret.go | 39 + internal/api/grpc/user/v2/server.go | 16 +- internal/api/grpc/user/v2/user.go | 105 +- .../grpc/user/v2/{query.go => user_query.go} | 0 internal/api/scim/resources/user.go | 1 - internal/command/instance_member.go | 2 +- internal/command/org_member.go | 2 +- ..._ower_model.go => resource_owner_model.go} | 0 internal/command/user.go | 24 +- internal/command/user_machine.go | 48 +- internal/command/user_machine_key.go | 24 +- internal/command/user_machine_model.go | 13 +- internal/command/user_machine_secret.go | 22 +- internal/command/user_machine_secret_test.go | 8 +- internal/command/user_machine_test.go | 238 ++- .../command/user_personal_access_token.go | 20 +- internal/command/user_test.go | 2 +- internal/command/user_v2.go | 2 - internal/command/user_v2_human.go | 64 +- internal/command/user_v2_human_test.go | 8 +- internal/command/user_v2_invite_test.go | 1 + internal/command/user_v2_machine.go | 94 + internal/command/user_v2_machine_test.go | 260 +++ internal/command/user_v2_model.go | 8 + internal/domain/permission.go | 2 +- internal/eventstore/write_model.go | 26 +- internal/integration/client.go | 40 + internal/query/authn_key.go | 146 +- internal/query/authn_key_test.go | 8 + internal/query/projection/authn_key.go | 3 + .../projection/user_personal_access_token.go | 2 + internal/query/user.go | 14 +- internal/query/user_personal_access_token.go | 60 +- internal/repository/user/machine.go | 7 +- internal/static/i18n/bg.yaml | 1 + internal/static/i18n/cs.yaml | 1 + internal/static/i18n/de.yaml | 1 + internal/static/i18n/en.yaml | 1 + internal/static/i18n/es.yaml | 1 + internal/static/i18n/fr.yaml | 1 + internal/static/i18n/hu.yaml | 1 + internal/static/i18n/id.yaml | 1 + internal/static/i18n/it.yaml | 1 + internal/static/i18n/ja.yaml | 1 + internal/static/i18n/ko.yaml | 1 + internal/static/i18n/mk.yaml | 1 + internal/static/i18n/nl.yaml | 1 + internal/static/i18n/pl.yaml | 1 + internal/static/i18n/pt.yaml | 1 + internal/static/i18n/ro.yaml | 1 + internal/static/i18n/ru.yaml | 1 + internal/static/i18n/sv.yaml | 1 + internal/static/i18n/zh.yaml | 1 + proto/buf.yaml | 2 +- proto/zitadel/filter/v2/filter.proto | 96 + proto/zitadel/filter/v2beta/filter.proto | 36 +- proto/zitadel/management.proto | 201 +- proto/zitadel/project/v2beta/query.proto | 66 +- proto/zitadel/user/v2/email.proto | 2 +- proto/zitadel/user/v2/key.proto | 69 + proto/zitadel/user/v2/pat.proto | 70 + proto/zitadel/user/v2/user_service.proto | 1186 +++++++++++- 86 files changed, 7033 insertions(+), 536 deletions(-) create mode 100644 internal/api/grpc/filter/v2/converter.go create mode 100644 internal/api/grpc/user/v2/human.go create mode 100644 internal/api/grpc/user/v2/human_test.go create mode 100644 internal/api/grpc/user/v2/integration_test/key_test.go create mode 100644 internal/api/grpc/user/v2/integration_test/pat_test.go create mode 100644 internal/api/grpc/user/v2/integration_test/secret_test.go create mode 100644 internal/api/grpc/user/v2/key.go create mode 100644 internal/api/grpc/user/v2/key_query.go create mode 100644 internal/api/grpc/user/v2/machine.go create mode 100644 internal/api/grpc/user/v2/machine_test.go create mode 100644 internal/api/grpc/user/v2/pat.go create mode 100644 internal/api/grpc/user/v2/pat_query.go create mode 100644 internal/api/grpc/user/v2/secret.go rename internal/api/grpc/user/v2/{query.go => user_query.go} (100%) rename internal/command/{resource_ower_model.go => resource_owner_model.go} (100%) create mode 100644 internal/command/user_v2_machine.go create mode 100644 internal/command/user_v2_machine_test.go create mode 100644 proto/zitadel/filter/v2/filter.proto create mode 100644 proto/zitadel/user/v2/key.proto create mode 100644 proto/zitadel/user/v2/pat.proto diff --git a/API_DESIGN.md b/API_DESIGN.md index 9e77657ab0..11b7766a49 100644 --- a/API_DESIGN.md +++ b/API_DESIGN.md @@ -206,6 +206,8 @@ The same applies to messages that are returned by multiple resources. For example, information about the `User` might be different when managing the user resource itself than when it's returned as part of an authorization or a manager role, where only limited information is needed. +On the other hand, types that always follow the same pattern and are used in multiple resources, such as `IDFilter`, `TimestampFilter` or `InIDsFilter` SHOULD be globalized and reused. + ##### Re-using messages Prevent reusing messages for the creation and the retrieval of a resource. @@ -271,7 +273,7 @@ Additionally, state changes, specific actions or operations that do not fit into The API uses OAuth 2 for authorization. There are corresponding middlewares that check the access token for validity and automatically return an error if the token is invalid. -Permissions grated to the user might be organization specific and can therefore only be checked based on the queried resource. +Permissions granted to the user might be organization specific and can therefore only be checked based on the queried resource. In such case, the API does not check the permissions itself but relies on the checks of the functions that are called by the API. If the permission can be checked by the API itself, e.g. if the permission is instance wide, it can be annotated on the endpoint in the proto file (see below). In any case, the required permissions need to be documented in the [API documentation](#documentation). diff --git a/cmd/start/start.go b/cmd/start/start.go index 2fc1fb8413..8820480f0c 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -461,7 +461,7 @@ func startAPIs( if err := apis.RegisterService(ctx, user_v2beta.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, user_v2.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil { + if err := apis.RegisterService(ctx, user_v2.CreateServer(config.SystemDefaults, commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil { return nil, err } if err := apis.RegisterService(ctx, session_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil { diff --git a/go.mod b/go.mod index c1cbf2dd77..21a7fe9f16 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( github.com/go-webauthn/webauthn v0.10.2 github.com/goccy/go-json v0.10.5 github.com/golang/protobuf v1.5.4 + github.com/google/go-cmp v0.7.0 github.com/gorilla/csrf v1.7.2 github.com/gorilla/mux v1.8.1 github.com/gorilla/schema v1.4.1 diff --git a/internal/api/grpc/admin/import.go b/internal/api/grpc/admin/import.go index 119afe9fc0..41a1e39081 100644 --- a/internal/api/grpc/admin/import.go +++ b/internal/api/grpc/admin/import.go @@ -510,7 +510,7 @@ func importMachineUsers(ctx context.Context, s *Server, errors *[]*admin_pb.Impo } for _, user := range org.GetMachineUsers() { logging.Debugf("import user: %s", user.GetUserId()) - _, err := s.command.AddMachine(ctx, management.AddMachineUserRequestToCommand(user.GetUser(), org.GetOrgId())) + _, err := s.command.AddMachine(ctx, management.AddMachineUserRequestToCommand(user.GetUser(), org.GetOrgId()), nil) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "machine_user", Id: user.GetUserId(), Message: err.Error()}) if isCtxTimeout(ctx) { diff --git a/internal/api/grpc/filter/v2/converter.go b/internal/api/grpc/filter/v2/converter.go new file mode 100644 index 0000000000..7a7d7cd8d7 --- /dev/null +++ b/internal/api/grpc/filter/v2/converter.go @@ -0,0 +1,50 @@ +package filter + +import ( + "fmt" + + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" +) + +func TimestampMethodPbToQuery(method filter.TimestampFilterMethod) query.TimestampComparison { + switch method { + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_EQUALS: + return query.TimestampEquals + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_BEFORE: + return query.TimestampLess + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER: + return query.TimestampGreater + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_BEFORE_OR_EQUALS: + return query.TimestampLessOrEquals + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS: + return query.TimestampGreaterOrEquals + default: + return -1 + } +} + +func PaginationPbToQuery(defaults systemdefaults.SystemDefaults, query *filter.PaginationRequest) (offset, limit uint64, asc bool, err error) { + limit = defaults.DefaultQueryLimit + if query == nil { + return 0, limit, asc, nil + } + offset = query.Offset + asc = query.Asc + if defaults.MaxQueryLimit > 0 && uint64(query.Limit) > defaults.MaxQueryLimit { + return 0, 0, false, zerrors.ThrowInvalidArgumentf(fmt.Errorf("given: %d, allowed: %d", query.Limit, defaults.MaxQueryLimit), "QUERY-4M0fs", "Errors.Query.LimitExceeded") + } + if query.Limit > 0 { + limit = uint64(query.Limit) + } + return offset, limit, asc, nil +} + +func QueryToPaginationPb(request query.SearchRequest, response query.SearchResponse) *filter.PaginationResponse { + return &filter.PaginationResponse{ + AppliedLimit: request.Limit, + TotalResult: response.Count, + } +} diff --git a/internal/api/grpc/management/project_application.go b/internal/api/grpc/management/project_application.go index 3a0e1d5f92..ab49905409 100644 --- a/internal/api/grpc/management/project_application.go +++ b/internal/api/grpc/management/project_application.go @@ -271,7 +271,7 @@ func (s *Server) ListAppKeys(ctx context.Context, req *mgmt_pb.ListAppKeysReques if err != nil { return nil, err } - keys, err := s.query.SearchAuthNKeys(ctx, queries, false) + keys, err := s.query.SearchAuthNKeys(ctx, queries, query.JoinFilterApp, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index f318051e63..ae1040cd1e 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -297,7 +297,7 @@ func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUs func (s *Server) AddMachineUser(ctx context.Context, req *mgmt_pb.AddMachineUserRequest) (*mgmt_pb.AddMachineUserResponse, error) { machine := AddMachineUserRequestToCommand(req, authz.GetCtxData(ctx).OrgID) - objectDetails, err := s.command.AddMachine(ctx, machine) + objectDetails, err := s.command.AddMachine(ctx, machine, nil) if err != nil { return nil, err } @@ -752,11 +752,11 @@ func (s *Server) GetMachineKeyByIDs(ctx context.Context, req *mgmt_pb.GetMachine } func (s *Server) ListMachineKeys(ctx context.Context, req *mgmt_pb.ListMachineKeysRequest) (*mgmt_pb.ListMachineKeysResponse, error) { - query, err := ListMachineKeysRequestToQuery(ctx, req) + q, err := ListMachineKeysRequestToQuery(ctx, req) if err != nil { return nil, err } - result, err := s.query.SearchAuthNKeys(ctx, query, false) + result, err := s.query.SearchAuthNKeys(ctx, q, query.JoinFilterUserMachine, nil) if err != nil { return nil, err } @@ -774,7 +774,6 @@ func (s *Server) AddMachineKey(ctx context.Context, req *mgmt_pb.AddMachineKeyRe if err != nil { return nil, err } - // Return key details only if the pubkey wasn't supplied, otherwise the user already has // private key locally var keyDetails []byte @@ -821,7 +820,7 @@ func (s *Server) GenerateMachineSecret(ctx context.Context, req *mgmt_pb.Generat } func (s *Server) RemoveMachineSecret(ctx context.Context, req *mgmt_pb.RemoveMachineSecretRequest) (*mgmt_pb.RemoveMachineSecretResponse, error) { - objectDetails, err := s.command.RemoveMachineSecret(ctx, req.UserId, authz.GetCtxData(ctx).OrgID) + objectDetails, err := s.command.RemoveMachineSecret(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, nil) if err != nil { return nil, err } @@ -839,7 +838,7 @@ func (s *Server) GetPersonalAccessTokenByIDs(ctx context.Context, req *mgmt_pb.G if err != nil { return nil, err } - token, err := s.query.PersonalAccessTokenByID(ctx, true, req.TokenId, false, resourceOwner, aggregateID) + token, err := s.query.PersonalAccessTokenByID(ctx, true, req.TokenId, resourceOwner, aggregateID) if err != nil { return nil, err } @@ -853,7 +852,7 @@ func (s *Server) ListPersonalAccessTokens(ctx context.Context, req *mgmt_pb.List if err != nil { return nil, err } - result, err := s.query.SearchPersonalAccessTokens(ctx, queries, false) + result, err := s.query.SearchPersonalAccessTokens(ctx, queries, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/project/v2beta/integration/query_test.go b/internal/api/grpc/project/v2beta/integration/query_test.go index f959bfe2f8..517f103628 100644 --- a/internal/api/grpc/project/v2beta/integration/query_test.go +++ b/internal/api/grpc/project/v2beta/integration/query_test.go @@ -168,8 +168,8 @@ func TestServer_ListProjects(t *testing.T) { orgID := instance.DefaultOrg.GetId() resp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{resp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, }, } }, @@ -188,8 +188,8 @@ func TestServer_ListProjects(t *testing.T) { orgID := instance.DefaultOrg.GetId() resp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{resp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, }, } }, @@ -208,8 +208,8 @@ func TestServer_ListProjects(t *testing.T) { orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{resp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, }, } }, @@ -232,8 +232,8 @@ func TestServer_ListProjects(t *testing.T) { req: &project.ListProjectsRequest{ Filters: []*project.ProjectSearchFilter{ {Filter: &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{"notfound"}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{"notfound"}, }, }, }, @@ -255,8 +255,8 @@ func TestServer_ListProjects(t *testing.T) { orgID := instance.DefaultOrg.GetId() response.Projects[0] = createProject(iamOwnerCtx, instance, t, orgID, false, false) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{response.Projects[0].GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{response.Projects[0].GetId()}, }, } }, @@ -317,8 +317,8 @@ func TestServer_ListProjects(t *testing.T) { response.Projects[1] = createProject(iamOwnerCtx, instance, t, orgID, true, false) response.Projects[0] = createProject(iamOwnerCtx, instance, t, orgID, false, true) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{response.Projects[0].GetId(), response.Projects[1].GetId(), response.Projects[2].GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{response.Projects[0].GetId(), response.Projects[1].GetId(), response.Projects[2].GetId()}, }, } }, @@ -349,8 +349,8 @@ func TestServer_ListProjects(t *testing.T) { resp2 := createProject(iamOwnerCtx, instance, t, orgID, true, false) resp3 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, true) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()}, }, } @@ -379,8 +379,8 @@ func TestServer_ListProjects(t *testing.T) { projectResp := createProject(iamOwnerCtx, instance, t, orgID, true, true) response.Projects[3] = projectResp request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } response.Projects[2] = createGrantedProject(iamOwnerCtx, instance, t, projectResp) @@ -416,7 +416,7 @@ func TestServer_ListProjects(t *testing.T) { response.Projects[1] = grantedProjectResp response.Projects[0] = createProject(iamOwnerCtx, instance, t, *grantedProjectResp.GrantedOrganizationId, true, true) request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectOrganizationIdFilter{ - ProjectOrganizationIdFilter: &project.ProjectOrganizationIDFilter{ProjectOrganizationId: *grantedProjectResp.GrantedOrganizationId}, + ProjectOrganizationIdFilter: &filter.IDFilter{Id: *grantedProjectResp.GrantedOrganizationId}, } }, req: &project.ListProjectsRequest{ @@ -445,7 +445,7 @@ func TestServer_ListProjects(t *testing.T) { grantedProjectResp := createGrantedProject(iamOwnerCtx, instance, t, projectResp) response.Projects[0] = createProject(iamOwnerCtx, instance, t, *grantedProjectResp.GrantedOrganizationId, true, true) request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectResourceOwnerFilter{ - ProjectResourceOwnerFilter: &project.ProjectResourceOwnerFilter{ProjectResourceOwner: *grantedProjectResp.GrantedOrganizationId}, + ProjectResourceOwnerFilter: &filter.IDFilter{Id: *grantedProjectResp.GrantedOrganizationId}, } }, req: &project.ListProjectsRequest{ @@ -513,8 +513,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{resp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, }, } }, @@ -531,8 +531,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{resp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, }, } }, @@ -550,8 +550,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), false, false) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{resp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, }, } }, @@ -574,8 +574,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { req: &project.ListProjectsRequest{ Filters: []*project.ProjectSearchFilter{ {Filter: &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{"notfound"}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{"notfound"}, }, }, }, @@ -596,8 +596,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{response.Projects[0].GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{response.Projects[0].GetId()}, }, } }, @@ -650,8 +650,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { response.Projects[1] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, false) response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, true) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{response.Projects[0].GetId(), response.Projects[1].GetId(), response.Projects[2].GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{response.Projects[0].GetId(), response.Projects[1].GetId(), response.Projects[2].GetId()}, }, } }, @@ -679,8 +679,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { projectResp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, true) response.Projects[3] = projectResp request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } response.Projects[2] = createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) @@ -715,7 +715,7 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { response.Projects[1] = grantedProjectResp response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, *grantedProjectResp.GrantedOrganizationId, true, true) request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectOrganizationIdFilter{ - ProjectOrganizationIdFilter: &project.ProjectOrganizationIDFilter{ProjectOrganizationId: *grantedProjectResp.GrantedOrganizationId}, + ProjectOrganizationIdFilter: &filter.IDFilter{Id: *grantedProjectResp.GrantedOrganizationId}, } }, req: &project.ListProjectsRequest{ @@ -743,7 +743,7 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { grantedProjectResp := createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, *grantedProjectResp.GrantedOrganizationId, true, true) request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectResourceOwnerFilter{ - ProjectResourceOwnerFilter: &project.ProjectResourceOwnerFilter{ProjectResourceOwner: *grantedProjectResp.GrantedOrganizationId}, + ProjectResourceOwnerFilter: &filter.IDFilter{Id: *grantedProjectResp.GrantedOrganizationId}, } }, req: &project.ListProjectsRequest{ @@ -770,8 +770,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { resp2 := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, false) resp3 := createProject(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), false, true) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()}, }, } @@ -882,15 +882,13 @@ func TestServer_ListProjectGrants(t *testing.T) { dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{ - ProjectGrantResourceOwnerFilter: &project.ProjectGrantResourceOwnerFilter{ - ProjectGrantResourceOwner: grantedOrg.GetOrganizationId(), - }, + ProjectGrantResourceOwnerFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()}, } instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) @@ -908,15 +906,13 @@ func TestServer_ListProjectGrants(t *testing.T) { dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{ - ProjectGrantResourceOwnerFilter: &project.ProjectGrantResourceOwnerFilter{ - ProjectGrantResourceOwner: grantedOrg.GetOrganizationId(), - }, + ProjectGrantResourceOwnerFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()}, } instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) @@ -934,8 +930,8 @@ func TestServer_ListProjectGrants(t *testing.T) { req: &project.ListProjectGrantsRequest{ Filters: []*project.ProjectGrantSearchFilter{ {Filter: &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{"notfound"}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{"notfound"}, }, }, }, @@ -958,8 +954,8 @@ func TestServer_ListProjectGrants(t *testing.T) { orgID := instance.DefaultOrg.GetId() projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } @@ -988,8 +984,8 @@ func TestServer_ListProjectGrants(t *testing.T) { orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } @@ -1016,8 +1012,8 @@ func TestServer_ListProjectGrants(t *testing.T) { orgID := instance.DefaultOrg.GetId() projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } @@ -1053,8 +1049,8 @@ func TestServer_ListProjectGrants(t *testing.T) { project2Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false) project3Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId()}, }, } @@ -1084,8 +1080,8 @@ func TestServer_ListProjectGrants(t *testing.T) { orgID := instance.DefaultOrg.GetId() projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } projectRoleResp := addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) @@ -1114,8 +1110,8 @@ func TestServer_ListProjectGrants(t *testing.T) { orgID := instance.DefaultOrg.GetId() projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } projectRoleResp := addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) @@ -1189,15 +1185,13 @@ func TestServer_ListProjectGrants_PermissionV2(t *testing.T) { dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), gofakeit.AppName(), false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } grantedOrg := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{ - ProjectGrantResourceOwnerFilter: &project.ProjectGrantResourceOwnerFilter{ - ProjectGrantResourceOwner: grantedOrg.GetOrganizationId(), - }, + ProjectGrantResourceOwnerFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()}, } instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) @@ -1215,15 +1209,13 @@ func TestServer_ListProjectGrants_PermissionV2(t *testing.T) { dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), gofakeit.AppName(), false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } grantedOrg := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{ - ProjectGrantResourceOwnerFilter: &project.ProjectGrantResourceOwnerFilter{ - ProjectGrantResourceOwner: grantedOrg.GetOrganizationId(), - }, + ProjectGrantResourceOwnerFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()}, } instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) @@ -1243,8 +1235,8 @@ func TestServer_ListProjectGrants_PermissionV2(t *testing.T) { orgID := instancePermissionV2.DefaultOrg.GetId() projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, name, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } @@ -1273,8 +1265,8 @@ func TestServer_ListProjectGrants_PermissionV2(t *testing.T) { orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } @@ -1301,8 +1293,8 @@ func TestServer_ListProjectGrants_PermissionV2(t *testing.T) { orgID := instancePermissionV2.DefaultOrg.GetId() projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, name, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } @@ -1338,8 +1330,8 @@ func TestServer_ListProjectGrants_PermissionV2(t *testing.T) { project2Resp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false) project3Resp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId()}, }, } diff --git a/internal/api/grpc/project/v2beta/query.go b/internal/api/grpc/project/v2beta/query.go index 1cdf9eefbd..42b69a480e 100644 --- a/internal/api/grpc/project/v2beta/query.go +++ b/internal/api/grpc/project/v2beta/query.go @@ -9,6 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" + filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" project_pb "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" ) @@ -109,20 +110,20 @@ func projectNameFilterToQuery(q *project_pb.ProjectNameFilter) (query.SearchQuer return query.NewGrantedProjectNameSearchQuery(filter.TextMethodPbToQuery(q.Method), q.GetProjectName()) } -func projectInIDsFilterToQuery(q *project_pb.InProjectIDsFilter) (query.SearchQuery, error) { - return query.NewGrantedProjectIDSearchQuery(q.ProjectIds) +func projectInIDsFilterToQuery(q *filter_pb.InIDsFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectIDSearchQuery(q.Ids) } -func projectResourceOwnerFilterToQuery(q *project_pb.ProjectResourceOwnerFilter) (query.SearchQuery, error) { - return query.NewGrantedProjectResourceOwnerSearchQuery(q.ProjectResourceOwner) +func projectResourceOwnerFilterToQuery(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectResourceOwnerSearchQuery(q.Id) } -func projectOrganizationIDFilterToQuery(q *project_pb.ProjectOrganizationIDFilter) (query.SearchQuery, error) { - return query.NewGrantedProjectOrganizationIDSearchQuery(q.ProjectOrganizationId) +func projectOrganizationIDFilterToQuery(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectOrganizationIDSearchQuery(q.Id) } -func projectGrantResourceOwnerFilterToQuery(q *project_pb.ProjectGrantResourceOwnerFilter) (query.SearchQuery, error) { - return query.NewGrantedProjectGrantResourceOwnerSearchQuery(q.ProjectGrantResourceOwner) +func projectGrantResourceOwnerFilterToQuery(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectGrantResourceOwnerSearchQuery(q.Id) } func grantedProjectsToPb(projects []*query.GrantedProject) []*project_pb.Project { @@ -283,11 +284,11 @@ func projectGrantFilterToModel(filter *project_pb.ProjectGrantSearchFilter) (que case *project_pb.ProjectGrantSearchFilter_RoleKeyFilter: return query.NewProjectGrantRoleKeySearchQuery(q.RoleKeyFilter.Key) case *project_pb.ProjectGrantSearchFilter_InProjectIdsFilter: - return query.NewProjectGrantProjectIDsSearchQuery(q.InProjectIdsFilter.ProjectIds) + return query.NewProjectGrantProjectIDsSearchQuery(q.InProjectIdsFilter.Ids) case *project_pb.ProjectGrantSearchFilter_ProjectResourceOwnerFilter: - return query.NewProjectGrantResourceOwnerSearchQuery(q.ProjectResourceOwnerFilter.ProjectResourceOwner) + return query.NewProjectGrantResourceOwnerSearchQuery(q.ProjectResourceOwnerFilter.Id) case *project_pb.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter: - return query.NewProjectGrantGrantedOrgIDSearchQuery(q.ProjectGrantResourceOwnerFilter.ProjectGrantResourceOwner) + return query.NewProjectGrantGrantedOrgIDSearchQuery(q.ProjectGrantResourceOwnerFilter.Id) default: return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-M099f", "List.Query.Invalid") } diff --git a/internal/api/grpc/user/v2/human.go b/internal/api/grpc/user/v2/human.go new file mode 100644 index 0000000000..d8a0891396 --- /dev/null +++ b/internal/api/grpc/user/v2/human.go @@ -0,0 +1,187 @@ +package user + +import ( + "context" + "io" + + "golang.org/x/text/language" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/zerrors" + legacyobject "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) createUserTypeHuman(ctx context.Context, humanPb *user.CreateUserRequest_Human, orgId string, userName, userId *string) (*user.CreateUserResponse, error) { + addHumanPb := &user.AddHumanUserRequest{ + Username: userName, + UserId: userId, + Organization: &legacyobject.Organization{ + Org: &legacyobject.Organization_OrgId{OrgId: orgId}, + }, + Profile: humanPb.Profile, + Email: humanPb.Email, + Phone: humanPb.Phone, + IdpLinks: humanPb.IdpLinks, + TotpSecret: humanPb.TotpSecret, + } + switch pwType := humanPb.GetPasswordType().(type) { + case *user.CreateUserRequest_Human_HashedPassword: + addHumanPb.PasswordType = &user.AddHumanUserRequest_HashedPassword{ + HashedPassword: pwType.HashedPassword, + } + case *user.CreateUserRequest_Human_Password: + addHumanPb.PasswordType = &user.AddHumanUserRequest_Password{ + Password: pwType.Password, + } + default: + // optional password is not set + } + newHuman, err := AddUserRequestToAddHuman(addHumanPb) + if err != nil { + return nil, err + } + if err = s.command.AddUserHuman( + ctx, + orgId, + newHuman, + false, + s.userCodeAlg, + ); err != nil { + return nil, err + } + return &user.CreateUserResponse{ + Id: newHuman.ID, + CreationDate: timestamppb.New(newHuman.Details.EventDate), + EmailCode: newHuman.EmailCode, + PhoneCode: newHuman.PhoneCode, + }, nil +} + +func (s *Server) updateUserTypeHuman(ctx context.Context, humanPb *user.UpdateUserRequest_Human, userId string, userName *string) (*user.UpdateUserResponse, error) { + cmd, err := updateHumanUserToCommand(userId, userName, humanPb) + if err != nil { + return nil, err + } + if err = s.command.ChangeUserHuman(ctx, cmd, s.userCodeAlg); err != nil { + return nil, err + } + return &user.UpdateUserResponse{ + ChangeDate: timestamppb.New(cmd.Details.EventDate), + EmailCode: cmd.EmailCode, + PhoneCode: cmd.PhoneCode, + }, nil +} + +func updateHumanUserToCommand(userId string, userName *string, human *user.UpdateUserRequest_Human) (*command.ChangeHuman, error) { + phone := human.GetPhone() + if phone != nil && phone.Phone == "" && phone.GetVerification() != nil { + return nil, zerrors.ThrowInvalidArgument(nil, "USERv2-4f3d6", "Errors.User.Phone.VerifyingRemovalIsNotSupported") + } + email, err := setHumanEmailToEmail(human.Email, userId) + if err != nil { + return nil, err + } + return &command.ChangeHuman{ + ID: userId, + Username: userName, + Profile: SetHumanProfileToProfile(human.Profile), + Email: email, + Phone: setHumanPhoneToPhone(human.Phone, true), + Password: setHumanPasswordToPassword(human.Password), + }, nil +} + +func updateHumanUserRequestToChangeHuman(req *user.UpdateHumanUserRequest) (*command.ChangeHuman, error) { + email, err := setHumanEmailToEmail(req.Email, req.GetUserId()) + if err != nil { + return nil, err + } + changeHuman := &command.ChangeHuman{ + ID: req.GetUserId(), + Username: req.Username, + Email: email, + Phone: setHumanPhoneToPhone(req.Phone, false), + Password: setHumanPasswordToPassword(req.Password), + } + if profile := req.GetProfile(); profile != nil { + var firstName *string + if profile.GivenName != "" { + firstName = &profile.GivenName + } + var lastName *string + if profile.FamilyName != "" { + lastName = &profile.FamilyName + } + changeHuman.Profile = SetHumanProfileToProfile(&user.UpdateUserRequest_Human_Profile{ + GivenName: firstName, + FamilyName: lastName, + NickName: profile.NickName, + DisplayName: profile.DisplayName, + PreferredLanguage: profile.PreferredLanguage, + Gender: profile.Gender, + }) + } + return changeHuman, nil +} + +func SetHumanProfileToProfile(profile *user.UpdateUserRequest_Human_Profile) *command.Profile { + if profile == nil { + return nil + } + return &command.Profile{ + FirstName: profile.GivenName, + LastName: profile.FamilyName, + NickName: profile.NickName, + DisplayName: profile.DisplayName, + PreferredLanguage: ifNotNilPtr(profile.PreferredLanguage, language.Make), + Gender: ifNotNilPtr(profile.Gender, genderToDomain), + } +} + +func setHumanEmailToEmail(email *user.SetHumanEmail, userID string) (*command.Email, error) { + if email == nil { + return nil, nil + } + var urlTemplate string + if email.GetSendCode() != nil && email.GetSendCode().UrlTemplate != nil { + urlTemplate = *email.GetSendCode().UrlTemplate + if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, userID, "code", "orgID"); err != nil { + return nil, err + } + } + return &command.Email{ + Address: domain.EmailAddress(email.Email), + Verified: email.GetIsVerified(), + ReturnCode: email.GetReturnCode() != nil, + URLTemplate: urlTemplate, + }, nil +} + +func setHumanPhoneToPhone(phone *user.SetHumanPhone, withRemove bool) *command.Phone { + if phone == nil { + return nil + } + number := phone.GetPhone() + return &command.Phone{ + Number: domain.PhoneNumber(number), + Verified: phone.GetIsVerified(), + ReturnCode: phone.GetReturnCode() != nil, + Remove: withRemove && number == "", + } +} + +func setHumanPasswordToPassword(password *user.SetPassword) *command.Password { + if password == nil { + return nil + } + return &command.Password{ + PasswordCode: password.GetVerificationCode(), + OldPassword: password.GetCurrentPassword(), + Password: password.GetPassword().GetPassword(), + EncodedPasswordHash: password.GetHashedPassword().GetHash(), + ChangeRequired: password.GetPassword().GetChangeRequired() || password.GetHashedPassword().GetChangeRequired(), + } +} diff --git a/internal/api/grpc/user/v2/human_test.go b/internal/api/grpc/user/v2/human_test.go new file mode 100644 index 0000000000..52e5371dcc --- /dev/null +++ b/internal/api/grpc/user/v2/human_test.go @@ -0,0 +1,254 @@ +package user + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func Test_patchHumanUserToCommand(t *testing.T) { + type args struct { + userId string + userName *string + human *user.UpdateUserRequest_Human + } + tests := []struct { + name string + args args + want *command.ChangeHuman + wantErr assert.ErrorAssertionFunc + }{{ + name: "single property", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("givenName"), + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Profile: &command.Profile{ + FirstName: gu.Ptr("givenName"), + }, + }, + wantErr: assert.NoError, + }, { + name: "all properties", + args: args{ + userId: "userId", + userName: gu.Ptr("userName"), + human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("givenName"), + FamilyName: gu.Ptr("familyName"), + NickName: gu.Ptr("nickName"), + DisplayName: gu.Ptr("displayName"), + PreferredLanguage: gu.Ptr("en-US"), + Gender: gu.Ptr(user.Gender_GENDER_FEMALE), + }, + Email: &user.SetHumanEmail{ + Email: "email@example.com", + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + Phone: &user.SetHumanPhone{ + Phone: "+123456789", + Verification: &user.SetHumanPhone_IsVerified{ + IsVerified: true, + }, + }, + Password: &user.SetPassword{ + Verification: &user.SetPassword_CurrentPassword{ + CurrentPassword: "currentPassword", + }, + PasswordType: &user.SetPassword_Password{ + Password: &user.Password{ + Password: "newPassword", + ChangeRequired: true, + }, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Username: gu.Ptr("userName"), + Profile: &command.Profile{ + FirstName: gu.Ptr("givenName"), + LastName: gu.Ptr("familyName"), + NickName: gu.Ptr("nickName"), + DisplayName: gu.Ptr("displayName"), + PreferredLanguage: &language.AmericanEnglish, + Gender: gu.Ptr(domain.GenderFemale), + }, + Email: &command.Email{ + Address: "email@example.com", + Verified: true, + }, + Phone: &command.Phone{ + Number: "+123456789", + Verified: true, + }, + Password: &command.Password{ + OldPassword: "currentPassword", + Password: "newPassword", + ChangeRequired: true, + }, + }, + wantErr: assert.NoError, + }, { + name: "set email and request code", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: "email@example.com", + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Email: &command.Email{ + Address: "email@example.com", + ReturnCode: true, + }, + }, + wantErr: assert.NoError, + }, { + name: "set email and send code", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: "email@example.com", + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{}, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Email: &command.Email{ + Address: "email@example.com", + }, + }, + wantErr: assert.NoError, + }, { + name: "set email and send code with template", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: "email@example.com", + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("Code: {{.Code}}"), + }, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Email: &command.Email{ + Address: "email@example.com", + URLTemplate: "Code: {{.Code}}", + }, + }, + wantErr: assert.NoError, + }, { + name: "set phone and request code", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{ + Phone: "+123456789", + Verification: &user.SetHumanPhone_ReturnCode{ + ReturnCode: &user.ReturnPhoneVerificationCode{}, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Phone: &command.Phone{ + Number: "+123456789", + ReturnCode: true, + }, + }, + wantErr: assert.NoError, + }, { + name: "set phone and send code", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{ + Phone: "+123456789", + Verification: &user.SetHumanPhone_SendCode{ + SendCode: &user.SendPhoneVerificationCode{}, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Phone: &command.Phone{ + Number: "+123456789", + }, + }, + wantErr: assert.NoError, + }, { + name: "remove phone, ok", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{}, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Phone: &command.Phone{ + Remove: true, + }, + }, + wantErr: assert.NoError, + }, { + name: "remove phone with verification, error", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{ + Verification: &user.SetHumanPhone_ReturnCode{}, + }, + }, + }, + wantErr: assert.Error, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := updateHumanUserToCommand(tt.args.userId, tt.args.userName, tt.args.human) + if !tt.wantErr(t, err, fmt.Sprintf("patchHumanUserToCommand(%v, %v, %v)", tt.args.userId, tt.args.userName, tt.args.human)) { + return + } + if diff := cmp.Diff(tt.want, got, cmpopts.EquateComparable(language.Tag{})); diff != "" { + t.Errorf("patchHumanUserToCommand() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/api/grpc/user/v2/integration_test/email_test.go b/internal/api/grpc/user/v2/integration_test/email_test.go index ad63c2ce5e..ad68ef5c5a 100644 --- a/internal/api/grpc/user/v2/integration_test/email_test.go +++ b/internal/api/grpc/user/v2/integration_test/email_test.go @@ -10,13 +10,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" - + "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/object/v2" "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) -func TestServer_SetEmail(t *testing.T) { +func TestServer_Deprecated_SetEmail(t *testing.T) { userID := Instance.CreateHumanUser(CTX).GetUserId() tests := []struct { diff --git a/internal/api/grpc/user/v2/integration_test/key_test.go b/internal/api/grpc/user/v2/integration_test/key_test.go new file mode 100644 index 0000000000..e85903b2cb --- /dev/null +++ b/internal/api/grpc/user/v2/integration_test/key_test.go @@ -0,0 +1,659 @@ +//go:build integration + +package user_test + +import ( + "context" + "fmt" + "slices" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func TestServer_AddKey(t *testing.T) { + resp := Instance.CreateUserTypeMachine(IamCTX) + userId := resp.GetId() + expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24)) + type args struct { + req *user.AddKeyRequest + prepare func(request *user.AddKeyRequest) error + } + tests := []struct { + name string + args args + wantErr bool + wantEmtpyKey bool + }{ + { + name: "add key, user not existing", + args: args{ + &user.AddKeyRequest{ + UserId: "notexisting", + ExpirationDate: expirationDate, + }, + func(request *user.AddKeyRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "generate key pair, ok", + args: args{ + &user.AddKeyRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddKeyRequest) error { + request.UserId = userId + return nil + }, + }, + }, + { + name: "add valid public key, ok", + args: args{ + &user.AddKeyRequest{ + ExpirationDate: expirationDate, + // This is the public key of the tester system user. This must be valid. + PublicKey: []byte(` +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzi+FFSJL7f5yw4KTwzgM +P34ePGycm/M+kT0M7V4Cgx5V3EaDIvTQKTLfBaEB45zb9LtjIXzDw0rXRoS2hO6t +h+CYQCz3KCvh09C0IzxZiB2IS3H/aT+5Bx9EFY+vnAkZjccbyG5YNRvmtOlnvIeI +H7qZ0tEwkPfF5GEZNPJPtmy3UGV7iofdVQS1xRj73+aMw5rvH4D8IdyiAC3VekIb +pt0Vj0SUX3DwKtog337BzTiPk3aXRF0sbFhQoqdJRI8NqgZjCwjq9yfI5tyxYswn ++JGzHGdHvW3idODlmwEt5K2pasiRIWK2OGfq+w0EcltQHabuqEPgZlmhCkRdNfix +BwIDAQAB +-----END PUBLIC KEY----- +`), + }, + func(request *user.AddKeyRequest) error { + request.UserId = userId + return nil + }, + }, + wantEmtpyKey: true, + }, + { + name: "add invalid public key, error", + args: args{ + &user.AddKeyRequest{ + ExpirationDate: expirationDate, + PublicKey: []byte(` +-----BEGIN PUBLIC KEY----- +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 +-----END PUBLIC KEY----- +`), + }, + func(request *user.AddKeyRequest) error { + request.UserId = userId + return nil + }, + }, + wantErr: true, + }, + { + name: "add key human, error", + args: args{ + &user.AddKeyRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddKeyRequest) error { + resp := Instance.CreateUserTypeHuman(IamCTX) + request.UserId = resp.Id + return nil + }, + }, + wantErr: true, + }, + { + name: "add another key, ok", + args: args{ + &user.AddKeyRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddKeyRequest) error { + request.UserId = userId + _, err := Client.AddKey(IamCTX, &user.AddKeyRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.AddKey(CTX, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.KeyId, "key id is empty") + if tt.wantEmtpyKey { + assert.Empty(t, got.KeyContent, "key content is not empty") + } else { + assert.NotEmpty(t, got.KeyContent, "key content is empty") + } + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_AddKey_Permission(t *testing.T) { + OrgCTX := CTX + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddKey-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + request := &user.AddKeyRequest{ + ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)), + UserId: otherOrgUser.GetId(), + } + type args struct { + ctx context.Context + req *user.AddKeyRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{SystemCTX, request}, + }, + { + name: "instance, ok", + args: args{IamCTX, request}, + }, + { + name: "org, error", + args: args{OrgCTX, request}, + wantErr: true, + }, + { + name: "user, error", + args: args{UserCTX, request}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, err) + got, err := Client.AddKey(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.KeyId, "key id is empty") + assert.NotEmpty(t, got.KeyContent, "key content is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemoveKey(t *testing.T) { + resp := Instance.CreateUserTypeMachine(IamCTX) + userId := resp.GetId() + expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24)) + type args struct { + req *user.RemoveKeyRequest + prepare func(request *user.RemoveKeyRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "remove key, user not existing", + args: args{ + &user.RemoveKeyRequest{ + UserId: "notexisting", + }, + func(request *user.RemoveKeyRequest) error { + key, err := Client.AddKey(IamCTX, &user.AddKeyRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + request.KeyId = key.GetKeyId() + return err + }, + }, + wantErr: true, + }, + { + name: "remove key, not existing", + args: args{ + &user.RemoveKeyRequest{ + KeyId: "notexisting", + }, + func(request *user.RemoveKeyRequest) error { + request.UserId = userId + return nil + }, + }, + wantErr: true, + }, + { + name: "remove key, ok", + args: args{ + &user.RemoveKeyRequest{}, + func(request *user.RemoveKeyRequest) error { + key, err := Client.AddKey(IamCTX, &user.AddKeyRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + request.KeyId = key.GetKeyId() + request.UserId = userId + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.RemoveKey(CTX, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + deletionDate := got.DeletionDate.AsTime() + assert.Greater(t, deletionDate, now, "creation date is before the test started") + assert.Less(t, deletionDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemoveKey_Permission(t *testing.T) { + OrgCTX := CTX + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("RemoveKey-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + request := &user.RemoveKeyRequest{ + UserId: otherOrgUser.GetId(), + } + prepare := func(request *user.RemoveKeyRequest) error { + key, err := Client.AddKey(IamCTX, &user.AddKeyRequest{ + ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)), + UserId: otherOrgUser.GetId(), + }) + request.KeyId = key.GetKeyId() + return err + } + require.NoError(t, err) + type args struct { + ctx context.Context + req *user.RemoveKeyRequest + prepare func(request *user.RemoveKeyRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{SystemCTX, request, prepare}, + }, + { + name: "instance, ok", + args: args{IamCTX, request, prepare}, + }, + { + name: "org, error", + args: args{OrgCTX, request, prepare}, + wantErr: true, + }, + { + name: "user, error", + args: args{UserCTX, request, prepare}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, tt.args.prepare(tt.args.req)) + got, err := Client.RemoveKey(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.DeletionDate, "client key is empty") + creationDate := got.DeletionDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_ListKeys(t *testing.T) { + type args struct { + ctx context.Context + req *user.ListKeysRequest + } + type testCase struct { + name string + args args + want *user.ListKeysResponse + } + OrgCTX := CTX + otherOrg := Instance.CreateOrganization(SystemCTX, fmt.Sprintf("ListKeys-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(SystemCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + otherOrgUserId := otherOrgUser.GetId() + otherUserId := Instance.CreateUserTypeMachine(SystemCTX).GetId() + onlySinceTestStartFilter := &user.KeysSearchFilter{Filter: &user.KeysSearchFilter_CreatedDateFilter{CreatedDateFilter: &filter.TimestampFilter{ + Timestamp: timestamppb.Now(), + Method: filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS, + }}} + myOrgId := Instance.DefaultOrg.GetId() + myUserId := Instance.Users.Get(integration.UserTypeNoPermission).ID + expiresInADay := time.Now().Truncate(time.Hour).Add(time.Hour * 24) + myDataPoint := setupKeyDataPoint(t, myUserId, myOrgId, expiresInADay) + otherUserDataPoint := setupKeyDataPoint(t, otherUserId, myOrgId, expiresInADay) + otherOrgDataPointExpiringSoon := setupKeyDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, time.Now().Truncate(time.Hour).Add(time.Hour)) + otherOrgDataPointExpiringLate := setupKeyDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, expiresInADay.Add(time.Hour*24*30)) + sortingColumnExpirationDate := user.KeyFieldName_KEY_FIELD_NAME_KEY_EXPIRATION_DATE + awaitKeys(t, onlySinceTestStartFilter, + otherOrgDataPointExpiringSoon.GetId(), + otherOrgDataPointExpiringLate.GetId(), + otherUserDataPoint.GetId(), + myDataPoint.GetId(), + ) + tests := []testCase{ + { + name: "list all, instance", + args: args{ + IamCTX, + &user.ListKeysRequest{Filters: []*user.KeysSearchFilter{onlySinceTestStartFilter}}, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherOrgDataPointExpiringLate, + otherOrgDataPointExpiringSoon, + otherUserDataPoint, + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all, org", + args: args{ + OrgCTX, + &user.ListKeysRequest{Filters: []*user.KeysSearchFilter{onlySinceTestStartFilter}}, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherUserDataPoint, + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all, user", + args: args{ + UserCTX, + &user.ListKeysRequest{Filters: []*user.KeysSearchFilter{onlySinceTestStartFilter}}, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + }, + }, + { + name: "list by id", + args: args{ + IamCTX, + &user.ListKeysRequest{ + Filters: []*user.KeysSearchFilter{ + onlySinceTestStartFilter, + { + Filter: &user.KeysSearchFilter_KeyIdFilter{ + KeyIdFilter: &filter.IDFilter{Id: otherOrgDataPointExpiringSoon.Id}, + }, + }, + }, + }, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherOrgDataPointExpiringSoon, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all from other org", + args: args{ + IamCTX, + &user.ListKeysRequest{ + Filters: []*user.KeysSearchFilter{ + onlySinceTestStartFilter, + { + Filter: &user.KeysSearchFilter_OrganizationIdFilter{ + OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId}, + }, + }, + }, + }, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherOrgDataPointExpiringLate, + otherOrgDataPointExpiringSoon, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "sort by next expiration dates", + args: args{ + IamCTX, + &user.ListKeysRequest{ + Pagination: &filter.PaginationRequest{ + Asc: true, + }, + SortingColumn: &sortingColumnExpirationDate, + Filters: []*user.KeysSearchFilter{ + onlySinceTestStartFilter, + {Filter: &user.KeysSearchFilter_OrganizationIdFilter{OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId}}}, + }, + }, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherOrgDataPointExpiringSoon, + otherOrgDataPointExpiringLate, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "get page", + args: args{ + IamCTX, + &user.ListKeysRequest{ + Pagination: &filter.PaginationRequest{ + Offset: 2, + Limit: 2, + Asc: true, + }, + Filters: []*user.KeysSearchFilter{ + onlySinceTestStartFilter, + }, + }, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherOrgDataPointExpiringSoon, + otherOrgDataPointExpiringLate, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 2, + }, + }, + }, + { + name: "empty list", + args: args{ + UserCTX, + &user.ListKeysRequest{ + Filters: []*user.KeysSearchFilter{ + { + Filter: &user.KeysSearchFilter_KeyIdFilter{ + KeyIdFilter: &filter.IDFilter{Id: otherUserDataPoint.Id}, + }, + }, + }, + }, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{}, + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + } + t.Run("with permission flag v2", func(t *testing.T) { + setPermissionCheckV2Flag(t, true) + defer setPermissionCheckV2Flag(t, false) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ListKeys(tt.args.ctx, tt.args.req) + require.NoError(t, err) + assert.Len(t, got.Result, len(tt.want.Result)) + if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ListKeys() mismatch (-want +got):\n%s", diff) + } + }) + } + }) + t.Run("without permission flag v2", func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ListKeys(tt.args.ctx, tt.args.req) + require.NoError(t, err) + assert.Len(t, got.Result, len(tt.want.Result)) + // ignore the total result, as this is a known bug with the in-memory permission checks. + // The command can't know how many keys exist in the system if the SQL statement has a limit. + // This is fixed, once the in-memory permission checks are removed with https://github.com/zitadel/zitadel/issues/9188 + tt.want.Pagination.TotalResult = got.Pagination.TotalResult + if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ListKeys() mismatch (-want +got):\n%s", diff) + } + }) + } + }) +} + +func setupKeyDataPoint(t *testing.T, userId, orgId string, expirationDate time.Time) *user.Key { + expirationDatePb := timestamppb.New(expirationDate) + newKey, err := Client.AddKey(SystemCTX, &user.AddKeyRequest{ + UserId: userId, + ExpirationDate: expirationDatePb, + PublicKey: nil, + }) + require.NoError(t, err) + return &user.Key{ + CreationDate: newKey.CreationDate, + ChangeDate: newKey.CreationDate, + Id: newKey.GetKeyId(), + UserId: userId, + OrganizationId: orgId, + ExpirationDate: expirationDatePb, + } +} + +func awaitKeys(t *testing.T, sinceTestStartFilter *user.KeysSearchFilter, keyIds ...string) { + sortingColumn := user.KeyFieldName_KEY_FIELD_NAME_ID + slices.Sort(keyIds) + require.EventuallyWithT(t, func(collect *assert.CollectT) { + result, err := Client.ListKeys(SystemCTX, &user.ListKeysRequest{ + Filters: []*user.KeysSearchFilter{sinceTestStartFilter}, + SortingColumn: &sortingColumn, + Pagination: &filter.PaginationRequest{ + Asc: true, + }, + }) + require.NoError(t, err) + if !assert.Len(collect, result.Result, len(keyIds)) { + return + } + for i := range keyIds { + keyId := keyIds[i] + require.Equal(collect, keyId, result.Result[i].GetId()) + } + }, 5*time.Second, time.Second, "key not created in time") +} diff --git a/internal/api/grpc/user/v2/integration_test/password_test.go b/internal/api/grpc/user/v2/integration_test/password_test.go index 0cd0da7454..258cdaf78d 100644 --- a/internal/api/grpc/user/v2/integration_test/password_test.go +++ b/internal/api/grpc/user/v2/integration_test/password_test.go @@ -104,7 +104,7 @@ func TestServer_RequestPasswordReset(t *testing.T) { } } -func TestServer_SetPassword(t *testing.T) { +func TestServer_Deprecated_SetPassword(t *testing.T) { type args struct { ctx context.Context req *user.SetPasswordRequest diff --git a/internal/api/grpc/user/v2/integration_test/pat_test.go b/internal/api/grpc/user/v2/integration_test/pat_test.go new file mode 100644 index 0000000000..ce974e0407 --- /dev/null +++ b/internal/api/grpc/user/v2/integration_test/pat_test.go @@ -0,0 +1,615 @@ +//go:build integration + +package user_test + +import ( + "context" + "fmt" + "slices" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func TestServer_AddPersonalAccessToken(t *testing.T) { + resp := Instance.CreateUserTypeMachine(IamCTX) + userId := resp.GetId() + expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24)) + type args struct { + req *user.AddPersonalAccessTokenRequest + prepare func(request *user.AddPersonalAccessTokenRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "add pat, user not existing", + args: args{ + &user.AddPersonalAccessTokenRequest{ + UserId: "notexisting", + ExpirationDate: expirationDate, + }, + func(request *user.AddPersonalAccessTokenRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "add pat, ok", + args: args{ + &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddPersonalAccessTokenRequest) error { + request.UserId = userId + return nil + }, + }, + }, + { + name: "add pat human, not ok", + args: args{ + &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddPersonalAccessTokenRequest) error { + resp := Instance.CreateUserTypeHuman(IamCTX) + request.UserId = resp.Id + return nil + }, + }, + wantErr: true, + }, + { + name: "add another pat, ok", + args: args{ + &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddPersonalAccessTokenRequest) error { + request.UserId = userId + _, err := Client.AddPersonalAccessToken(IamCTX, &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.AddPersonalAccessToken(CTX, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.TokenId, "id is empty") + assert.NotEmpty(t, got.Token, "token is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_AddPersonalAccessToken_Permission(t *testing.T) { + OrgCTX := CTX + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddPersonalAccessToken-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + request := &user.AddPersonalAccessTokenRequest{ + ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)), + UserId: otherOrgUser.GetId(), + } + type args struct { + ctx context.Context + req *user.AddPersonalAccessTokenRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{SystemCTX, request}, + }, + { + name: "instance, ok", + args: args{IamCTX, request}, + }, + { + name: "org, error", + args: args{OrgCTX, request}, + wantErr: true, + }, + { + name: "user, error", + args: args{UserCTX, request}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, err) + got, err := Client.AddPersonalAccessToken(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.TokenId, "id is empty") + assert.NotEmpty(t, got.Token, "token is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemovePersonalAccessToken(t *testing.T) { + resp := Instance.CreateUserTypeMachine(IamCTX) + userId := resp.GetId() + expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24)) + type args struct { + req *user.RemovePersonalAccessTokenRequest + prepare func(request *user.RemovePersonalAccessTokenRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "remove pat, user not existing", + args: args{ + &user.RemovePersonalAccessTokenRequest{ + UserId: "notexisting", + }, + func(request *user.RemovePersonalAccessTokenRequest) error { + pat, err := Client.AddPersonalAccessToken(CTX, &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + request.TokenId = pat.GetTokenId() + return err + }, + }, + wantErr: true, + }, + { + name: "remove pat, not existing", + args: args{ + &user.RemovePersonalAccessTokenRequest{ + TokenId: "notexisting", + }, + func(request *user.RemovePersonalAccessTokenRequest) error { + request.UserId = userId + return nil + }, + }, + wantErr: true, + }, + { + name: "remove pat, ok", + args: args{ + &user.RemovePersonalAccessTokenRequest{}, + func(request *user.RemovePersonalAccessTokenRequest) error { + pat, err := Client.AddPersonalAccessToken(CTX, &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + request.TokenId = pat.GetTokenId() + request.UserId = userId + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.RemovePersonalAccessToken(CTX, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + deletionDate := got.DeletionDate.AsTime() + assert.Greater(t, deletionDate, now, "creation date is before the test started") + assert.Less(t, deletionDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemovePersonalAccessToken_Permission(t *testing.T) { + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("RemovePersonalAccessToken-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + request := &user.RemovePersonalAccessTokenRequest{ + UserId: otherOrgUser.GetId(), + } + prepare := func(request *user.RemovePersonalAccessTokenRequest) error { + pat, err := Client.AddPersonalAccessToken(IamCTX, &user.AddPersonalAccessTokenRequest{ + ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)), + UserId: otherOrgUser.GetId(), + }) + request.TokenId = pat.GetTokenId() + return err + } + require.NoError(t, err) + type args struct { + ctx context.Context + req *user.RemovePersonalAccessTokenRequest + prepare func(request *user.RemovePersonalAccessTokenRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{SystemCTX, request, prepare}, + }, + { + name: "instance, ok", + args: args{IamCTX, request, prepare}, + }, + { + name: "org, error", + args: args{CTX, request, prepare}, + wantErr: true, + }, + { + name: "user, error", + args: args{UserCTX, request, prepare}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, tt.args.prepare(tt.args.req)) + got, err := Client.RemovePersonalAccessToken(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.DeletionDate, "client pat is empty") + creationDate := got.DeletionDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_ListPersonalAccessTokens(t *testing.T) { + type args struct { + ctx context.Context + req *user.ListPersonalAccessTokensRequest + } + type testCase struct { + name string + args args + want *user.ListPersonalAccessTokensResponse + } + OrgCTX := CTX + otherOrg := Instance.CreateOrganization(SystemCTX, fmt.Sprintf("ListPersonalAccessTokens-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(SystemCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + otherOrgUserId := otherOrgUser.GetId() + otherUserId := Instance.CreateUserTypeMachine(SystemCTX).GetId() + onlySinceTestStartFilter := &user.PersonalAccessTokensSearchFilter{Filter: &user.PersonalAccessTokensSearchFilter_CreatedDateFilter{CreatedDateFilter: &filter.TimestampFilter{ + Timestamp: timestamppb.Now(), + Method: filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS, + }}} + myOrgId := Instance.DefaultOrg.GetId() + myUserId := Instance.Users.Get(integration.UserTypeNoPermission).ID + expiresInADay := time.Now().Truncate(time.Hour).Add(time.Hour * 24) + myDataPoint := setupPATDataPoint(t, myUserId, myOrgId, expiresInADay) + otherUserDataPoint := setupPATDataPoint(t, otherUserId, myOrgId, expiresInADay) + otherOrgDataPointExpiringSoon := setupPATDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, time.Now().Truncate(time.Hour).Add(time.Hour)) + otherOrgDataPointExpiringLate := setupPATDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, expiresInADay.Add(time.Hour*24*30)) + sortingColumnExpirationDate := user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_EXPIRATION_DATE + awaitPersonalAccessTokens(t, + onlySinceTestStartFilter, + otherOrgDataPointExpiringSoon.GetId(), + otherOrgDataPointExpiringLate.GetId(), + otherUserDataPoint.GetId(), + myDataPoint.GetId(), + ) + tests := []testCase{ + { + name: "list all, instance", + args: args{ + IamCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{onlySinceTestStartFilter}, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherOrgDataPointExpiringLate, + otherOrgDataPointExpiringSoon, + otherUserDataPoint, + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all, org", + args: args{ + OrgCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{onlySinceTestStartFilter}, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherUserDataPoint, + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all, user", + args: args{ + UserCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{onlySinceTestStartFilter}, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + }, + }, + { + name: "list by id", + args: args{ + IamCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{ + onlySinceTestStartFilter, + { + Filter: &user.PersonalAccessTokensSearchFilter_TokenIdFilter{ + TokenIdFilter: &filter.IDFilter{Id: otherOrgDataPointExpiringSoon.Id}, + }, + }, + }, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherOrgDataPointExpiringSoon, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all from other org", + args: args{ + IamCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{ + onlySinceTestStartFilter, + { + Filter: &user.PersonalAccessTokensSearchFilter_OrganizationIdFilter{ + OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId}, + }, + }}, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherOrgDataPointExpiringLate, + otherOrgDataPointExpiringSoon, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "sort by next expiration dates", + args: args{ + IamCTX, + &user.ListPersonalAccessTokensRequest{ + Pagination: &filter.PaginationRequest{ + Asc: true, + }, + SortingColumn: &sortingColumnExpirationDate, + Filters: []*user.PersonalAccessTokensSearchFilter{ + onlySinceTestStartFilter, + {Filter: &user.PersonalAccessTokensSearchFilter_OrganizationIdFilter{OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId}}}, + }, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherOrgDataPointExpiringSoon, + otherOrgDataPointExpiringLate, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "get page", + args: args{ + IamCTX, + &user.ListPersonalAccessTokensRequest{ + Pagination: &filter.PaginationRequest{ + Offset: 2, + Limit: 2, + Asc: true, + }, + Filters: []*user.PersonalAccessTokensSearchFilter{ + onlySinceTestStartFilter, + }, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherOrgDataPointExpiringSoon, + otherOrgDataPointExpiringLate, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 2, + }, + }, + }, + { + name: "empty list", + args: args{ + UserCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{ + { + Filter: &user.PersonalAccessTokensSearchFilter_TokenIdFilter{ + TokenIdFilter: &filter.IDFilter{Id: otherUserDataPoint.Id}, + }, + }, + }, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{}, + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + } + t.Run("with permission flag v2", func(t *testing.T) { + setPermissionCheckV2Flag(t, true) + defer setPermissionCheckV2Flag(t, false) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ListPersonalAccessTokens(tt.args.ctx, tt.args.req) + require.NoError(t, err) + assert.Len(t, got.Result, len(tt.want.Result)) + if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ListPersonalAccessTokens() mismatch (-want +got):\n%s", diff) + } + }) + } + }) + t.Run("without permission flag v2", func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ListPersonalAccessTokens(tt.args.ctx, tt.args.req) + require.NoError(t, err) + assert.Len(t, got.Result, len(tt.want.Result)) + // ignore the total result, as this is a known bug with the in-memory permission checks. + // The command can't know how many keys exist in the system if the SQL statement has a limit. + // This is fixed, once the in-memory permission checks are removed with https://github.com/zitadel/zitadel/issues/9188 + tt.want.Pagination.TotalResult = got.Pagination.TotalResult + if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ListPersonalAccessTokens() mismatch (-want +got):\n%s", diff) + } + }) + } + }) +} + +func setupPATDataPoint(t *testing.T, userId, orgId string, expirationDate time.Time) *user.PersonalAccessToken { + expirationDatePb := timestamppb.New(expirationDate) + newPersonalAccessToken, err := Client.AddPersonalAccessToken(SystemCTX, &user.AddPersonalAccessTokenRequest{ + UserId: userId, + ExpirationDate: expirationDatePb, + }) + require.NoError(t, err) + return &user.PersonalAccessToken{ + CreationDate: newPersonalAccessToken.CreationDate, + ChangeDate: newPersonalAccessToken.CreationDate, + Id: newPersonalAccessToken.GetTokenId(), + UserId: userId, + OrganizationId: orgId, + ExpirationDate: expirationDatePb, + } +} + +func awaitPersonalAccessTokens(t *testing.T, sinceTestStartFilter *user.PersonalAccessTokensSearchFilter, patIds ...string) { + sortingColumn := user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_ID + slices.Sort(patIds) + require.EventuallyWithT(t, func(collect *assert.CollectT) { + result, err := Client.ListPersonalAccessTokens(SystemCTX, &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{sinceTestStartFilter}, + SortingColumn: &sortingColumn, + Pagination: &filter.PaginationRequest{ + Asc: true, + }, + }) + require.NoError(t, err) + if !assert.Len(collect, result.Result, len(patIds)) { + return + } + for i := range patIds { + patId := patIds[i] + require.Equal(collect, patId, result.Result[i].GetId()) + } + }, 5*time.Second, time.Second, "pat not created in time") +} diff --git a/internal/api/grpc/user/v2/integration_test/phone_test.go b/internal/api/grpc/user/v2/integration_test/phone_test.go index 49050c5fe6..b87f9a9f28 100644 --- a/internal/api/grpc/user/v2/integration_test/phone_test.go +++ b/internal/api/grpc/user/v2/integration_test/phone_test.go @@ -17,7 +17,7 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) -func TestServer_SetPhone(t *testing.T) { +func TestServer_Deprecated_SetPhone(t *testing.T) { userID := Instance.CreateHumanUser(CTX).GetUserId() tests := []struct { @@ -249,7 +249,7 @@ func TestServer_VerifyPhone(t *testing.T) { } } -func TestServer_RemovePhone(t *testing.T) { +func TestServer_Deprecated_RemovePhone(t *testing.T) { userResp := Instance.CreateHumanUser(CTX) failResp := Instance.CreateHumanUserNoPhone(CTX) otherUser := Instance.CreateHumanUser(CTX).GetUserId() diff --git a/internal/api/grpc/user/v2/integration_test/secret_test.go b/internal/api/grpc/user/v2/integration_test/secret_test.go new file mode 100644 index 0000000000..8ff537b1fd --- /dev/null +++ b/internal/api/grpc/user/v2/integration_test/secret_test.go @@ -0,0 +1,347 @@ +//go:build integration + +package user_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func TestServer_AddSecret(t *testing.T) { + type args struct { + ctx context.Context + req *user.AddSecretRequest + prepare func(request *user.AddSecretRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "add secret, user not existing", + args: args{ + CTX, + &user.AddSecretRequest{ + UserId: "notexisting", + }, + func(request *user.AddSecretRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "add secret, ok", + args: args{ + CTX, + &user.AddSecretRequest{}, + func(request *user.AddSecretRequest) error { + resp := Instance.CreateUserTypeMachine(CTX) + request.UserId = resp.GetId() + return nil + }, + }, + }, + { + name: "add secret human, not ok", + args: args{ + CTX, + &user.AddSecretRequest{}, + func(request *user.AddSecretRequest) error { + resp := Instance.CreateUserTypeMachine(CTX) + request.UserId = resp.GetId() + return nil + }, + }, + }, + { + name: "overwrite secret, ok", + args: args{ + CTX, + &user.AddSecretRequest{}, + func(request *user.AddSecretRequest) error { + resp := Instance.CreateUserTypeMachine(CTX) + request.UserId = resp.GetId() + _, err := Client.AddSecret(CTX, &user.AddSecretRequest{ + UserId: resp.GetId(), + }) + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.AddSecret(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.ClientSecret, "client secret is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_AddSecret_Permission(t *testing.T) { + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddSecret-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Instance.Client.UserV2.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + + type args struct { + ctx context.Context + req *user.AddSecretRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{ + SystemCTX, + &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + }, + }, + { + name: "instance, ok", + args: args{ + IamCTX, + &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + }, + }, + { + name: "org, error", + args: args{ + CTX, + &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + }, + wantErr: true, + }, + { + name: "user, error", + args: args{ + UserCTX, + &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, err) + got, err := Client.AddSecret(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.ClientSecret, "client secret is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemoveSecret(t *testing.T) { + type args struct { + ctx context.Context + req *user.RemoveSecretRequest + prepare func(request *user.RemoveSecretRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "remove secret, user not existing", + args: args{ + CTX, + &user.RemoveSecretRequest{ + UserId: "notexisting", + }, + func(request *user.RemoveSecretRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "remove secret, not existing", + args: args{ + CTX, + &user.RemoveSecretRequest{}, + func(request *user.RemoveSecretRequest) error { + resp := Instance.CreateUserTypeMachine(CTX) + request.UserId = resp.GetId() + return nil + }, + }, + wantErr: true, + }, + { + name: "remove secret, ok", + args: args{ + CTX, + &user.RemoveSecretRequest{}, + func(request *user.RemoveSecretRequest) error { + resp := Instance.CreateUserTypeMachine(CTX) + request.UserId = resp.GetId() + _, err := Instance.Client.UserV2.AddSecret(CTX, &user.AddSecretRequest{ + UserId: resp.GetId(), + }) + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.RemoveSecret(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + deletionDate := got.DeletionDate.AsTime() + assert.Greater(t, deletionDate, now, "creation date is before the test started") + assert.Less(t, deletionDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemoveSecret_Permission(t *testing.T) { + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("RemoveSecret-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Instance.Client.UserV2.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + + type args struct { + ctx context.Context + req *user.RemoveSecretRequest + prepare func(request *user.RemoveSecretRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{ + SystemCTX, + &user.RemoveSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + func(request *user.RemoveSecretRequest) error { + _, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }) + return err + }, + }, + }, + { + name: "instance, ok", + args: args{ + IamCTX, + &user.RemoveSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + func(request *user.RemoveSecretRequest) error { + _, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }) + return err + }, + }, + }, + { + name: "org, error", + args: args{ + CTX, + &user.RemoveSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + func(request *user.RemoveSecretRequest) error { + _, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }) + return err + }, + }, + wantErr: true, + }, + { + name: "user, error", + args: args{ + UserCTX, + &user.RemoveSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + func(request *user.RemoveSecretRequest) error { + _, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }) + return err + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, tt.args.prepare(tt.args.req)) + got, err := Client.RemoveSecret(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.DeletionDate, "client secret is empty") + creationDate := got.DeletionDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} diff --git a/internal/api/grpc/user/v2/integration_test/user_test.go b/internal/api/grpc/user/v2/integration_test/user_test.go index 4cf4ab21f8..4eee44ab44 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -57,7 +57,7 @@ func TestMain(m *testing.M) { }()) } -func TestServer_AddHumanUser(t *testing.T) { +func TestServer_Deprecated_AddHumanUser(t *testing.T) { idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) type args struct { ctx context.Context @@ -652,6 +652,7 @@ func TestServer_AddHumanUser(t *testing.T) { t.Run(tt.name, func(t *testing.T) { userID := fmt.Sprint(time.Now().UnixNano() + int64(i)) tt.args.req.UserId = &userID + // In order to prevent unique constraint errors, we set the email to a unique value if email := tt.args.req.GetEmail(); email != nil { email.Email = fmt.Sprintf("%s@me.now", userID) } @@ -666,7 +667,6 @@ func TestServer_AddHumanUser(t *testing.T) { return } require.NoError(t, err) - assert.Equal(t, tt.want.GetUserId(), got.GetUserId()) if tt.want.GetEmailCode() != "" { assert.NotEmpty(t, got.GetEmailCode()) @@ -683,7 +683,7 @@ func TestServer_AddHumanUser(t *testing.T) { } } -func TestServer_AddHumanUser_Permission(t *testing.T) { +func TestServer_Deprecated_AddHumanUser_Permission(t *testing.T) { newOrgOwnerEmail := gofakeit.Email() newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddHuman-%s", gofakeit.AppName()), newOrgOwnerEmail) type args struct { @@ -876,7 +876,7 @@ func TestServer_AddHumanUser_Permission(t *testing.T) { } } -func TestServer_UpdateHumanUser(t *testing.T) { +func TestServer_Deprecated_UpdateHumanUser(t *testing.T) { type args struct { ctx context.Context req *user.UpdateHumanUserRequest @@ -1237,7 +1237,7 @@ func TestServer_UpdateHumanUser(t *testing.T) { } } -func TestServer_UpdateHumanUser_Permission(t *testing.T) { +func TestServer_Deprecated_UpdateHumanUser_Permission(t *testing.T) { newOrgOwnerEmail := gofakeit.Email() newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("UpdateHuman-%s", gofakeit.AppName()), newOrgOwnerEmail) newUserID := newOrg.CreatedAdmins[0].GetUserId() @@ -1834,15 +1834,26 @@ func TestServer_DeleteUser(t *testing.T) { args: args{ req: &user.DeleteUserRequest{}, prepare: func(t *testing.T, request *user.DeleteUserRequest) context.Context { - removeUser, err := Instance.Client.Mgmt.AddMachineUser(CTX, &mgmt.AddMachineUserRequest{ - UserName: gofakeit.Username(), - Name: gofakeit.Name(), + removeUser, err := Client.CreateUser(CTX, &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "givenName", + FamilyName: "familyName", + }, + Email: &user.SetHumanEmail{ + Email: gofakeit.Email(), + Verification: &user.SetHumanEmail_IsVerified{IsVerified: true}, + }, + }, + }, }) - request.UserId = removeUser.UserId require.NoError(t, err) - tokenResp, err := Instance.Client.Mgmt.AddPersonalAccessToken(CTX, &mgmt.AddPersonalAccessTokenRequest{UserId: removeUser.UserId}) - require.NoError(t, err) - return integration.WithAuthorizationToken(UserCTX, tokenResp.Token) + request.UserId = removeUser.Id + Instance.RegisterUserPasskey(CTX, removeUser.Id) + _, token, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, removeUser.Id) + return integration.WithAuthorizationToken(UserCTX, token) }, }, want: &user.DeleteUserResponse{ @@ -3610,7 +3621,6 @@ func TestServer_HumanMFAInitSkipped(t *testing.T) { t.Run(tt.name, func(t *testing.T) { err := tt.args.prepare(tt.args.req) require.NoError(t, err) - got, err := Client.HumanMFAInitSkipped(tt.args.ctx, tt.args.req) if tt.wantErr { require.Error(t, err) @@ -3624,3 +3634,1678 @@ func TestServer_HumanMFAInitSkipped(t *testing.T) { }) } } + +func TestServer_CreateUser(t *testing.T) { + type args struct { + ctx context.Context + req *user.CreateUserRequest + } + type testCase struct { + args args + want *user.CreateUserResponse + wantErr bool + } + tests := []struct { + name string + testCase func(runId string) testCase + }{ + { + name: "default verification", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + wantErr: false, + } + }, + }, + { + name: "return email verification code", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + EmailCode: gu.Ptr("something"), + }, + } + }, + }, + { + name: "custom template", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"), + }, + }, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "return phone verification code", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + Phone: &user.SetHumanPhone{ + Phone: "+41791234567", + Verification: &user.SetHumanPhone_ReturnCode{ + ReturnCode: &user.ReturnPhoneVerificationCode{}, + }, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + PhoneCode: gu.Ptr("something"), + }, + } + }, + }, + { + name: "custom template error", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("{{"), + }, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "missing REQUIRED profile", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "missing REQUIRED email", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "missing empty email", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{}, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "missing idp", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + IdpLinks: []*user.IDPLink{ + { + IdpId: "idpID", + UserId: "userID", + UserName: "username", + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "with idp", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + IdpLinks: []*user.IDPLink{ + { + IdpId: idpResp.Id, + UserId: "userID", + UserName: "username", + }, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "with totp", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + TotpSecret: gu.Ptr("secret"), + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "password not complexity conform", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + NickName: gu.Ptr("Dukkie"), + DisplayName: gu.Ptr("Donald Duck"), + PreferredLanguage: gu.Ptr("en"), + Gender: user.Gender_GENDER_DIVERSE.Enum(), + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + PasswordType: &user.CreateUserRequest_Human_Password{ + Password: &user.Password{ + Password: "insufficient", + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "hashed password", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + PasswordType: &user.CreateUserRequest_Human_HashedPassword{ + HashedPassword: &user.HashedPassword{ + Hash: "$2y$12$hXUrnqdq1RIIYZ2HPytIIe5lXdIvbhqrTvdPsSF7o.jFh817Z6lwm", + }, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "unsupported hashed password", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + PasswordType: &user.CreateUserRequest_Human_HashedPassword{ + HashedPassword: &user.HashedPassword{ + Hash: "$scrypt$ln=16,r=8,p=1$cmFuZG9tc2FsdGlzaGFyZA$Rh+NnJNo1I6nRwaNqbDm6kmADswD1+7FTKZ7Ln9D8nQ", + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "human default username", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "machine user", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "machine default username to generated id", + testCase: func(runId string) testCase { + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "machine default username to given id", + testCase: func(runId string) testCase { + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + UserId: &runId, + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: runId, + }, + } + }, + }, + { + name: "org does not exist human, error", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: "does not exist", + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "org does not exist machine, error", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: "does not exist", + Username: &username, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }, + }, + wantErr: true, + } + }, + }, + } + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + runId := fmt.Sprint(now.UnixNano() + int64(i)) + test := tt.testCase(runId) + got, err := Client.CreateUser(test.args.ctx, test.args.req) + if test.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + if test.want.GetEmailCode() != "" { + assert.NotEmpty(t, got.GetEmailCode(), "email code is empty") + } else { + assert.Empty(t, got.GetEmailCode(), "email code is not empty") + } + if test.want.GetPhoneCode() != "" { + assert.NotEmpty(t, got.GetPhoneCode(), "phone code is empty") + } else { + assert.Empty(t, got.GetPhoneCode(), "phone code is not empty") + } + if test.want.GetId() == "is generated" { + assert.Len(t, got.GetId(), 18, "ID is not 18 characters") + } else { + assert.Equal(t, test.want.GetId(), got.GetId(), "ID is not the same") + } + }) + } +} + +func TestServer_CreateUser_And_Compare(t *testing.T) { + type args struct { + ctx context.Context + req *user.CreateUserRequest + } + type testCase struct { + name string + args args + assert func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) + } + tests := []struct { + name string + testCase func(runId string) testCase + }{{ + name: "human given username", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + ctx: CTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, username, getResponse.GetUser().GetUsername()) + }, + } + }, + }, { + name: "human username default to email", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + ctx: CTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, email, getResponse.GetUser().GetUsername()) + }, + } + }, + }, { + name: "machine username given", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + ctx: CTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, username, getResponse.GetUser().GetUsername()) + }, + } + }, + }, { + name: "machine username default to generated id", + testCase: func(runId string) testCase { + return testCase{ + args: args{ + ctx: CTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + assert: func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, createResponse.GetId(), getResponse.GetUser().GetUsername()) + }, + } + }, + }, { + name: "machine username default to given id", + testCase: func(runId string) testCase { + return testCase{ + args: args{ + ctx: CTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserId: &runId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + assert: func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, runId, getResponse.GetUser().GetUsername()) + }, + } + }, + }} + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + runId := fmt.Sprint(now.UnixNano() + int64(i)) + test := tt.testCase(runId) + createResponse, err := Client.CreateUser(test.args.ctx, test.args.req) + require.NoError(t, err) + Instance.TriggerUserByID(test.args.ctx, createResponse.GetId()) + getResponse, err := Client.GetUserByID(test.args.ctx, &user.GetUserByIDRequest{ + UserId: createResponse.GetId(), + }) + require.NoError(t, err) + test.assert(t, createResponse, getResponse) + }) + } +} + +func TestServer_CreateUser_Permission(t *testing.T) { + newOrgOwnerEmail := gofakeit.Email() + newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddHuman-%s", gofakeit.AppName()), newOrgOwnerEmail) + type args struct { + ctx context.Context + req *user.CreateUserRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "human system, ok", + args: args{ + SystemCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: "this is overwritten with a unique address", + }, + }, + }, + }, + }, + }, + { + name: "human instance, ok", + args: args{ + IamCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: "this is overwritten with a unique address", + }, + }, + }, + }, + }, + }, + { + name: "human org, error", + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: "this is overwritten with a unique address", + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "human user, error", + args: args{ + UserCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: "this is overwritten with a unique address", + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "machine system, ok", + args: args{ + SystemCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + }, + { + name: "machine instance, ok", + args: args{ + IamCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + }, + { + name: "machine org, error", + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + wantErr: true, + }, + { + name: "machine user, error", + args: args{ + UserCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + wantErr: true, + }, + } + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + userID := fmt.Sprint(time.Now().UnixNano() + int64(i)) + tt.args.req.UserId = &userID + if email := tt.args.req.GetHuman().GetEmail(); email != nil { + email.Email = fmt.Sprintf("%s@example.com", userID) + } + _, err := Client.CreateUser(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestServer_UpdateUserTypeHuman(t *testing.T) { + type args struct { + ctx context.Context + req *user.UpdateUserRequest + } + type testCase struct { + args args + want *user.UpdateUserResponse + wantErr bool + } + tests := []struct { + name string + testCase func(runId, userId string) testCase + }{ + { + name: "default verification", + testCase: func(runId, userId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + want: &user.UpdateUserResponse{}, + wantErr: false, + } + }, + }, + { + name: "return email verification code", + testCase: func(runId, userId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + }, + }, + want: &user.UpdateUserResponse{ + EmailCode: gu.Ptr("something"), + }, + } + }, + }, + { + name: "custom template", + testCase: func(runId, userId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"), + }, + }, + }, + }, + }, + }, + }, + want: &user.UpdateUserResponse{}, + } + }, + }, + { + name: "return phone verification code", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{ + Phone: "+41791234568", + Verification: &user.SetHumanPhone_ReturnCode{ + ReturnCode: &user.ReturnPhoneVerificationCode{}, + }, + }, + }, + }, + }, + }, + want: &user.UpdateUserResponse{ + PhoneCode: gu.Ptr("something"), + }, + } + }, + }, + { + name: "custom template error", + testCase: func(runId, userId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("{{"), + }, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "missing empty email", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{}, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "password not complexity conform", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Password: &user.SetPassword{ + PasswordType: &user.SetPassword_Password{ + Password: &user.Password{ + Password: "insufficient", + }, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "hashed password", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Password: &user.SetPassword{ + PasswordType: &user.SetPassword_HashedPassword{ + HashedPassword: &user.HashedPassword{ + Hash: "$2y$12$hXUrnqdq1RIIYZ2HPytIIe5lXdIvbhqrTvdPsSF7o.jFh817Z6lwm", + }, + }, + }, + }, + }, + }, + }, + want: &user.UpdateUserResponse{}, + } + }, + }, + { + name: "unsupported hashed password", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Password: &user.SetPassword{ + PasswordType: &user.SetPassword_HashedPassword{ + HashedPassword: &user.HashedPassword{ + Hash: "$scrypt$ln=16,r=8,p=1$cmFuZG9tc2FsdGlzaGFyZA$Rh+NnJNo1I6nRwaNqbDm6kmADswD1+7FTKZ7Ln9D8nQ", + }, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "update human user with machine fields, error", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: &runId, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + } + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + runId := fmt.Sprint(now.UnixNano() + int64(i)) + userId := Instance.CreateUserTypeHuman(CTX).GetId() + test := tt.testCase(runId, userId) + got, err := Client.UpdateUser(test.args.ctx, test.args.req) + if test.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + changeDate := got.ChangeDate.AsTime() + assert.Greater(t, changeDate, now, "change date is before the test started") + assert.Less(t, changeDate, time.Now(), "change date is in the future") + if test.want.GetEmailCode() != "" { + assert.NotEmpty(t, got.GetEmailCode(), "email code is empty") + } else { + assert.Empty(t, got.GetEmailCode(), "email code is not empty") + } + if test.want.GetPhoneCode() != "" { + assert.NotEmpty(t, got.GetPhoneCode(), "phone code is empty") + } else { + assert.Empty(t, got.GetPhoneCode(), "phone code is not empty") + } + }) + } +} + +func TestServer_UpdateUserTypeMachine(t *testing.T) { + type args struct { + ctx context.Context + req *user.UpdateUserRequest + } + type testCase struct { + args args + wantErr bool + } + tests := []struct { + name string + testCase func(runId, userId string) testCase + }{ + { + name: "update machine, ok", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("donald"), + }, + }, + }, + }, + } + }, + }, + { + name: "update machine user with human fields, error", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("Donald"), + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + } + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + runId := fmt.Sprint(now.UnixNano() + int64(i)) + userId := Instance.CreateUserTypeMachine(CTX).GetId() + test := tt.testCase(runId, userId) + got, err := Client.UpdateUser(test.args.ctx, test.args.req) + if test.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + changeDate := got.ChangeDate.AsTime() + assert.Greater(t, changeDate, now, "change date is before the test started") + assert.Less(t, changeDate, time.Now(), "change date is in the future") + }) + } +} + +func TestServer_UpdateUser_And_Compare(t *testing.T) { + type args struct { + ctx context.Context + create *user.CreateUserRequest + update *user.UpdateUserRequest + } + type testCase struct { + args args + assert func(t *testing.T, getResponse *user.GetUserByIDResponse) + } + tests := []struct { + name string + testCase func(runId string) testCase + }{{ + name: "human remove phone", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + ctx: CTX, + create: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserId: &runId, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + Phone: &user.SetHumanPhone{ + Phone: "+1234567890", + }, + }, + }, + }, + update: &user.UpdateUserRequest{ + UserId: runId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{}, + }, + }, + }, + }, + assert: func(t *testing.T, getResponse *user.GetUserByIDResponse) { + assert.Empty(t, getResponse.GetUser().GetHuman().GetPhone().GetPhone(), "phone is not empty") + }, + } + }, + }, { + name: "human username", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + ctx: CTX, + create: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserId: &runId, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + update: &user.UpdateUserRequest{ + UserId: runId, + Username: &username, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{}, + }, + }, + }, + assert: func(t *testing.T, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, username, getResponse.GetUser().GetUsername()) + }, + } + }, + }, { + name: "machine username", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + ctx: CTX, + create: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserId: &runId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "Donald", + }, + }, + }, + update: &user.UpdateUserRequest{ + UserId: runId, + Username: &username, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{}, + }, + }, + }, + assert: func(t *testing.T, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, username, getResponse.GetUser().GetUsername()) + }, + } + }, + }} + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + runId := fmt.Sprint(now.UnixNano() + int64(i)) + test := tt.testCase(runId) + createResponse, err := Client.CreateUser(test.args.ctx, test.args.create) + require.NoError(t, err) + _, err = Client.UpdateUser(test.args.ctx, test.args.update) + require.NoError(t, err) + Instance.TriggerUserByID(test.args.ctx, createResponse.GetId()) + getResponse, err := Client.GetUserByID(test.args.ctx, &user.GetUserByIDRequest{ + UserId: createResponse.GetId(), + }) + require.NoError(t, err) + test.assert(t, getResponse) + }) + } +} + +func TestServer_UpdateUser_Permission(t *testing.T) { + newOrgOwnerEmail := gofakeit.Email() + newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddHuman-%s", gofakeit.AppName()), newOrgOwnerEmail) + newHumanUserID := newOrg.CreatedAdmins[0].GetUserId() + machineUserResp, err := Instance.Client.UserV2.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: newOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "Donald", + }, + }, + }) + require.NoError(t, err) + newMachineUserID := machineUserResp.GetId() + Instance.TriggerUserByID(IamCTX, newMachineUserID) + type args struct { + ctx context.Context + req *user.UpdateUserRequest + } + type testCase struct { + args args + wantErr bool + } + tests := []struct { + name string + testCase func() testCase + }{ + { + name: "human, system, ok", + testCase: func() testCase { + return testCase{ + args: args{ + SystemCTX, + &user.UpdateUserRequest{ + UserId: newHumanUserID, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("Donald"), + }, + }, + }, + }, + }, + } + }, + }, + { + name: "human instance, ok", + testCase: func() testCase { + return testCase{ + args: args{ + IamCTX, + &user.UpdateUserRequest{ + UserId: newHumanUserID, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("Donald"), + }, + }, + }, + }, + }, + } + }, + }, + { + name: "human org, error", + testCase: func() testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: newHumanUserID, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("Donald"), + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "human user, error", + testCase: func() testCase { + return testCase{ + args: args{ + UserCTX, + &user.UpdateUserRequest{ + UserId: newHumanUserID, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("Donald"), + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "machine system, ok", + testCase: func() testCase { + return testCase{ + args: args{ + SystemCTX, + &user.UpdateUserRequest{ + UserId: newMachineUserID, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("Donald"), + }, + }, + }, + }, + } + }, + }, + { + name: "machine instance, ok", + testCase: func() testCase { + return testCase{ + args: args{ + IamCTX, + &user.UpdateUserRequest{ + UserId: newMachineUserID, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("Donald"), + }, + }, + }, + }, + } + }, + }, + { + name: "machine org, error", + testCase: func() testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: newMachineUserID, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("Donald"), + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "machine user, error", + testCase: func() testCase { + return testCase{ + args: args{ + UserCTX, + &user.UpdateUserRequest{ + UserId: newMachineUserID, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("Donald"), + }, + }, + }, + }, + wantErr: true, + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + test := tt.testCase() + _, err := Client.UpdateUser(test.args.ctx, test.args.req) + if test.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/internal/api/grpc/user/v2/key.go b/internal/api/grpc/user/v2/key.go new file mode 100644 index 0000000000..59dab44248 --- /dev/null +++ b/internal/api/grpc/user/v2/key.go @@ -0,0 +1,62 @@ +package user + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) AddKey(ctx context.Context, req *user.AddKeyRequest) (*user.AddKeyResponse, error) { + newMachineKey := &command.MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.UserId, + }, + ExpirationDate: req.GetExpirationDate().AsTime(), + Type: domain.AuthNKeyTypeJSON, + PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx), + } + newMachineKey.PublicKey = req.PublicKey + + pubkeySupplied := len(newMachineKey.PublicKey) > 0 + details, err := s.command.AddUserMachineKey(ctx, newMachineKey) + if err != nil { + return nil, err + } + // Return key details only if the pubkey wasn't supplied, otherwise the user already has + // private key locally + var keyDetails []byte + if !pubkeySupplied { + var err error + keyDetails, err = newMachineKey.Detail() + if err != nil { + return nil, err + } + } + return &user.AddKeyResponse{ + KeyId: newMachineKey.KeyID, + KeyContent: keyDetails, + CreationDate: timestamppb.New(details.EventDate), + }, nil +} + +func (s *Server) RemoveKey(ctx context.Context, req *user.RemoveKeyRequest) (*user.RemoveKeyResponse, error) { + machineKey := &command.MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.UserId, + }, + PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx), + KeyID: req.KeyId, + } + objectDetails, err := s.command.RemoveUserMachineKey(ctx, machineKey) + if err != nil { + return nil, err + } + return &user.RemoveKeyResponse{ + DeletionDate: timestamppb.New(objectDetails.EventDate), + }, nil +} diff --git a/internal/api/grpc/user/v2/key_query.go b/internal/api/grpc/user/v2/key_query.go new file mode 100644 index 0000000000..da4f47decf --- /dev/null +++ b/internal/api/grpc/user/v2/key_query.go @@ -0,0 +1,124 @@ +package user + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) ListKeys(ctx context.Context, req *user.ListKeysRequest) (*user.ListKeysResponse, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + + filters, err := keyFiltersToQueries(req.Filters) + if err != nil { + return nil, err + } + search := &query.AuthNKeySearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: authnKeyFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: filters, + } + result, err := s.query.SearchAuthNKeys(ctx, search, query.JoinFilterUserMachine, s.checkPermission) + if err != nil { + return nil, err + } + resp := &user.ListKeysResponse{ + Result: make([]*user.Key, len(result.AuthNKeys)), + Pagination: filter.QueryToPaginationPb(search.SearchRequest, result.SearchResponse), + } + for i, key := range result.AuthNKeys { + resp.Result[i] = &user.Key{ + CreationDate: timestamppb.New(key.CreationDate), + ChangeDate: timestamppb.New(key.ChangeDate), + Id: key.ID, + UserId: key.AggregateID, + OrganizationId: key.ResourceOwner, + ExpirationDate: timestamppb.New(key.Expiration), + } + } + return resp, nil +} + +func keyFiltersToQueries(filters []*user.KeysSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(filters)) + for i, filter := range filters { + q[i], err = keyFilterToQuery(filter) + if err != nil { + return nil, err + } + } + return q, nil +} + +func keyFilterToQuery(filter *user.KeysSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *user.KeysSearchFilter_CreatedDateFilter: + return authnKeyCreatedFilterToQuery(q.CreatedDateFilter) + case *user.KeysSearchFilter_ExpirationDateFilter: + return authnKeyExpirationFilterToQuery(q.ExpirationDateFilter) + case *user.KeysSearchFilter_KeyIdFilter: + return authnKeyIdFilterToQuery(q.KeyIdFilter) + case *user.KeysSearchFilter_UserIdFilter: + return authnKeyUserIdFilterToQuery(q.UserIdFilter) + case *user.KeysSearchFilter_OrganizationIdFilter: + return authnKeyOrgIdFilterToQuery(q.OrganizationIdFilter) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") + } +} + +func authnKeyIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewAuthNKeyIDQuery(f.Id) +} + +func authnKeyUserIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewAuthNKeyIdentifyerQuery(f.Id) +} + +func authnKeyOrgIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewAuthNKeyResourceOwnerQuery(f.Id) +} + +func authnKeyCreatedFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewAuthNKeyCreationDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method)) +} + +func authnKeyExpirationFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewAuthNKeyExpirationDateDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method)) +} + +// authnKeyFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination +func authnKeyFieldNameToSortingColumn(field *user.KeyFieldName) query.Column { + if field == nil { + return query.AuthNKeyColumnCreationDate + } + switch *field { + case user.KeyFieldName_KEY_FIELD_NAME_UNSPECIFIED: + return query.AuthNKeyColumnCreationDate + case user.KeyFieldName_KEY_FIELD_NAME_ID: + return query.AuthNKeyColumnID + case user.KeyFieldName_KEY_FIELD_NAME_USER_ID: + return query.AuthNKeyColumnIdentifier + case user.KeyFieldName_KEY_FIELD_NAME_ORGANIZATION_ID: + return query.AuthNKeyColumnResourceOwner + case user.KeyFieldName_KEY_FIELD_NAME_CREATED_DATE: + return query.AuthNKeyColumnCreationDate + case user.KeyFieldName_KEY_FIELD_NAME_KEY_EXPIRATION_DATE: + return query.AuthNKeyColumnExpiration + default: + return query.AuthNKeyColumnCreationDate + } +} diff --git a/internal/api/grpc/user/v2/machine.go b/internal/api/grpc/user/v2/machine.go new file mode 100644 index 0000000000..010ba75678 --- /dev/null +++ b/internal/api/grpc/user/v2/machine.go @@ -0,0 +1,58 @@ +package user + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) createUserTypeMachine(ctx context.Context, machinePb *user.CreateUserRequest_Machine, orgId, userName, userId string) (*user.CreateUserResponse, error) { + cmd := &command.Machine{ + Username: userName, + Name: machinePb.Name, + Description: machinePb.GetDescription(), + AccessTokenType: domain.OIDCTokenTypeBearer, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: orgId, + AggregateID: userId, + }, + } + details, err := s.command.AddMachine( + ctx, + cmd, + s.command.NewPermissionCheckUserWrite(ctx), + command.AddMachineWithUsernameToIDFallback(), + ) + if err != nil { + return nil, err + } + return &user.CreateUserResponse{ + Id: cmd.AggregateID, + CreationDate: timestamppb.New(details.EventDate), + }, nil +} + +func (s *Server) updateUserTypeMachine(ctx context.Context, machinePb *user.UpdateUserRequest_Machine, userId string, userName *string) (*user.UpdateUserResponse, error) { + cmd := updateMachineUserToCommand(userId, userName, machinePb) + err := s.command.ChangeUserMachine(ctx, cmd) + if err != nil { + return nil, err + } + return &user.UpdateUserResponse{ + ChangeDate: timestamppb.New(cmd.Details.EventDate), + }, nil +} + +func updateMachineUserToCommand(userId string, userName *string, machine *user.UpdateUserRequest_Machine) *command.ChangeMachine { + return &command.ChangeMachine{ + ID: userId, + Username: userName, + Name: machine.Name, + Description: machine.Description, + } +} diff --git a/internal/api/grpc/user/v2/machine_test.go b/internal/api/grpc/user/v2/machine_test.go new file mode 100644 index 0000000000..96d77d8fa2 --- /dev/null +++ b/internal/api/grpc/user/v2/machine_test.go @@ -0,0 +1,62 @@ +package user + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/muhlemmer/gu" + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func Test_patchMachineUserToCommand(t *testing.T) { + type args struct { + userId string + userName *string + machine *user.UpdateUserRequest_Machine + } + tests := []struct { + name string + args args + want *command.ChangeMachine + }{{ + name: "single property", + args: args{ + userId: "userId", + machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("name"), + }, + }, + want: &command.ChangeMachine{ + ID: "userId", + Name: gu.Ptr("name"), + }, + }, { + name: "all properties", + args: args{ + userId: "userId", + userName: gu.Ptr("userName"), + machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("name"), + Description: gu.Ptr("description"), + }, + }, + want: &command.ChangeMachine{ + ID: "userId", + Username: gu.Ptr("userName"), + Name: gu.Ptr("name"), + Description: gu.Ptr("description"), + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := updateMachineUserToCommand(tt.args.userId, tt.args.userName, tt.args.machine) + if diff := cmp.Diff(tt.want, got, cmpopts.EquateComparable(language.Tag{})); diff != "" { + t.Errorf("patchMachineUserToCommand() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/api/grpc/user/v2/pat.go b/internal/api/grpc/user/v2/pat.go new file mode 100644 index 0000000000..54f6e99367 --- /dev/null +++ b/internal/api/grpc/user/v2/pat.go @@ -0,0 +1,56 @@ +package user + +import ( + "context" + + "github.com/zitadel/oidc/v3/pkg/oidc" + "google.golang.org/protobuf/types/known/timestamppb" + + z_oidc "github.com/zitadel/zitadel/internal/api/oidc" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) AddPersonalAccessToken(ctx context.Context, req *user.AddPersonalAccessTokenRequest) (*user.AddPersonalAccessTokenResponse, error) { + newPat := &command.PersonalAccessToken{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.UserId, + }, + PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx), + ExpirationDate: req.ExpirationDate.AsTime(), + Scopes: []string{ + oidc.ScopeOpenID, + oidc.ScopeProfile, + z_oidc.ScopeUserMetaData, + z_oidc.ScopeResourceOwner, + }, + AllowedUserType: domain.UserTypeMachine, + } + details, err := s.command.AddPersonalAccessToken(ctx, newPat) + if err != nil { + return nil, err + } + return &user.AddPersonalAccessTokenResponse{ + CreationDate: timestamppb.New(details.EventDate), + TokenId: newPat.TokenID, + Token: newPat.Token, + }, nil +} + +func (s *Server) RemovePersonalAccessToken(ctx context.Context, req *user.RemovePersonalAccessTokenRequest) (*user.RemovePersonalAccessTokenResponse, error) { + objectDetails, err := s.command.RemovePersonalAccessToken(ctx, &command.PersonalAccessToken{ + TokenID: req.TokenId, + ObjectRoot: models.ObjectRoot{ + AggregateID: req.UserId, + }, + PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx), + }) + if err != nil { + return nil, err + } + return &user.RemovePersonalAccessTokenResponse{ + DeletionDate: timestamppb.New(objectDetails.EventDate), + }, nil +} diff --git a/internal/api/grpc/user/v2/pat_query.go b/internal/api/grpc/user/v2/pat_query.go new file mode 100644 index 0000000000..6bbd44d511 --- /dev/null +++ b/internal/api/grpc/user/v2/pat_query.go @@ -0,0 +1,123 @@ +package user + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) ListPersonalAccessTokens(ctx context.Context, req *user.ListPersonalAccessTokensRequest) (*user.ListPersonalAccessTokensResponse, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + filters, err := patFiltersToQueries(req.Filters) + if err != nil { + return nil, err + } + search := &query.PersonalAccessTokenSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: authnPersonalAccessTokenFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: filters, + } + result, err := s.query.SearchPersonalAccessTokens(ctx, search, s.checkPermission) + if err != nil { + return nil, err + } + resp := &user.ListPersonalAccessTokensResponse{ + Result: make([]*user.PersonalAccessToken, len(result.PersonalAccessTokens)), + Pagination: filter.QueryToPaginationPb(search.SearchRequest, result.SearchResponse), + } + for i, pat := range result.PersonalAccessTokens { + resp.Result[i] = &user.PersonalAccessToken{ + CreationDate: timestamppb.New(pat.CreationDate), + ChangeDate: timestamppb.New(pat.ChangeDate), + Id: pat.ID, + UserId: pat.UserID, + OrganizationId: pat.ResourceOwner, + ExpirationDate: timestamppb.New(pat.Expiration), + } + } + return resp, nil +} + +func patFiltersToQueries(filters []*user.PersonalAccessTokensSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(filters)) + for i, filter := range filters { + q[i], err = patFilterToQuery(filter) + if err != nil { + return nil, err + } + } + return q, nil +} + +func patFilterToQuery(filter *user.PersonalAccessTokensSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *user.PersonalAccessTokensSearchFilter_CreatedDateFilter: + return authnPersonalAccessTokenCreatedFilterToQuery(q.CreatedDateFilter) + case *user.PersonalAccessTokensSearchFilter_ExpirationDateFilter: + return authnPersonalAccessTokenExpirationFilterToQuery(q.ExpirationDateFilter) + case *user.PersonalAccessTokensSearchFilter_TokenIdFilter: + return authnPersonalAccessTokenIdFilterToQuery(q.TokenIdFilter) + case *user.PersonalAccessTokensSearchFilter_UserIdFilter: + return authnPersonalAccessTokenUserIdFilterToQuery(q.UserIdFilter) + case *user.PersonalAccessTokensSearchFilter_OrganizationIdFilter: + return authnPersonalAccessTokenOrgIdFilterToQuery(q.OrganizationIdFilter) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") + } +} + +func authnPersonalAccessTokenIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewPersonalAccessTokenIDQuery(f.Id) +} + +func authnPersonalAccessTokenUserIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewPersonalAccessTokenUserIDSearchQuery(f.Id) +} + +func authnPersonalAccessTokenOrgIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewPersonalAccessTokenResourceOwnerSearchQuery(f.Id) +} + +func authnPersonalAccessTokenCreatedFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewPersonalAccessTokenCreationDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method)) +} + +func authnPersonalAccessTokenExpirationFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewPersonalAccessTokenExpirationDateDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method)) +} + +// authnPersonalAccessTokenFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination +func authnPersonalAccessTokenFieldNameToSortingColumn(field *user.PersonalAccessTokenFieldName) query.Column { + if field == nil { + return query.PersonalAccessTokenColumnCreationDate + } + switch *field { + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_UNSPECIFIED: + return query.PersonalAccessTokenColumnCreationDate + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_ID: + return query.PersonalAccessTokenColumnID + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_USER_ID: + return query.PersonalAccessTokenColumnUserID + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_ORGANIZATION_ID: + return query.PersonalAccessTokenColumnResourceOwner + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_CREATED_DATE: + return query.PersonalAccessTokenColumnCreationDate + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_EXPIRATION_DATE: + return query.PersonalAccessTokenColumnExpiration + default: + return query.PersonalAccessTokenColumnCreationDate + } +} diff --git a/internal/api/grpc/user/v2/secret.go b/internal/api/grpc/user/v2/secret.go new file mode 100644 index 0000000000..1d54e1dde8 --- /dev/null +++ b/internal/api/grpc/user/v2/secret.go @@ -0,0 +1,39 @@ +package user + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) AddSecret(ctx context.Context, req *user.AddSecretRequest) (*user.AddSecretResponse, error) { + newSecret := &command.GenerateMachineSecret{ + PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx), + } + details, err := s.command.GenerateMachineSecret(ctx, req.UserId, "", newSecret) + if err != nil { + return nil, err + } + return &user.AddSecretResponse{ + CreationDate: timestamppb.New(details.EventDate), + ClientSecret: newSecret.ClientSecret, + }, nil +} + +func (s *Server) RemoveSecret(ctx context.Context, req *user.RemoveSecretRequest) (*user.RemoveSecretResponse, error) { + details, err := s.command.RemoveMachineSecret( + ctx, + req.UserId, + "", + s.command.NewPermissionCheckUserWrite(ctx), + ) + if err != nil { + return nil, err + } + return &user.RemoveSecretResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }, nil +} diff --git a/internal/api/grpc/user/v2/server.go b/internal/api/grpc/user/v2/server.go index 9272ea27ee..e3c7e8011e 100644 --- a/internal/api/grpc/user/v2/server.go +++ b/internal/api/grpc/user/v2/server.go @@ -8,6 +8,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" @@ -18,12 +19,13 @@ var _ user.UserServiceServer = (*Server)(nil) type Server struct { user.UnimplementedUserServiceServer - command *command.Commands - query *query.Queries - userCodeAlg crypto.EncryptionAlgorithm - idpAlg crypto.EncryptionAlgorithm - idpCallback func(ctx context.Context) string - samlRootURL func(ctx context.Context, idpID string) string + systemDefaults systemdefaults.SystemDefaults + command *command.Commands + query *query.Queries + userCodeAlg crypto.EncryptionAlgorithm + idpAlg crypto.EncryptionAlgorithm + idpCallback func(ctx context.Context) string + samlRootURL func(ctx context.Context, idpID string) string assetAPIPrefix func(context.Context) string @@ -33,6 +35,7 @@ type Server struct { type Config struct{} func CreateServer( + systemDefaults systemdefaults.SystemDefaults, command *command.Commands, query *query.Queries, userCodeAlg crypto.EncryptionAlgorithm, @@ -43,6 +46,7 @@ func CreateServer( checkPermission domain.PermissionCheck, ) *Server { return &Server{ + systemDefaults: systemDefaults, command: command, query: query, userCodeAlg: userCodeAlg, diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go index 0f958f0d40..6b4b2da75b 100644 --- a/internal/api/grpc/user/v2/user.go +++ b/internal/api/grpc/user/v2/user.go @@ -11,6 +11,7 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) @@ -117,7 +118,7 @@ func genderToDomain(gender user.Gender) domain.Gender { } func (s *Server) UpdateHumanUser(ctx context.Context, req *user.UpdateHumanUserRequest) (_ *user.UpdateHumanUserResponse, err error) { - human, err := UpdateUserRequestToChangeHuman(req) + human, err := updateHumanUserRequestToChangeHuman(req) if err != nil { return nil, err } @@ -181,86 +182,6 @@ func ifNotNilPtr[v, p any](value *v, conv func(v) p) *p { return &pVal } -func UpdateUserRequestToChangeHuman(req *user.UpdateHumanUserRequest) (*command.ChangeHuman, error) { - email, err := SetHumanEmailToEmail(req.Email, req.GetUserId()) - if err != nil { - return nil, err - } - return &command.ChangeHuman{ - ID: req.GetUserId(), - Username: req.Username, - Profile: SetHumanProfileToProfile(req.Profile), - Email: email, - Phone: SetHumanPhoneToPhone(req.Phone), - Password: SetHumanPasswordToPassword(req.Password), - }, nil -} - -func SetHumanProfileToProfile(profile *user.SetHumanProfile) *command.Profile { - if profile == nil { - return nil - } - var firstName *string - if profile.GivenName != "" { - firstName = &profile.GivenName - } - var lastName *string - if profile.FamilyName != "" { - lastName = &profile.FamilyName - } - return &command.Profile{ - FirstName: firstName, - LastName: lastName, - NickName: profile.NickName, - DisplayName: profile.DisplayName, - PreferredLanguage: ifNotNilPtr(profile.PreferredLanguage, language.Make), - Gender: ifNotNilPtr(profile.Gender, genderToDomain), - } -} - -func SetHumanEmailToEmail(email *user.SetHumanEmail, userID string) (*command.Email, error) { - if email == nil { - return nil, nil - } - var urlTemplate string - if email.GetSendCode() != nil && email.GetSendCode().UrlTemplate != nil { - urlTemplate = *email.GetSendCode().UrlTemplate - if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, userID, "code", "orgID"); err != nil { - return nil, err - } - } - return &command.Email{ - Address: domain.EmailAddress(email.Email), - Verified: email.GetIsVerified(), - ReturnCode: email.GetReturnCode() != nil, - URLTemplate: urlTemplate, - }, nil -} - -func SetHumanPhoneToPhone(phone *user.SetHumanPhone) *command.Phone { - if phone == nil { - return nil - } - return &command.Phone{ - Number: domain.PhoneNumber(phone.GetPhone()), - Verified: phone.GetIsVerified(), - ReturnCode: phone.GetReturnCode() != nil, - } -} - -func SetHumanPasswordToPassword(password *user.SetPassword) *command.Password { - if password == nil { - return nil - } - return &command.Password{ - PasswordCode: password.GetVerificationCode(), - OldPassword: password.GetCurrentPassword(), - Password: password.GetPassword().GetPassword(), - EncodedPasswordHash: password.GetHashedPassword().GetHash(), - ChangeRequired: password.GetPassword().GetChangeRequired() || password.GetHashedPassword().GetChangeRequired(), - } -} - func (s *Server) DeleteUser(ctx context.Context, req *user.DeleteUserRequest) (_ *user.DeleteUserResponse, err error) { memberships, grants, err := s.removeUserDependencies(ctx, req.GetUserId()) if err != nil { @@ -482,3 +403,25 @@ func (s *Server) HumanMFAInitSkipped(ctx context.Context, req *user.HumanMFAInit Details: object.DomainToDetailsPb(details), }, nil } + +func (s *Server) CreateUser(ctx context.Context, req *user.CreateUserRequest) (*user.CreateUserResponse, error) { + switch userType := req.GetUserType().(type) { + case *user.CreateUserRequest_Human_: + return s.createUserTypeHuman(ctx, userType.Human, req.OrganizationId, req.Username, req.UserId) + case *user.CreateUserRequest_Machine_: + return s.createUserTypeMachine(ctx, userType.Machine, req.OrganizationId, req.GetUsername(), req.GetUserId()) + default: + return nil, zerrors.ThrowInternal(nil, "", "user type is not implemented") + } +} + +func (s *Server) UpdateUser(ctx context.Context, req *user.UpdateUserRequest) (*user.UpdateUserResponse, error) { + switch userType := req.GetUserType().(type) { + case *user.UpdateUserRequest_Human_: + return s.updateUserTypeHuman(ctx, userType.Human, req.UserId, req.Username) + case *user.UpdateUserRequest_Machine_: + return s.updateUserTypeMachine(ctx, userType.Machine, req.UserId, req.Username) + default: + return nil, zerrors.ThrowUnimplemented(nil, "", "user type is not implemented") + } +} diff --git a/internal/api/grpc/user/v2/query.go b/internal/api/grpc/user/v2/user_query.go similarity index 100% rename from internal/api/grpc/user/v2/query.go rename to internal/api/grpc/user/v2/user_query.go diff --git a/internal/api/scim/resources/user.go b/internal/api/scim/resources/user.go index bc8d864994..13baed5d51 100644 --- a/internal/api/scim/resources/user.go +++ b/internal/api/scim/resources/user.go @@ -203,7 +203,6 @@ func (h *UsersHandler) Delete(ctx context.Context, id string) error { if err != nil { return err } - _, err = h.command.RemoveUserV2(ctx, id, authz.GetCtxData(ctx).OrgID, memberships, grants...) return err } diff --git a/internal/command/instance_member.go b/internal/command/instance_member.go index ee9bf15f84..a33635e8f5 100644 --- a/internal/command/instance_member.go +++ b/internal/command/instance_member.go @@ -22,7 +22,7 @@ func (c *Commands) AddInstanceMemberCommand(a *instance.Aggregate, userID string return nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-4m0fS", "Errors.IAM.MemberInvalid") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - if exists, err := ExistsUser(ctx, filter, userID, ""); err != nil || !exists { + if exists, err := ExistsUser(ctx, filter, userID, "", false); err != nil || !exists { return nil, zerrors.ThrowPreconditionFailed(err, "INSTA-GSXOn", "Errors.User.NotFound") } if isMember, err := IsInstanceMember(ctx, filter, a.ID, userID); err != nil || isMember { diff --git a/internal/command/org_member.go b/internal/command/org_member.go index ae9bef2151..bf1ae91d8a 100644 --- a/internal/command/org_member.go +++ b/internal/command/org_member.go @@ -28,7 +28,7 @@ func (c *Commands) AddOrgMemberCommand(a *org.Aggregate, userID string, roles .. ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if exists, err := ExistsUser(ctx, filter, userID, ""); err != nil || !exists { + if exists, err := ExistsUser(ctx, filter, userID, "", false); err != nil || !exists { return nil, zerrors.ThrowPreconditionFailed(err, "ORG-GoXOn", "Errors.User.NotFound") } if isMember, err := IsOrgMember(ctx, filter, a.ID, userID); err != nil || isMember { diff --git a/internal/command/resource_ower_model.go b/internal/command/resource_owner_model.go similarity index 100% rename from internal/command/resource_ower_model.go rename to internal/command/resource_owner_model.go diff --git a/internal/command/user.go b/internal/command/user.go index 6b65aa83ec..0db4fda328 100644 --- a/internal/command/user.go +++ b/internal/command/user.go @@ -353,21 +353,27 @@ func (c *Commands) userWriteModelByID(ctx context.Context, userID, resourceOwner return writeModel, nil } -func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id, resourceOwner string) (exists bool, err error) { +func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id, resourceOwner string, machineOnly bool) (exists bool, err error) { + eventTypes := []eventstore.EventType{ + user.MachineAddedEventType, + user.UserRemovedType, + } + if !machineOnly { + eventTypes = append(eventTypes, + user.HumanRegisteredType, + user.UserV1RegisteredType, + user.HumanAddedType, + user.UserV1AddedType, + ) + } events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). ResourceOwner(resourceOwner). OrderAsc(). AddQuery(). AggregateTypes(user.AggregateType). AggregateIDs(id). - EventTypes( - user.HumanRegisteredType, - user.UserV1RegisteredType, - user.HumanAddedType, - user.UserV1AddedType, - user.MachineAddedEventType, - user.UserRemovedType, - ).Builder()) + EventTypes(eventTypes...). + Builder()) if err != nil { return false, err } diff --git a/internal/command/user_machine.go b/internal/command/user_machine.go index 1ec32450ac..7c8fd89eac 100644 --- a/internal/command/user_machine.go +++ b/internal/command/user_machine.go @@ -25,6 +25,7 @@ type Machine struct { Name string Description string AccessTokenType domain.OIDCTokenType + PermissionCheck PermissionCheck } func (m *Machine) IsZero() bool { @@ -33,8 +34,8 @@ func (m *Machine) IsZero() bool { func AddMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { - if a.ResourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-xiown2", "Errors.ResourceOwnerMissing") + if a.ResourceOwner == "" && machine.PermissionCheck == nil { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-xiown3", "Errors.ResourceOwnerMissing") } if a.ID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-p0p2mi", "Errors.User.UserIDMissing") @@ -49,7 +50,7 @@ func AddMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validati ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter, machine.PermissionCheck) if err != nil { return nil, err } @@ -67,7 +68,18 @@ func AddMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validati } } -func (c *Commands) AddMachine(ctx context.Context, machine *Machine) (_ *domain.ObjectDetails, err error) { +type addMachineOption func(context.Context, *Machine) error + +func AddMachineWithUsernameToIDFallback() addMachineOption { + return func(ctx context.Context, m *Machine) error { + if m.Username == "" { + m.Username = m.AggregateID + } + return nil + } +} + +func (c *Commands) AddMachine(ctx context.Context, machine *Machine, check PermissionCheck, options ...addMachineOption) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -80,6 +92,16 @@ func (c *Commands) AddMachine(ctx context.Context, machine *Machine) (_ *domain. } agg := user.NewAggregate(machine.AggregateID, machine.ResourceOwner) + for _, option := range options { + if err = option(ctx, machine); err != nil { + return nil, err + } + } + if check != nil { + if err = check(machine.ResourceOwner, machine.AggregateID); err != nil { + return nil, err + } + } cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, AddMachineCommand(agg, machine)) if err != nil { return nil, err @@ -97,6 +119,7 @@ func (c *Commands) AddMachine(ctx context.Context, machine *Machine) (_ *domain. }, nil } +// Deprecated: use ChangeUserMachine instead func (c *Commands) ChangeMachine(ctx context.Context, machine *Machine) (*domain.ObjectDetails, error) { agg := user.NewAggregate(machine.AggregateID, machine.ResourceOwner) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, changeMachineCommand(agg, machine)) @@ -118,24 +141,21 @@ func (c *Commands) ChangeMachine(ctx context.Context, machine *Machine) (*domain func changeMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { - if a.ResourceOwner == "" { + if a.ResourceOwner == "" && machine.PermissionCheck == nil { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-xiown3", "Errors.ResourceOwnerMissing") } if a.ID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-p0p3mi", "Errors.User.UserIDMissing") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter, machine.PermissionCheck) if err != nil { return nil, err } if !isUserStateExists(writeModel.UserState) { return nil, zerrors.ThrowNotFound(nil, "COMMAND-5M0od", "Errors.User.NotFound") } - changedEvent, hasChanged, err := writeModel.NewChangedEvent(ctx, &a.Aggregate, machine.Name, machine.Description, machine.AccessTokenType) - if err != nil { - return nil, err - } + changedEvent, hasChanged := writeModel.NewChangedEvent(ctx, &a.Aggregate, machine.Name, machine.Description, machine.AccessTokenType) if !hasChanged { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-2n8vs", "Errors.User.NotChanged") } @@ -147,10 +167,9 @@ func changeMachineCommand(a *user.Aggregate, machine *Machine) preparation.Valid } } -func getMachineWriteModel(ctx context.Context, userID, resourceOwner string, filter preparation.FilterToQueryReducer) (_ *MachineWriteModel, err error) { +func getMachineWriteModel(ctx context.Context, userID, resourceOwner string, filter preparation.FilterToQueryReducer, permissionCheck PermissionCheck) (_ *MachineWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - writeModel := NewMachineWriteModel(userID, resourceOwner) events, err := filter(ctx, writeModel.Query()) if err != nil { @@ -161,5 +180,10 @@ func getMachineWriteModel(ctx context.Context, userID, resourceOwner string, fil } writeModel.AppendEvents(events...) err = writeModel.Reduce() + if permissionCheck != nil { + if err := permissionCheck(writeModel.ResourceOwner, writeModel.AggregateID); err != nil { + return nil, err + } + } return writeModel, err } diff --git a/internal/command/user_machine_key.go b/internal/command/user_machine_key.go index 8a0f0f437b..d628bf4c2d 100644 --- a/internal/command/user_machine_key.go +++ b/internal/command/user_machine_key.go @@ -15,12 +15,14 @@ import ( ) type AddMachineKey struct { - Type domain.AuthNKeyType - ExpirationDate time.Time + Type domain.AuthNKeyType + ExpirationDate time.Time + PermissionCheck PermissionCheck } type MachineKey struct { models.ObjectRoot + PermissionCheck PermissionCheck KeyID string Type domain.AuthNKeyType @@ -64,7 +66,7 @@ func (key *MachineKey) Detail() ([]byte, error) { } func (key *MachineKey) content() error { - if key.ResourceOwner == "" { + if key.PermissionCheck == nil && key.ResourceOwner == "" { return zerrors.ThrowInvalidArgument(nil, "COMMAND-kqpoix", "Errors.ResourceOwnerMissing") } if key.AggregateID == "" { @@ -91,7 +93,7 @@ func (key *MachineKey) valid() (err error) { } func (key *MachineKey) checkAggregate(ctx context.Context, filter preparation.FilterToQueryReducer) error { - if exists, err := ExistsUser(ctx, filter, key.AggregateID, key.ResourceOwner); err != nil || !exists { + if exists, err := ExistsUser(ctx, filter, key.AggregateID, key.ResourceOwner, true); err != nil || !exists { return zerrors.ThrowPreconditionFailed(err, "COMMAND-bnipwm1", "Errors.User.NotFound") } return nil @@ -142,7 +144,7 @@ func prepareAddUserMachineKey(machineKey *MachineKey, keySize int) preparation.V return nil, err } } - writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner) + writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner, machineKey.PermissionCheck) if err != nil { return nil, err } @@ -186,7 +188,7 @@ func prepareRemoveUserMachineKey(machineKey *MachineKey) preparation.Validation return nil, err } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner) + writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner, machineKey.PermissionCheck) if err != nil { return nil, err } @@ -204,16 +206,18 @@ func prepareRemoveUserMachineKey(machineKey *MachineKey) preparation.Validation } } -func getMachineKeyWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, keyID, resourceOwner string) (_ *MachineKeyWriteModel, err error) { +func getMachineKeyWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, keyID, resourceOwner string, permissionCheck PermissionCheck) (_ *MachineKeyWriteModel, err error) { writeModel := NewMachineKeyWriteModel(userID, keyID, resourceOwner) events, err := filter(ctx, writeModel.Query()) if err != nil { return nil, err } - if len(events) == 0 { - return writeModel, nil - } writeModel.AppendEvents(events...) err = writeModel.Reduce() + if permissionCheck != nil { + if err := permissionCheck(writeModel.ResourceOwner, writeModel.AggregateID); err != nil { + return nil, err + } + } return writeModel, err } diff --git a/internal/command/user_machine_model.go b/internal/command/user_machine_model.go index b7dfb02d32..1ed6c8ca58 100644 --- a/internal/command/user_machine_model.go +++ b/internal/command/user_machine_model.go @@ -2,7 +2,6 @@ package command import ( "context" - "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -106,9 +105,8 @@ func (wm *MachineWriteModel) NewChangedEvent( name, description string, accessTokenType domain.OIDCTokenType, -) (*user.MachineChangedEvent, bool, error) { +) (*user.MachineChangedEvent, bool) { changes := make([]user.MachineChanges, 0) - var err error if wm.Name != name { changes = append(changes, user.ChangeName(name)) @@ -120,11 +118,8 @@ func (wm *MachineWriteModel) NewChangedEvent( changes = append(changes, user.ChangeAccessTokenType(accessTokenType)) } if len(changes) == 0 { - return nil, false, nil + return nil, false } - changeEvent, err := user.NewMachineChangedEvent(ctx, aggregate, changes) - if err != nil { - return nil, false, err - } - return changeEvent, true, nil + changeEvent := user.NewMachineChangedEvent(ctx, aggregate, changes) + return changeEvent, true } diff --git a/internal/command/user_machine_secret.go b/internal/command/user_machine_secret.go index 3349fc90a5..34e9c0c5cc 100644 --- a/internal/command/user_machine_secret.go +++ b/internal/command/user_machine_secret.go @@ -11,7 +11,8 @@ import ( ) type GenerateMachineSecret struct { - ClientSecret string + PermissionCheck PermissionCheck + ClientSecret string } func (c *Commands) GenerateMachineSecret(ctx context.Context, userID string, resourceOwner string, set *GenerateMachineSecret) (*domain.ObjectDetails, error) { @@ -35,14 +36,14 @@ func (c *Commands) GenerateMachineSecret(ctx context.Context, userID string, res func (c *Commands) prepareGenerateMachineSecret(a *user.Aggregate, set *GenerateMachineSecret) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { - if a.ResourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-x0992n", "Errors.ResourceOwnerMissing") + if a.ResourceOwner == "" && set.PermissionCheck == nil { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-0qp2hus", "Errors.ResourceOwnerMissing") } if a.ID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-bzoqjs", "Errors.User.UserIDMissing") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter, set.PermissionCheck) if err != nil { return nil, err } @@ -62,9 +63,10 @@ func (c *Commands) prepareGenerateMachineSecret(a *user.Aggregate, set *Generate } } -func (c *Commands) RemoveMachineSecret(ctx context.Context, userID string, resourceOwner string) (*domain.ObjectDetails, error) { +func (c *Commands) RemoveMachineSecret(ctx context.Context, userID string, resourceOwner string, permissionCheck PermissionCheck) (*domain.ObjectDetails, error) { agg := user.NewAggregate(userID, resourceOwner) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareRemoveMachineSecret(agg)) + //nolint:staticcheck + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareRemoveMachineSecret(agg, permissionCheck)) if err != nil { return nil, err } @@ -81,16 +83,16 @@ func (c *Commands) RemoveMachineSecret(ctx context.Context, userID string, resou }, nil } -func prepareRemoveMachineSecret(a *user.Aggregate) preparation.Validation { +func prepareRemoveMachineSecret(a *user.Aggregate, check PermissionCheck) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { - if a.ResourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-0qp2hus", "Errors.ResourceOwnerMissing") + if a.ResourceOwner == "" && check == nil { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-x0992n", "Errors.ResourceOwnerMissing") } if a.ID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-bzosjs", "Errors.User.UserIDMissing") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter, check) if err != nil { return nil, err } diff --git a/internal/command/user_machine_secret_test.go b/internal/command/user_machine_secret_test.go index 4c6d16960c..8e839efe07 100644 --- a/internal/command/user_machine_secret_test.go +++ b/internal/command/user_machine_secret_test.go @@ -44,7 +44,7 @@ func TestCommandSide_GenerateMachineSecret(t *testing.T) { ctx: context.Background(), userID: "", resourceOwner: "org1", - set: nil, + set: new(GenerateMachineSecret), }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -59,7 +59,7 @@ func TestCommandSide_GenerateMachineSecret(t *testing.T) { ctx: context.Background(), userID: "user1", resourceOwner: "", - set: nil, + set: new(GenerateMachineSecret), }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -76,7 +76,7 @@ func TestCommandSide_GenerateMachineSecret(t *testing.T) { ctx: context.Background(), userID: "user1", resourceOwner: "org1", - set: nil, + set: new(GenerateMachineSecret), }, res: res{ err: zerrors.IsPreconditionFailed, @@ -289,7 +289,7 @@ func TestCommandSide_RemoveMachineSecret(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore, } - got, err := r.RemoveMachineSecret(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) + got, err := r.RemoveMachineSecret(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, nil) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/user_machine_test.go b/internal/command/user_machine_test.go index c7b4b8caf4..19548ae9c6 100644 --- a/internal/command/user_machine_test.go +++ b/internal/command/user_machine_test.go @@ -24,6 +24,8 @@ func TestCommandSide_AddMachine(t *testing.T) { type args struct { ctx context.Context machine *Machine + check PermissionCheck + options func(*Commands) []addMachineOption } type res struct { want *domain.ObjectDetails @@ -194,14 +196,242 @@ func TestCommandSide_AddMachine(t *testing.T) { }, }, }, + { + name: "with username fallback to given username", + fields: fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "aggregateID"), + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + "username", + "name", + "", + true, + domain.OIDCTokenTypeBearer, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Name: "name", + Username: "username", + }, + options: func(commands *Commands) []addMachineOption { + return []addMachineOption{ + AddMachineWithUsernameToIDFallback(), + } + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with username fallback to generated id", + fields: fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "aggregateID"), + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + "aggregateID", + "name", + "", + true, + domain.OIDCTokenTypeBearer, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Name: "name", + }, + options: func(commands *Commands) []addMachineOption { + return []addMachineOption{ + AddMachineWithUsernameToIDFallback(), + } + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with username fallback to given id", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + "aggregateID", + "name", + "", + true, + domain.OIDCTokenTypeBearer, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + AggregateID: "aggregateID", + }, + Name: "name", + }, + options: func(commands *Commands) []addMachineOption { + return []addMachineOption{ + AddMachineWithUsernameToIDFallback(), + } + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with succeeding permission check, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "name", + "description", + true, + domain.OIDCTokenTypeBearer, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Description: "description", + Name: "name", + Username: "username", + }, + check: func(resourceOwner, aggregateID string) error { + return nil + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with failing permission check, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Description: "description", + Name: "name", + Username: "username", + }, + check: func(resourceOwner, aggregateID string) error { + return zerrors.ThrowPermissionDenied(nil, "", "") + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + checkPermission: newMockPermissionCheckAllowed(), } - got, err := r.AddMachine(tt.args.ctx, tt.args.machine) + var options []addMachineOption + if tt.args.options != nil { + options = tt.args.options(r) + } + got, err := r.AddMachine(tt.args.ctx, tt.args.machine, tt.args.check, options...) if tt.res.err == nil { assert.NoError(t, err) } @@ -391,7 +621,7 @@ func TestCommandSide_ChangeMachine(t *testing.T) { } func newMachineChangedEvent(ctx context.Context, userID, resourceOwner, name, description string) *user.MachineChangedEvent { - event, _ := user.NewMachineChangedEvent(ctx, + event := user.NewMachineChangedEvent(ctx, &user.NewAggregate(userID, resourceOwner).Aggregate, []user.MachineChanges{ user.ChangeName(name), diff --git a/internal/command/user_personal_access_token.go b/internal/command/user_personal_access_token.go index 0faf85d5eb..f37953f3d6 100644 --- a/internal/command/user_personal_access_token.go +++ b/internal/command/user_personal_access_token.go @@ -21,6 +21,7 @@ type AddPat struct { type PersonalAccessToken struct { models.ObjectRoot + PermissionCheck PermissionCheck ExpirationDate time.Time Scopes []string @@ -43,7 +44,7 @@ func NewPersonalAccessToken(resourceOwner string, userID string, expirationDate } func (pat *PersonalAccessToken) content() error { - if pat.ResourceOwner == "" { + if pat.ResourceOwner == "" && pat.PermissionCheck == nil { return zerrors.ThrowInvalidArgument(nil, "COMMAND-xs0k2n", "Errors.ResourceOwnerMissing") } if pat.AggregateID == "" { @@ -109,11 +110,10 @@ func prepareAddPersonalAccessToken(pat *PersonalAccessToken, algorithm crypto.En if err := pat.checkAggregate(ctx, filter); err != nil { return nil, err } - writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner) + writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner, pat.PermissionCheck) if err != nil { return nil, err } - pat.Token, err = createToken(algorithm, writeModel.TokenID, writeModel.AggregateID) if err != nil { return nil, err @@ -155,7 +155,7 @@ func prepareRemovePersonalAccessToken(pat *PersonalAccessToken) preparation.Vali return nil, err } return func(ctx context.Context, filter preparation.FilterToQueryReducer) (_ []eventstore.Command, err error) { - writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner) + writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner, pat.PermissionCheck) if err != nil { return nil, err } @@ -181,16 +181,18 @@ func createToken(algorithm crypto.EncryptionAlgorithm, tokenID, userID string) ( return base64.RawURLEncoding.EncodeToString(encrypted), nil } -func getPersonalAccessTokenWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, tokenID, resourceOwner string) (_ *PersonalAccessTokenWriteModel, err error) { +func getPersonalAccessTokenWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, tokenID, resourceOwner string, check PermissionCheck) (_ *PersonalAccessTokenWriteModel, err error) { writeModel := NewPersonalAccessTokenWriteModel(userID, tokenID, resourceOwner) events, err := filter(ctx, writeModel.Query()) if err != nil { return nil, err } - if len(events) == 0 { - return writeModel, nil - } writeModel.AppendEvents(events...) - err = writeModel.Reduce() + if err = writeModel.Reduce(); err != nil { + return nil, err + } + if check != nil { + err = check(writeModel.ResourceOwner, writeModel.AggregateID) + } return writeModel, err } diff --git a/internal/command/user_test.go b/internal/command/user_test.go index 9abae187c1..6a1597fc8b 100644 --- a/internal/command/user_test.go +++ b/internal/command/user_test.go @@ -1813,7 +1813,7 @@ func TestExistsUser(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotExists, err := ExistsUser(context.Background(), tt.args.filter, tt.args.id, tt.args.resourceOwner) + gotExists, err := ExistsUser(context.Background(), tt.args.filter, tt.args.id, tt.args.resourceOwner, false) if (err != nil) != tt.wantErr { t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/internal/command/user_v2.go b/internal/command/user_v2.go index 5f8e8d6ff5..be10fd03fe 100644 --- a/internal/command/user_v2.go +++ b/internal/command/user_v2.go @@ -132,7 +132,6 @@ func (c *Commands) RemoveUserV2(ctx context.Context, userID, resourceOwner strin if userID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-vaipl7s13l", "Errors.User.UserIDMissing") } - existingUser, err := c.userRemoveWriteModel(ctx, userID, resourceOwner) if err != nil { return nil, err @@ -143,7 +142,6 @@ func (c *Commands) RemoveUserV2(ctx context.Context, userID, resourceOwner strin if err := c.checkPermissionDeleteUser(ctx, existingUser.ResourceOwner, existingUser.AggregateID); err != nil { return nil, err } - domainPolicy, err := c.domainPolicyWriteModel(ctx, existingUser.ResourceOwner) if err != nil { return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-l40ykb3xh2", "Errors.Org.DomainPolicy.NotExisting") diff --git a/internal/command/user_v2_human.go b/internal/command/user_v2_human.go index f88e2017d5..0945ae7578 100644 --- a/internal/command/user_v2_human.go +++ b/internal/command/user_v2_human.go @@ -5,6 +5,7 @@ import ( "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -121,7 +122,10 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human if resourceOwner == "" { return zerrors.ThrowInvalidArgument(nil, "COMMA-095xh8fll1", "Errors.Internal") } - + if human.Details == nil { + human.Details = &domain.ObjectDetails{} + } + human.Details.ResourceOwner = resourceOwner if err := human.Validate(c.userPasswordHasher); err != nil { return err } @@ -132,7 +136,12 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human return err } } - + // check for permission to create user on resourceOwner + if !human.Register { + if err := c.checkPermissionUpdateUser(ctx, resourceOwner, human.ID); err != nil { + return err + } + } // only check if user is already existing existingHuman, err := c.userExistsWriteModel( ctx, @@ -144,12 +153,6 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human if isUserStateExists(existingHuman.UserState) { return zerrors.ThrowPreconditionFailed(nil, "COMMAND-7yiox1isql", "Errors.User.AlreadyExisting") } - // check for permission to create user on resourceOwner - if !human.Register { - if err := c.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, human.ID); err != nil { - return err - } - } // add resourceowner for the events with the aggregate existingHuman.ResourceOwner = resourceOwner @@ -161,6 +164,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human if err = c.userValidateDomain(ctx, resourceOwner, human.Username, domainPolicy.UserLoginMustBeDomain); err != nil { return err } + var createCmd humanCreationCommand if human.Register { createCmd = user.NewHumanRegisteredEvent( @@ -203,17 +207,33 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human return err } - cmds := make([]eventstore.Command, 0, 3) - cmds = append(cmds, createCmd) - - cmds, err = c.addHumanCommandEmail(ctx, filter, cmds, existingHuman.Aggregate(), human, alg, allowInitMail) + cmds, err := c.addUserHumanCommands(ctx, filter, existingHuman, human, allowInitMail, alg, createCmd) if err != nil { return err } + if len(cmds) == 0 { + human.Details = writeModelToObjectDetails(&existingHuman.WriteModel) + return nil + } + err = c.pushAppendAndReduce(ctx, existingHuman, cmds...) + if err != nil { + return err + } + human.Details = writeModelToObjectDetails(&existingHuman.WriteModel) + return nil +} + +func (c *Commands) addUserHumanCommands(ctx context.Context, filter preparation.FilterToQueryReducer, existingHuman *UserV2WriteModel, human *AddHuman, allowInitMail bool, alg crypto.EncryptionAlgorithm, addUserCommand eventstore.Command) ([]eventstore.Command, error) { + cmds := []eventstore.Command{addUserCommand} + var err error + cmds, err = c.addHumanCommandEmail(ctx, filter, cmds, existingHuman.Aggregate(), human, alg, allowInitMail) + if err != nil { + return nil, err + } cmds, err = c.addHumanCommandPhone(ctx, filter, cmds, existingHuman.Aggregate(), human, alg) if err != nil { - return err + return nil, err } for _, metadataEntry := range human.Metadata { @@ -227,7 +247,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human for _, link := range human.Links { cmd, err := addLink(ctx, filter, existingHuman.Aggregate(), link) if err != nil { - return err + return nil, err } cmds = append(cmds, cmd) } @@ -235,7 +255,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human if human.TOTPSecret != "" { encryptedSecret, err := crypto.Encrypt([]byte(human.TOTPSecret), c.multifactors.OTP.CryptoMFA) if err != nil { - return err + return nil, err } cmds = append(cmds, user.NewHumanOTPAddedEvent(ctx, &existingHuman.Aggregate().Aggregate, encryptedSecret), @@ -246,18 +266,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human if human.SetInactive { cmds = append(cmds, user.NewUserDeactivatedEvent(ctx, &existingHuman.Aggregate().Aggregate)) } - - if len(cmds) == 0 { - human.Details = writeModelToObjectDetails(&existingHuman.WriteModel) - return nil - } - - err = c.pushAppendAndReduce(ctx, existingHuman, cmds...) - if err != nil { - return err - } - human.Details = writeModelToObjectDetails(&existingHuman.WriteModel) - return nil + return cmds, nil } func (c *Commands) ChangeUserHuman(ctx context.Context, human *ChangeHuman, alg crypto.EncryptionAlgorithm) (err error) { @@ -341,7 +350,6 @@ func (c *Commands) ChangeUserHuman(ctx context.Context, human *ChangeHuman, alg if human.State != nil { // only allow toggling between active and inactive // any other target state is not supported - // the existing human's state has to be the switch { case isUserStateActive(*human.State): if isUserStateActive(existingHuman.UserState) { diff --git a/internal/command/user_v2_human_test.go b/internal/command/user_v2_human_test.go index 2b4399fb2a..e44e182b92 100644 --- a/internal/command/user_v2_human_test.go +++ b/internal/command/user_v2_human_test.go @@ -302,9 +302,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { { name: "add human (with initial code), no permission", fields: fields{ - eventstore: expectEventstore( - expectFilter(), - ), + eventstore: expectEventstore(), checkPermission: newMockPermissionCheckNotAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), newCode: mockEncryptedCode("userinit", time.Hour), @@ -326,9 +324,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, res: res{ - err: func(err error) bool { - return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) - }, + err: zerrors.IsPermissionDenied, }, }, { diff --git a/internal/command/user_v2_invite_test.go b/internal/command/user_v2_invite_test.go index 04c00d876e..75bd3157db 100644 --- a/internal/command/user_v2_invite_test.go +++ b/internal/command/user_v2_invite_test.go @@ -352,6 +352,7 @@ func TestCommands_ResendInviteCode(t *testing.T) { "user does not exist", fields{ eventstore: expectEventstore( + // The write model doesn't query any events expectFilter(), ), checkPermission: newMockPermissionCheckAllowed(), diff --git a/internal/command/user_v2_machine.go b/internal/command/user_v2_machine.go new file mode 100644 index 0000000000..34079b7e6f --- /dev/null +++ b/internal/command/user_v2_machine.go @@ -0,0 +1,94 @@ +package command + +import ( + "context" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/user" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type ChangeMachine struct { + ID string + ResourceOwner string + Username *string + Name *string + Description *string + + // Details are set after a successful execution of the command + Details *domain.ObjectDetails +} + +func (h *ChangeMachine) Changed() bool { + if h.Username != nil { + return true + } + if h.Name != nil { + return true + } + if h.Description != nil { + return true + } + return false +} + +func (c *Commands) ChangeUserMachine(ctx context.Context, machine *ChangeMachine) (err error) { + existingMachine, err := c.UserMachineWriteModel( + ctx, + machine.ID, + machine.ResourceOwner, + false, + ) + if err != nil { + return err + } + if machine.Changed() { + if err := c.checkPermissionUpdateUser(ctx, existingMachine.ResourceOwner, existingMachine.AggregateID); err != nil { + return err + } + } + + cmds := make([]eventstore.Command, 0) + if machine.Username != nil { + cmds, err = c.changeUsername(ctx, cmds, existingMachine, *machine.Username) + if err != nil { + return err + } + } + var machineChanges []user.MachineChanges + if machine.Name != nil && *machine.Name != existingMachine.Name { + machineChanges = append(machineChanges, user.ChangeName(*machine.Name)) + } + if machine.Description != nil && *machine.Description != existingMachine.Description { + machineChanges = append(machineChanges, user.ChangeDescription(*machine.Description)) + } + if len(machineChanges) > 0 { + cmds = append(cmds, user.NewMachineChangedEvent(ctx, &existingMachine.Aggregate().Aggregate, machineChanges)) + } + if len(cmds) == 0 { + machine.Details = writeModelToObjectDetails(&existingMachine.WriteModel) + return nil + } + err = c.pushAppendAndReduce(ctx, existingMachine, cmds...) + if err != nil { + return err + } + machine.Details = writeModelToObjectDetails(&existingMachine.WriteModel) + return nil +} + +func (c *Commands) UserMachineWriteModel(ctx context.Context, userID, resourceOwner string, metadataWM bool) (writeModel *UserV2WriteModel, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + writeModel = NewUserMachineWriteModel(userID, resourceOwner, metadataWM) + err = c.eventstore.FilterToQueryReducer(ctx, writeModel) + if err != nil { + return nil, err + } + if !isUserStateExists(writeModel.UserState) { + return nil, zerrors.ThrowNotFound(nil, "COMMAND-ugjs0upun6", "Errors.User.NotFound") + } + return writeModel, nil +} diff --git a/internal/command/user_v2_machine_test.go b/internal/command/user_v2_machine_test.go new file mode 100644 index 0000000000..14df4bfae7 --- /dev/null +++ b/internal/command/user_v2_machine_test.go @@ -0,0 +1,260 @@ +package command + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + + "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" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestCommandSide_ChangeUserMachine(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + ctx context.Context + orgID string + machine *ChangeMachine + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + + userAgg := user.NewAggregate("user1", "org1") + userAddedEvent := user.NewMachineAddedEvent(context.Background(), + &userAgg.Aggregate, + "username", + "name", + "description", + true, + domain.OIDCTokenTypeBearer, + ) + + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "change machine username, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Username: gu.Ptr("changed"), + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + { + name: "change machine username, not found", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Username: gu.Ptr("changed"), + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-ugjs0upun6", "Errors.User.NotFound")) + }, + }, + }, + { + name: "change machine username, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &userAgg.Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewUsernameChangedEvent(context.Background(), + &userAgg.Aggregate, + "username", + "changed", + true, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Username: gu.Ptr("changed"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + Sequence: 0, + EventDate: time.Time{}, + ResourceOwner: "org1", + }, + }, + }, + { + name: "change machine username, no change", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Username: gu.Ptr("username"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + Sequence: 0, + EventDate: time.Time{}, + ResourceOwner: "org1", + }, + }, + }, + { + name: "change machine description, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Description: gu.Ptr("changed"), + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + { + name: "change machine description, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + expectPush( + user.NewMachineChangedEvent(context.Background(), + &userAgg.Aggregate, + []user.MachineChanges{ + user.ChangeDescription("changed"), + }, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Description: gu.Ptr("changed"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "change machine description, no change", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Description: gu.Ptr("description"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + err := r.ChangeUserMachine(tt.args.ctx, tt.args.machine) + if tt.res.err == nil { + if !assert.NoError(t, err) { + t.FailNow() + } + } else if !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + return + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.want, tt.args.machine.Details) + } + }) + } +} diff --git a/internal/command/user_v2_model.go b/internal/command/user_v2_model.go index 214a2a5f9d..92346bf3b6 100644 --- a/internal/command/user_v2_model.go +++ b/internal/command/user_v2_model.go @@ -118,6 +118,14 @@ func NewUserHumanWriteModel(userID, resourceOwner string, profileWM, emailWM, ph return newUserV2WriteModel(userID, resourceOwner, opts...) } +func NewUserMachineWriteModel(userID, resourceOwner string, metadataListWM bool) *UserV2WriteModel { + opts := []UserV2WMOption{WithMachine(), WithState()} + if metadataListWM { + opts = append(opts, WithMetadata()) + } + return newUserV2WriteModel(userID, resourceOwner, opts...) +} + func newUserV2WriteModel(userID, resourceOwner string, opts ...UserV2WMOption) *UserV2WriteModel { wm := &UserV2WriteModel{ WriteModel: eventstore.WriteModel{ diff --git a/internal/domain/permission.go b/internal/domain/permission.go index fd300f63b9..bb569955f5 100644 --- a/internal/domain/permission.go +++ b/internal/domain/permission.go @@ -24,7 +24,7 @@ func (p *Permissions) appendPermission(ctxID, permission string) { p.Permissions = append(p.Permissions, permission) } -type PermissionCheck func(ctx context.Context, permission, orgID, resourceID string) (err error) +type PermissionCheck func(ctx context.Context, permission, resourceOwnerID, aggregateID string) (err error) const ( PermissionUserWrite = "user.write" diff --git a/internal/eventstore/write_model.go b/internal/eventstore/write_model.go index 277e65ed82..965fb16d0e 100644 --- a/internal/eventstore/write_model.go +++ b/internal/eventstore/write_model.go @@ -1,6 +1,8 @@ package eventstore -import "time" +import ( + "time" +) // WriteModel is the minimum representation of a command side write model. // It implements a basic reducer @@ -27,21 +29,25 @@ func (wm *WriteModel) Reduce() error { return nil } + latestEvent := wm.Events[len(wm.Events)-1] if wm.AggregateID == "" { - wm.AggregateID = wm.Events[0].Aggregate().ID - } - if wm.ResourceOwner == "" { - wm.ResourceOwner = wm.Events[0].Aggregate().ResourceOwner - } - if wm.InstanceID == "" { - wm.InstanceID = wm.Events[0].Aggregate().InstanceID + wm.AggregateID = latestEvent.Aggregate().ID } - wm.ProcessedSequence = wm.Events[len(wm.Events)-1].Sequence() - wm.ChangeDate = wm.Events[len(wm.Events)-1].CreatedAt() + if wm.ResourceOwner == "" { + wm.ResourceOwner = latestEvent.Aggregate().ResourceOwner + } + + if wm.InstanceID == "" { + wm.InstanceID = latestEvent.Aggregate().InstanceID + } + + wm.ProcessedSequence = latestEvent.Sequence() + wm.ChangeDate = latestEvent.CreatedAt() // all events processed and not needed anymore wm.Events = nil wm.Events = []Event{} + return nil } diff --git a/internal/integration/client.go b/internal/integration/client.go index 320809a7e8..3bf794f5f6 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -141,6 +141,7 @@ func (c *Client) pollHealth(ctx context.Context) (err error) { } } +// Deprecated: use CreateUserTypeHuman instead func (i *Instance) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserResponse { resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ @@ -172,6 +173,7 @@ func (i *Instance) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserRes return resp } +// Deprecated: user CreateUserTypeHuman instead func (i *Instance) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHumanUserResponse { resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ @@ -197,6 +199,7 @@ func (i *Instance) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHuman return resp } +// Deprecated: user CreateUserTypeHuman instead func (i *Instance) CreateHumanUserWithTOTP(ctx context.Context, secret string) *user_v2.AddHumanUserResponse { resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ @@ -229,6 +232,43 @@ func (i *Instance) CreateHumanUserWithTOTP(ctx context.Context, secret string) * return resp } +func (i *Instance) CreateUserTypeHuman(ctx context.Context) *user_v2.CreateUserResponse { + resp, err := i.Client.UserV2.CreateUser(ctx, &user_v2.CreateUserRequest{ + OrganizationId: i.DefaultOrg.GetId(), + UserType: &user_v2.CreateUserRequest_Human_{ + Human: &user_v2.CreateUserRequest_Human{ + Profile: &user_v2.SetHumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + }, + Email: &user_v2.SetHumanEmail{ + Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()), + Verification: &user_v2.SetHumanEmail_ReturnCode{ + ReturnCode: &user_v2.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + }) + logging.OnError(err).Panic("create human user") + i.TriggerUserByID(ctx, resp.GetId()) + return resp +} + +func (i *Instance) CreateUserTypeMachine(ctx context.Context) *user_v2.CreateUserResponse { + resp, err := i.Client.UserV2.CreateUser(ctx, &user_v2.CreateUserRequest{ + OrganizationId: i.DefaultOrg.GetId(), + UserType: &user_v2.CreateUserRequest_Machine_{ + Machine: &user_v2.CreateUserRequest_Machine{ + Name: "machine", + }, + }, + }) + logging.OnError(err).Panic("create machine user") + i.TriggerUserByID(ctx, resp.GetId()) + return resp +} + // TriggerUserByID makes sure the user projection gets triggered after creation. func (i *Instance) TriggerUserByID(ctx context.Context, users ...string) { var wg sync.WaitGroup diff --git a/internal/query/authn_key.go b/internal/query/authn_key.go index 8075422e63..ffbe38e7ae 100644 --- a/internal/query/authn_key.go +++ b/internal/query/authn_key.go @@ -5,6 +5,7 @@ import ( "database/sql" _ "embed" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" @@ -18,6 +19,14 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) +func keysCheckPermission(ctx context.Context, keys *AuthNKeys, permissionCheck domain.PermissionCheck) { + keys.AuthNKeys = slices.DeleteFunc(keys.AuthNKeys, + func(key *AuthNKey) bool { + return userCheckPermission(ctx, key.ResourceOwner, key.AggregateID, permissionCheck) != nil + }, + ) +} + var ( authNKeyTable = table{ name: projection.AuthNKeyTable, @@ -84,6 +93,7 @@ type AuthNKeys struct { type AuthNKey struct { ID string + AggregateID string CreationDate time.Time ChangeDate time.Time ResourceOwner string @@ -124,12 +134,47 @@ func (q *AuthNKeySearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder return query } -func (q *Queries) SearchAuthNKeys(ctx context.Context, queries *AuthNKeySearchQueries, withOwnerRemoved bool) (authNKeys *AuthNKeys, err error) { +type JoinFilter int + +const ( + JoinFilterUnspecified JoinFilter = iota + JoinFilterApp + JoinFilterUserMachine +) + +// SearchAuthNKeys returns machine or app keys, depending on the join filter. +// If permissionCheck is nil, the keys are not filtered. +// If permissionCheck is not nil and the PermissionCheckV2 feature flag is false, the returned keys are filtered in-memory by the given permission check. +// If permissionCheck is not nil and the PermissionCheckV2 feature flag is true, the returned keys are filtered in the database. +func (q *Queries) SearchAuthNKeys(ctx context.Context, queries *AuthNKeySearchQueries, filter JoinFilter, permissionCheck domain.PermissionCheck) (authNKeys *AuthNKeys, err error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + keys, err := q.searchAuthNKeys(ctx, queries, filter, permissionCheckV2) + if err != nil { + return nil, err + } + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + keysCheckPermission(ctx, keys, permissionCheck) + } + return keys, nil +} + +func (q *Queries) searchAuthNKeys(ctx context.Context, queries *AuthNKeySearchQueries, joinFilter JoinFilter, permissionCheckV2 bool) (authNKeys *AuthNKeys, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := prepareAuthNKeysQuery() query = queries.toQuery(query) + switch joinFilter { + case JoinFilterUnspecified: + // Select all authN keys + case JoinFilterApp: + joinCol := ProjectColumnID + query = query.Join(joinCol.table.identifier() + " ON " + AuthNKeyColumnIdentifier.identifier() + " = " + joinCol.identifier()) + case JoinFilterUserMachine: + joinCol := MachineUserIDCol + query = query.Join(joinCol.table.identifier() + " ON " + AuthNKeyColumnIdentifier.identifier() + " = " + joinCol.identifier()) + query = userPermissionCheckV2WithCustomColumns(ctx, query, permissionCheckV2, queries.Queries, AuthNKeyColumnResourceOwner, AuthNKeyColumnIdentifier) + } eq := sq.Eq{ AuthNKeyColumnEnabled.identifier(): true, AuthNKeyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -249,6 +294,22 @@ func NewAuthNKeyObjectIDQuery(id string) (SearchQuery, error) { return NewTextQuery(AuthNKeyColumnObjectID, id, TextEquals) } +func NewAuthNKeyIDQuery(id string) (SearchQuery, error) { + return NewTextQuery(AuthNKeyColumnID, id, TextEquals) +} + +func NewAuthNKeyIdentifyerQuery(id string) (SearchQuery, error) { + return NewTextQuery(AuthNKeyColumnIdentifier, id, TextEquals) +} + +func NewAuthNKeyCreationDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) { + return NewTimestampQuery(AuthNKeyColumnCreationDate, ts, compare) +} + +func NewAuthNKeyExpirationDateDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) { + return NewTimestampQuery(AuthNKeyColumnExpiration, ts, compare) +} + //go:embed authn_key_user.sql var authNKeyUserQuery string @@ -288,49 +349,52 @@ func (q *Queries) GetAuthNKeyUser(ctx context.Context, keyID, userID string) (_ } func prepareAuthNKeysQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeys, error)) { - return sq.Select( - AuthNKeyColumnID.identifier(), - AuthNKeyColumnCreationDate.identifier(), - AuthNKeyColumnChangeDate.identifier(), - AuthNKeyColumnResourceOwner.identifier(), - AuthNKeyColumnSequence.identifier(), - AuthNKeyColumnExpiration.identifier(), - AuthNKeyColumnType.identifier(), - countColumn.identifier(), - ).From(authNKeyTable.identifier()). - PlaceholderFormat(sq.Dollar), - func(rows *sql.Rows) (*AuthNKeys, error) { - authNKeys := make([]*AuthNKey, 0) - var count uint64 - for rows.Next() { - authNKey := new(AuthNKey) - err := rows.Scan( - &authNKey.ID, - &authNKey.CreationDate, - &authNKey.ChangeDate, - &authNKey.ResourceOwner, - &authNKey.Sequence, - &authNKey.Expiration, - &authNKey.Type, - &count, - ) - if err != nil { - return nil, err - } - authNKeys = append(authNKeys, authNKey) - } + query := sq.Select( + AuthNKeyColumnID.identifier(), + AuthNKeyColumnAggregateID.identifier(), + AuthNKeyColumnCreationDate.identifier(), + AuthNKeyColumnChangeDate.identifier(), + AuthNKeyColumnResourceOwner.identifier(), + AuthNKeyColumnSequence.identifier(), + AuthNKeyColumnExpiration.identifier(), + AuthNKeyColumnType.identifier(), + countColumn.identifier(), + ).From(authNKeyTable.identifier()). + PlaceholderFormat(sq.Dollar) - if err := rows.Close(); err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Dgfn3", "Errors.Query.CloseRows") + return query, func(rows *sql.Rows) (*AuthNKeys, error) { + authNKeys := make([]*AuthNKey, 0) + var count uint64 + for rows.Next() { + authNKey := new(AuthNKey) + err := rows.Scan( + &authNKey.ID, + &authNKey.AggregateID, + &authNKey.CreationDate, + &authNKey.ChangeDate, + &authNKey.ResourceOwner, + &authNKey.Sequence, + &authNKey.Expiration, + &authNKey.Type, + &count, + ) + if err != nil { + return nil, err } - - return &AuthNKeys{ - AuthNKeys: authNKeys, - SearchResponse: SearchResponse{ - Count: count, - }, - }, nil + authNKeys = append(authNKeys, authNKey) } + + if err := rows.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-Dgfn3", "Errors.Query.CloseRows") + } + + return &AuthNKeys{ + AuthNKeys: authNKeys, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil + } } func prepareAuthNKeyQuery() (sq.SelectBuilder, func(row *sql.Row) (*AuthNKey, error)) { diff --git a/internal/query/authn_key_test.go b/internal/query/authn_key_test.go index c7441f8dae..b7c66cc665 100644 --- a/internal/query/authn_key_test.go +++ b/internal/query/authn_key_test.go @@ -19,6 +19,7 @@ import ( var ( prepareAuthNKeysStmt = `SELECT projections.authn_keys2.id,` + + ` projections.authn_keys2.aggregate_id,` + ` projections.authn_keys2.creation_date,` + ` projections.authn_keys2.change_date,` + ` projections.authn_keys2.resource_owner,` + @@ -29,6 +30,7 @@ var ( ` FROM projections.authn_keys2` prepareAuthNKeysCols = []string{ "id", + "aggregate_id", "creation_date", "change_date", "resource_owner", @@ -120,6 +122,7 @@ func Test_AuthNKeyPrepares(t *testing.T) { [][]driver.Value{ { "id", + "aggId", testNow, testNow, "ro", @@ -137,6 +140,7 @@ func Test_AuthNKeyPrepares(t *testing.T) { AuthNKeys: []*AuthNKey{ { ID: "id", + AggregateID: "aggId", CreationDate: testNow, ChangeDate: testNow, ResourceOwner: "ro", @@ -157,6 +161,7 @@ func Test_AuthNKeyPrepares(t *testing.T) { [][]driver.Value{ { "id-1", + "aggId-1", testNow, testNow, "ro", @@ -166,6 +171,7 @@ func Test_AuthNKeyPrepares(t *testing.T) { }, { "id-2", + "aggId-2", testNow, testNow, "ro", @@ -183,6 +189,7 @@ func Test_AuthNKeyPrepares(t *testing.T) { AuthNKeys: []*AuthNKey{ { ID: "id-1", + AggregateID: "aggId-1", CreationDate: testNow, ChangeDate: testNow, ResourceOwner: "ro", @@ -192,6 +199,7 @@ func Test_AuthNKeyPrepares(t *testing.T) { }, { ID: "id-2", + AggregateID: "aggId-2", CreationDate: testNow, ChangeDate: testNow, ResourceOwner: "ro", diff --git a/internal/query/projection/authn_key.go b/internal/query/projection/authn_key.go index e2229ad332..a287701cfb 100644 --- a/internal/query/projection/authn_key.go +++ b/internal/query/projection/authn_key.go @@ -62,6 +62,9 @@ func (*authNKeyProjection) Init() *old_handler.Check { handler.NewPrimaryKey(AuthNKeyInstanceIDCol, AuthNKeyIDCol), handler.WithIndex(handler.NewIndex("enabled", []string{AuthNKeyEnabledCol})), handler.WithIndex(handler.NewIndex("identifier", []string{AuthNKeyIdentifierCol})), + handler.WithIndex(handler.NewIndex("resource_owner", []string{AuthNKeyResourceOwnerCol})), + handler.WithIndex(handler.NewIndex("creation_date", []string{AuthNKeyCreationDateCol})), + handler.WithIndex(handler.NewIndex("expiration_date", []string{AuthNKeyExpirationCol})), ), ) } diff --git a/internal/query/projection/user_personal_access_token.go b/internal/query/projection/user_personal_access_token.go index 0efb5d6412..610ca9c4e2 100644 --- a/internal/query/projection/user_personal_access_token.go +++ b/internal/query/projection/user_personal_access_token.go @@ -56,6 +56,8 @@ func (*personalAccessTokenProjection) Init() *old_handler.Check { handler.WithIndex(handler.NewIndex("user_id", []string{PersonalAccessTokenColumnUserID})), handler.WithIndex(handler.NewIndex("resource_owner", []string{PersonalAccessTokenColumnResourceOwner})), handler.WithIndex(handler.NewIndex("owner_removed", []string{PersonalAccessTokenColumnOwnerRemoved})), + handler.WithIndex(handler.NewIndex("creation_date", []string{PersonalAccessTokenColumnCreationDate})), + handler.WithIndex(handler.NewIndex("expiration_date", []string{PersonalAccessTokenColumnExpiration})), ), ) } diff --git a/internal/query/user.go b/internal/query/user.go index 3d47847cac..6844982f07 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -132,16 +132,20 @@ func usersCheckPermission(ctx context.Context, users *Users, permissionCheck dom ) } -func userPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *UserSearchQueries) sq.SelectBuilder { +func userPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, filters []SearchQuery) sq.SelectBuilder { + return userPermissionCheckV2WithCustomColumns(ctx, query, enabled, filters, UserResourceOwnerCol, UserIDCol) +} + +func userPermissionCheckV2WithCustomColumns(ctx context.Context, query sq.SelectBuilder, enabled bool, filters []SearchQuery, userResourceOwnerCol, userID Column) sq.SelectBuilder { if !enabled { return query } join, args := PermissionClause( ctx, - UserResourceOwnerCol, + userResourceOwnerCol, domain.PermissionUserRead, - SingleOrgPermissionOption(queries.Queries), - OwnedRowsPermissionOption(UserIDCol), + SingleOrgPermissionOption(filters), + OwnedRowsPermissionOption(userID), ) return query.JoinClause(join, args...) } @@ -637,7 +641,7 @@ func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries, p defer func() { span.EndWithError(err) }() query, scan := prepareUsersQuery() - query = userPermissionCheckV2(ctx, query, permissionCheckV2, queries) + query = userPermissionCheckV2(ctx, query, permissionCheckV2, queries.Queries) stmt, args, err := queries.toQuery(query).Where(sq.Eq{ UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), }).ToSql() diff --git a/internal/query/user_personal_access_token.go b/internal/query/user_personal_access_token.go index 8ea33f51a4..61d349961c 100644 --- a/internal/query/user_personal_access_token.go +++ b/internal/query/user_personal_access_token.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" @@ -11,12 +12,21 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" ) +func patsCheckPermission(ctx context.Context, tokens *PersonalAccessTokens, permissionCheck domain.PermissionCheck) { + tokens.PersonalAccessTokens = slices.DeleteFunc(tokens.PersonalAccessTokens, + func(token *PersonalAccessToken) bool { + return userCheckPermission(ctx, token.ResourceOwner, token.UserID, permissionCheck) != nil + }, + ) +} + var ( personalAccessTokensTable = table{ name: projection.PersonalAccessTokenProjectionTable, @@ -86,7 +96,7 @@ type PersonalAccessTokenSearchQueries struct { Queries []SearchQuery } -func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk bool, id string, withOwnerRemoved bool, queries ...SearchQuery) (pat *PersonalAccessToken, err error) { +func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk bool, id string, queries ...SearchQuery) (pat *PersonalAccessToken, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -102,11 +112,9 @@ func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk query = q.toQuery(query) } eq := sq.Eq{ - PersonalAccessTokenColumnID.identifier(): id, - PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), - } - if !withOwnerRemoved { - eq[PersonalAccessTokenColumnOwnerRemoved.identifier()] = false + PersonalAccessTokenColumnID.identifier(): id, + PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + PersonalAccessTokenColumnOwnerRemoved.identifier(): false, } stmt, args, err := query.Where(eq).ToSql() if err != nil { @@ -123,18 +131,34 @@ func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk return pat, nil } -func (q *Queries) SearchPersonalAccessTokens(ctx context.Context, queries *PersonalAccessTokenSearchQueries, withOwnerRemoved bool) (personalAccessTokens *PersonalAccessTokens, err error) { +// SearchPersonalAccessTokens returns personal access token resources. +// If permissionCheck is nil, the PATs are not filtered. +// If permissionCheck is not nil and the PermissionCheckV2 feature flag is false, the returned PATs are filtered in-memory by the given permission check. +// If permissionCheck is not nil and the PermissionCheckV2 feature flag is true, the returned PATs are filtered in the database. +func (q *Queries) SearchPersonalAccessTokens(ctx context.Context, queries *PersonalAccessTokenSearchQueries, permissionCheck domain.PermissionCheck) (authNKeys *PersonalAccessTokens, err error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + keys, err := q.searchPersonalAccessTokens(ctx, queries, permissionCheckV2) + if err != nil { + return nil, err + } + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + patsCheckPermission(ctx, keys, permissionCheck) + } + return keys, nil +} + +func (q *Queries) searchPersonalAccessTokens(ctx context.Context, queries *PersonalAccessTokenSearchQueries, permissionCheckV2 bool) (personalAccessTokens *PersonalAccessTokens, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := preparePersonalAccessTokensQuery() + query = queries.toQuery(query) + query = userPermissionCheckV2WithCustomColumns(ctx, query, permissionCheckV2, queries.Queries, PersonalAccessTokenColumnResourceOwner, PersonalAccessTokenColumnUserID) eq := sq.Eq{ - PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + PersonalAccessTokenColumnOwnerRemoved.identifier(): false, } - if !withOwnerRemoved { - eq[PersonalAccessTokenColumnOwnerRemoved.identifier()] = false - } - stmt, args, err := queries.toQuery(query).Where(eq).ToSql() + stmt, args, err := query.Where(eq).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-Hjw2w", "Errors.Query.InvalidRequest") } @@ -160,6 +184,18 @@ func NewPersonalAccessTokenUserIDSearchQuery(value string) (SearchQuery, error) return NewTextQuery(PersonalAccessTokenColumnUserID, value, TextEquals) } +func NewPersonalAccessTokenIDQuery(id string) (SearchQuery, error) { + return NewTextQuery(PersonalAccessTokenColumnID, id, TextEquals) +} + +func NewPersonalAccessTokenCreationDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) { + return NewTimestampQuery(PersonalAccessTokenColumnCreationDate, ts, compare) +} + +func NewPersonalAccessTokenExpirationDateDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) { + return NewTimestampQuery(PersonalAccessTokenColumnExpiration, ts, compare) +} + func (r *PersonalAccessTokenSearchQueries) AppendMyResourceOwnerQuery(orgID string) error { query, err := NewPersonalAccessTokenResourceOwnerSearchQuery(orgID) if err != nil { diff --git a/internal/repository/user/machine.go b/internal/repository/user/machine.go index d76290931a..a466f92fe3 100644 --- a/internal/repository/user/machine.go +++ b/internal/repository/user/machine.go @@ -88,10 +88,7 @@ func NewMachineChangedEvent( ctx context.Context, aggregate *eventstore.Aggregate, changes []MachineChanges, -) (*MachineChangedEvent, error) { - if len(changes) == 0 { - return nil, zerrors.ThrowPreconditionFailed(nil, "USER-3M9fs", "Errors.NoChangesFound") - } +) *MachineChangedEvent { changeEvent := &MachineChangedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( ctx, @@ -102,7 +99,7 @@ func NewMachineChangedEvent( for _, change := range changes { change(changeEvent) } - return changeEvent, nil + return changeEvent } type MachineChanges func(event *MachineChangedEvent) diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index 8254b82b45..d58b2eb64a 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -117,6 +117,7 @@ Errors: AlreadyVerified: Телефонът вече е потвърден Empty: Телефонът е празен NotChanged: Телефонът не е сменен + VerifyingRemovalIsNotSupported: Премахването на проверката не се поддържа Address: NotFound: Адресът не е намерен NotChanged: Адресът не е променен diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index bb4172fbff..d248ce4ca7 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telefon již ověřen Empty: Telefon je prázdný NotChanged: Telefon nezměněn + VerifyingRemovalIsNotSupported: Ověření odstranění telefonu není podporováno Address: NotFound: Adresa nenalezena NotChanged: Adresa nezměněna diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index a24ce7c933..96edf57456 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telefonnummer bereits verifiziert Empty: Telefonnummer ist leer NotChanged: Telefonnummer wurde nicht geändert + VerifyingRemovalIsNotSupported: Verifizieren der Telefonnummer Entfernung wird nicht unterstützt Address: NotFound: Adresse nicht gefunden NotChanged: Adresse wurde nicht geändert diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index e8f2781de1..0f512defe4 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -116,6 +116,7 @@ Errors: AlreadyVerified: Phone already verified Empty: Phone is empty NotChanged: Phone not changed + VerifyingRemovalIsNotSupported: Verifying phone removal is not supported Address: NotFound: Address not found NotChanged: Address not changed diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index b91d055f70..8c901f8ebe 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: El teléfono ya se verificó Empty: El teléfono está vacío NotChanged: El teléfono no ha cambiado + VerifyingRemovalIsNotSupported: La verificación de eliminación no está soportada Address: NotFound: Dirección no encontrada NotChanged: La dirección no ha cambiado diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 98f2bee9a0..2a2a51d7c4 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Téléphone déjà vérifié Empty: Téléphone est vide NotChanged: Téléphone n'a pas changé + VerifyingRemovalIsNotSupported: La vérification de la suppression n'est pas prise en charge Address: NotFound: Adresse non trouvée NotChanged: L'adresse n'a pas changé diff --git a/internal/static/i18n/hu.yaml b/internal/static/i18n/hu.yaml index 5becd6e606..a4cc908fa2 100644 --- a/internal/static/i18n/hu.yaml +++ b/internal/static/i18n/hu.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telefon már ellenőrizve Empty: A telefon mező üres NotChanged: Telefon nem lett megváltoztatva + VerifyingRemovalIsNotSupported: A telefon eltávolításának ellenőrzése nem támogatott Address: NotFound: Cím nem található NotChanged: Cím nem lett megváltoztatva diff --git a/internal/static/i18n/id.yaml b/internal/static/i18n/id.yaml index 0108d7618b..c9187020f7 100644 --- a/internal/static/i18n/id.yaml +++ b/internal/static/i18n/id.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telepon sudah diverifikasi Empty: Telepon kosong NotChanged: Telepon tidak berubah + VerifyingRemovalIsNotSupported: Verifikasi penghapusan tidak didukung Address: NotFound: Alamat tidak ditemukan NotChanged: Alamat tidak berubah diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 750c48471a..d1dccef4c7 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telefono già verificato Empty: Il telefono è vuoto NotChanged: Telefono non cambiato + VerifyingRemovalIsNotSupported: La rimozione della verifica non è supportata Address: NotFound: Indirizzo non trovato NotChanged: Indirizzo non cambiato diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index fcd7920999..4b0f2ea203 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -116,6 +116,7 @@ Errors: AlreadyVerified: 電話番号はすでに認証済みです Empty: 電話番号が空です NotChanged: 電話番号が変更されていません + VerifyingRemovalIsNotSupported: 電話番号の削除を検証することはできません Address: NotFound: 住所が見つかりません NotChanged: 住所は変更されていません diff --git a/internal/static/i18n/ko.yaml b/internal/static/i18n/ko.yaml index d83af62235..2c87aa1f97 100644 --- a/internal/static/i18n/ko.yaml +++ b/internal/static/i18n/ko.yaml @@ -116,6 +116,7 @@ Errors: AlreadyVerified: 전화번호가 이미 인증되었습니다 Empty: 전화번호가 비어 있습니다 NotChanged: 전화번호가 변경되지 않았습니다 + VerifyingRemovalIsNotSupported: 전화번호 제거를 확인하는 것은 지원되지 않습니다 Address: NotFound: 주소를 찾을 수 없습니다 NotChanged: 주소가 변경되지 않았습니다 diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index 7126925279..64ae87a618 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Телефонскиот број веќе е верифициран Empty: Телефонскиот број е празен NotChanged: Телефонскиот број не е променет + VerifyingRemovalIsNotSupported: Отстранувањето на верификацијата не е поддржано Address: NotFound: Адресата не е пронајдена NotChanged: Адресата не е променета diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index a398e4b770..dc9fd83721 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telefoon is al geverifieerd Empty: Telefoon is leeg NotChanged: Telefoon niet veranderd + VerifyingRemovalIsNotSupported: Verwijderen van verificatie is niet ondersteund Address: NotFound: Adres niet gevonden NotChanged: Adres niet veranderd diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 049a189930..4952345510 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Numer telefonu już zweryfikowany Empty: Numer telefonu jest pusty NotChanged: Numer telefonu nie zmieniony + VerifyingRemovalIsNotSupported: Usunięcie weryfikacji nie jest obsługiwane Address: NotFound: Adres nie znaleziony NotChanged: Adres nie zmieniony diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index 09a5fc02c5..e5fc785d0c 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: O telefone já foi verificado Empty: O telefone está vazio NotChanged: Telefone não alterado + VerifyingRemovalIsNotSupported: Remoção de verificação não suportada Address: NotFound: Endereço não encontrado NotChanged: Endereço não alterado diff --git a/internal/static/i18n/ro.yaml b/internal/static/i18n/ro.yaml index 9010e57032..ece4680de6 100644 --- a/internal/static/i18n/ro.yaml +++ b/internal/static/i18n/ro.yaml @@ -116,6 +116,7 @@ Errors: AlreadyVerified: Numărul de telefon este deja verificat Empty: Numărul de telefon este gol NotChanged: Numărul de telefon nu a fost schimbat + VerifyingRemovalIsNotSupported: Verificarea eliminării nu este acceptată Address: NotFound: Adresa nu a fost găsită NotChanged: Adresa nu a fost schimbată diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index 38b2847637..a2efd25322 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -116,6 +116,7 @@ Errors: AlreadyVerified: Телефон уже подтверждён Empty: Телефон пуст NotChanged: Телефон не менялся + VerifyingRemovalIsNotSupported: Удаление телефона не поддерживается Address: NotFound: Адрес не найден NotChanged: Адрес не изменён diff --git a/internal/static/i18n/sv.yaml b/internal/static/i18n/sv.yaml index ed4b863886..be40ceba3c 100644 --- a/internal/static/i18n/sv.yaml +++ b/internal/static/i18n/sv.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Mobilnr redan verifierad Empty: Mobilnr är tom NotChanged: Mobilnr ändrades inte + VerifyingRemovalIsNotSupported: Verifiering av borttagning stöds inte Address: NotFound: Adress hittades inte NotChanged: Adress ändrades inte diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 03aa168a50..930fcaddae 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: 手机号码已经验证 Empty: 电话号码是空的 NotChanged: 电话号码没有改变 + VerifyingRemovalIsNotSupported: 验证手机号码删除不受支持 Address: NotFound: 找不到地址 NotChanged: 地址没有改变 diff --git a/proto/buf.yaml b/proto/buf.yaml index 31bc7b4ccc..abe35b3055 100644 --- a/proto/buf.yaml +++ b/proto/buf.yaml @@ -40,4 +40,4 @@ lint: - zitadel/system.proto - zitadel/text.proto - zitadel/user.proto - - zitadel/v1.proto \ No newline at end of file + - zitadel/v1.proto diff --git a/proto/zitadel/filter/v2/filter.proto b/proto/zitadel/filter/v2/filter.proto new file mode 100644 index 0000000000..3817324d31 --- /dev/null +++ b/proto/zitadel/filter/v2/filter.proto @@ -0,0 +1,96 @@ +syntax = "proto3"; + +package zitadel.filter.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/filter/v2;filter"; + +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +enum TextFilterMethod { + TEXT_FILTER_METHOD_EQUALS = 0; + TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE = 1; + TEXT_FILTER_METHOD_STARTS_WITH = 2; + TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE = 3; + TEXT_FILTER_METHOD_CONTAINS = 4; + TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE = 5; + TEXT_FILTER_METHOD_ENDS_WITH = 6; + TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE = 7; +} + +enum ListFilterMethod { + LIST_FILTER_METHOD_IN = 0; +} + +enum TimestampFilterMethod { + TIMESTAMP_FILTER_METHOD_EQUALS = 0; + TIMESTAMP_FILTER_METHOD_AFTER = 1; + TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS = 2; + TIMESTAMP_FILTER_METHOD_BEFORE = 3; + TIMESTAMP_FILTER_METHOD_BEFORE_OR_EQUALS = 4; +} + +message PaginationRequest { + // Starting point for retrieval, in combination of offset used to query a set list of objects. + uint64 offset = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "0"; + } + ]; + // limit is the maximum amount of objects returned. The default is set to 100 + // with a maximum of 1000 in the runtime configuration. + // If the limit exceeds the maximum configured ZITADEL will throw an error. + // If no limit is present the default is taken. + uint32 limit = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "10"; + } + ]; + // Asc is the sorting order. If true the list is sorted ascending, if false + // the list is sorted descending. The default is descending. + bool asc = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "false"; + } + ]; +} + +message PaginationResponse { + // Absolute number of objects matching the query, regardless of applied limit. + uint64 total_result = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "100"; + } + ]; + // Applied limit from query, defines maximum amount of objects per request, to compare if all objects are returned. + uint64 applied_limit = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "100"; + } + ]; +} + +message IDFilter { + // Only return resources that belong to this id. + string id = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"69629023906488337\""; + } + ]; +} + +message TimestampFilter { + // Filter resources by timestamp. + google.protobuf.Timestamp timestamp = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // Defines the condition (e.g., equals, before, after) that the timestamp of the retrieved resources should match. + TimestampFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} diff --git a/proto/zitadel/filter/v2beta/filter.proto b/proto/zitadel/filter/v2beta/filter.proto index 6aae583cde..2265fa4125 100644 --- a/proto/zitadel/filter/v2beta/filter.proto +++ b/proto/zitadel/filter/v2beta/filter.proto @@ -6,6 +6,7 @@ option go_package = "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta;filter"; import "google/protobuf/timestamp.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; enum TextFilterMethod { TEXT_FILTER_METHOD_EQUALS = 0; @@ -56,4 +57,37 @@ message PaginationResponse { example: "\"100\""; } ]; -} \ No newline at end of file +} + +message IDFilter { + // Only return resources that belong to this id. + string id = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"69629023906488337\""; + } + ]; +} + +message TimestampFilter { + // Filter resources by timestamp. + google.protobuf.Timestamp timestamp = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // Defines the condition (e.g., equals, before, after) that the timestamp of the retrieved resources should match. + TimestampFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message InIDsFilter { + // Defines the ids to query for. + repeated string ids = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"69629023906488334\",\"69622366012355662\"]"; + } + ]; +} diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 34a8384d39..8cd0b22759 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -432,7 +432,11 @@ service ManagementService { }; } - // Deprecated: use ImportHumanUser + // Create User (Human) + // + // Deprecated: use [ImportHumanUser](apis/resources/mgmt/management-service-import-human-user.api.mdx) instead. + // + // Create a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login. rpc AddHumanUser(AddHumanUserRequest) returns (AddHumanUserResponse) { option (google.api.http) = { post: "/users/human" @@ -444,10 +448,8 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Deprecated: Create User (Human)"; - description: "Create a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login.\n\nDeprecated: use ImportHumanUser" - tags: "Users"; deprecated: true; + tags: "Users"; parameters: { headers: { name: "x-zitadel-orgid"; @@ -459,7 +461,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 AddHumanUser + // Create/Import User (Human) + // + // Deprecated: use [UpdateHumanUser](apis/resources/user_service_v2/user-service-update-human-user.api.mdx) instead. + // + // Create/import a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login. rpc ImportHumanUser(ImportHumanUserRequest) returns (ImportHumanUserResponse) { option (google.api.http) = { post: "/users/human/_import" @@ -471,11 +477,9 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create/Import User (Human)"; - description: "Create/import a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login.\n\nDeprecated: please use user service v2 [AddHumanUser](apis/resources/user_service_v2/user-service-add-human-user.api.mdx)" + deprecated: true; tags: "Users"; tags: "User Human" - deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -487,6 +491,11 @@ service ManagementService { }; } + // Create User (Machine) + // + // Deprecated: use [user service v2 CreateUser](apis/resources/user_service_v2/user-service-create-user.api.mdx) to create a user of type machine instead. + // + // Create a new user with the type machine for your API, service or device. These users are used for non-interactive authentication flows. rpc AddMachineUser(AddMachineUserRequest) returns (AddMachineUserResponse) { option (google.api.http) = { post: "/users/machine" @@ -498,8 +507,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create User (Machine)"; - description: "Create a new user with the type machine for your API, service or device. These users are used for non-interactive authentication flows." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -683,7 +691,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 UpdateHumanUser + // Change user name + // + // Deprecated: use [user service v2 UpdateUser](apis/resources/user_service_v2/user-service-update-user.api.mdx) instead. + // + // Change the username of the user. Be aware that the user has to log in with the newly added username afterward rpc UpdateUserName(UpdateUserNameRequest) returns (UpdateUserNameResponse) { option (google.api.http) = { put: "/users/{user_id}/username" @@ -695,8 +707,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Change user name"; - description: "Change the username of the user. Be aware that the user has to log in with the newly added username afterward.\n\nDeprecated: please use user service v2 UpdateHumanUser" tags: "Users"; deprecated: true; responses: { @@ -903,7 +913,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 UpdateHumanUser + // Update User Profile (Human) + // + // Deprecated: use [user service v2 UpdateHumanUser](apis/resources/user_service_v2/user-service-update-human-user.api.mdx) instead. + // + // Update the profile information from a user. The profile includes basic information like first_name and last_name. rpc UpdateHumanProfile(UpdateHumanProfileRequest) returns (UpdateHumanProfileResponse) { option (google.api.http) = { put: "/users/{user_id}/profile" @@ -915,11 +929,9 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Update User Profile (Human)"; - description: "Update the profile information from a user. The profile includes basic information like first_name and last_name.\n\nDeprecated: please use user service v2 UpdateHumanUser" + deprecated: true; tags: "Users"; tags: "User Human"; - deprecated: true; responses: { key: "200" value: { @@ -970,7 +982,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 SetEmail + // Update User Email (Human) + // + // Deprecated: use [user service v2 SetEmail](apis/resources/user_service_v2/user-service-set-email.api.mdx) instead. + // + // Change the email address of a user. If the state is set to not verified, the user will get a verification email. rpc UpdateHumanEmail(UpdateHumanEmailRequest) returns (UpdateHumanEmailResponse) { option (google.api.http) = { put: "/users/{user_id}/email" @@ -982,8 +998,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Update User Email (Human)"; - description: "Change the email address of a user. If the state is set to not verified, the user will get a verification email.\n\nDeprecated: please use user service v2 SetEmail" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1039,7 +1053,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 ResendEmailCode + // Resend User Email Verification + // + // Deprecated: use [user service v2 ResendEmailCode](apis/resources/user_service_v2/user-service-resend-email-code.api.mdx) instead. + // + // Resend the email verification notification to the given email address of the user. rpc ResendHumanEmailVerification(ResendHumanEmailVerificationRequest) returns (ResendHumanEmailVerificationResponse) { option (google.api.http) = { post: "/users/{user_id}/email/_resend_verification" @@ -1051,8 +1069,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Resend User Email Verification"; - description: "Resend the email verification notification to the given email address of the user.\n\nDeprecated: please use user service v2 ResendEmailCode" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1106,7 +1122,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 SetPhone + // Update User Phone (Human) + // + // Deprecated: use [user service v2 SetPhone](apis/resources/user_service_v2/user-service-update-user.api.mdx) instead. + // + // Change the phone number of a user. If the state is set to not verified, the user will get an SMS to verify (if a notification provider is configured). The phone number is only for informational purposes and to send messages, not for Authentication (2FA). rpc UpdateHumanPhone(UpdateHumanPhoneRequest) returns (UpdateHumanPhoneResponse) { option (google.api.http) = { put: "/users/{user_id}/phone" @@ -1118,8 +1138,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Update User Phone (Human)"; - description: "Change the phone number of a user. If the state is set to not verified, the user will get an SMS to verify (if a notification provider is configured). The phone number is only for informational purposes and to send messages, not for Authentication (2FA).\n\nDeprecated: please use user service v2 SetPhone" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1140,7 +1158,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 SetPhone + // Remove User Phone (Human) + // + // Deprecated: use user service v2 [user service v2 SetPhone](apis/resources/user_service_v2/user-service-set-phone.api.mdx) instead. + // + // Remove the configured phone number of a user. rpc RemoveHumanPhone(RemoveHumanPhoneRequest) returns (RemoveHumanPhoneResponse) { option (google.api.http) = { delete: "/users/{user_id}/phone" @@ -1151,8 +1173,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Remove User Phone (Human)"; - description: "Remove the configured phone number of a user.\n\nDeprecated: please use user service v2 SetPhone" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1173,7 +1193,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 ResendPhoneCode + // Resend User Phone Verification + // + // Deprecated: use user service v2 [user service v2 ResendPhoneCode](apis/resources/user_service_v2/user-service-resend-phone-code.api.mdx) instead. + // + // Resend the notification for the verification of the phone number, to the number stored on the user. rpc ResendHumanPhoneVerification(ResendHumanPhoneVerificationRequest) returns (ResendHumanPhoneVerificationResponse) { option (google.api.http) = { post: "/users/{user_id}/phone/_resend_verification" @@ -1185,8 +1209,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Resend User Phone Verification"; - description: "Resend the notification for the verification of the phone number, to the number stored on the user.\n\nDeprecated: please use user service v2 ResendPhoneCode" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1238,7 +1260,9 @@ service ManagementService { }; } - // Deprecated: please use user service v2 SetPassword + // Set Human Initial Password + // + // Deprecated: use [user service v2 SetPassword](apis/resources/user_service_v2/user-service-set-password.api.mdx) instead. rpc SetHumanInitialPassword(SetHumanInitialPasswordRequest) returns (SetHumanInitialPasswordResponse) { option (google.api.http) = { post: "/users/{user_id}/password/_initialize" @@ -1252,7 +1276,6 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Users"; tags: "User Human"; - summary: "Set Human Initial Password\n\nDeprecated: please use user service v2 SetPassword"; deprecated: true; parameters: { headers: { @@ -1265,7 +1288,9 @@ service ManagementService { }; } - // Deprecated: please use user service v2 SetPassword + // Set User Password + // + // Deprecated: use [user service v2 SetPassword](apis/resources/user_service_v2/user-service-set-password.api.mdx) instead. rpc SetHumanPassword(SetHumanPasswordRequest) returns (SetHumanPasswordResponse) { option (google.api.http) = { post: "/users/{user_id}/password" @@ -1277,8 +1302,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Set User Password"; - description: "Set a new password for a user. Per default, the user has to change the password on the next login. You can set no_change_required to true, to avoid the change on the next login.\n\nDeprecated: please use user service v2 SetPassword" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1299,7 +1322,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 PasswordReset + // Send Reset Password Notification + // + // Deprecated: use [user service v2 PasswordReset](apis/resources/user_service_v2/user-service-password-reset.api.mdx) instead. + // + // The user will receive an email with a link to change the password. rpc SendHumanResetPasswordNotification(SendHumanResetPasswordNotificationRequest) returns (SendHumanResetPasswordNotificationResponse) { option (google.api.http) = { post: "/users/{user_id}/password/_reset" @@ -1311,8 +1338,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Send Reset Password Notification"; - description: "The user will receive an email with a link to change the password.\n\nDeprecated: please use user service v2 PasswordReset" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1629,6 +1654,11 @@ service ManagementService { }; } + // Update Machine User + // + // Deprecated: use [user service v2 UpdateUser](apis/resources/user_service_v2/user-service-update-user.api.mdx) to update a user of type machine instead. + // + // Change a service account/machine user. It is used for accounts with non-interactive authentication possibilities. rpc UpdateMachine(UpdateMachineRequest) returns (UpdateMachineResponse) { option (google.api.http) = { put: "/users/{user_id}/machine" @@ -1640,8 +1670,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Update Machine User"; - description: "Change a service account/machine user. It is used for accounts with non-interactive authentication possibilities." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1661,6 +1690,11 @@ service ManagementService { }; } + // Create Secret for Machine User + // + // Deprecated: use [user service v2 AddSecret](apis/resources/user_service_v2/user-service-add-secret.api.mdx) instead. + // + // Create a new secret for a machine user/service account. It is used to authenticate the user (client credential grant). rpc GenerateMachineSecret(GenerateMachineSecretRequest) returns (GenerateMachineSecretResponse) { option (google.api.http) = { put: "/users/{user_id}/secret" @@ -1672,8 +1706,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create Secret for Machine User"; - description: "Create a new secret for a machine user/service account. It is used to authenticate the user (client credential grant)." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1693,6 +1726,11 @@ service ManagementService { }; } + // Delete Secret of Machine User + // + // Deprecated: use [user service v2 RemoveSecret](apis/resources/user_service_v2/user-service-remove-secret.api.mdx) instead. + // + // Delete a secret of a machine user/service account. The user will not be able to authenticate with the secret afterward. rpc RemoveMachineSecret(RemoveMachineSecretRequest) returns (RemoveMachineSecretResponse) { option (google.api.http) = { delete: "/users/{user_id}/secret" @@ -1703,8 +1741,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Delete Secret of Machine User"; - description: "Delete a secret of a machine user/service account. The user will not be able to authenticate with the secret afterward." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1724,6 +1761,11 @@ service ManagementService { }; } + // Get Machine user Key By ID + // + // Deprecated: use [user service v2 ListKeys](apis/resources/user_service_v2/user-service-list-keys.api.mdx) instead. + // + // Get a specific Key of a machine user by its id. Machine keys are used to authenticate with jwt profile authentication. rpc GetMachineKeyByIDs(GetMachineKeyByIDsRequest) returns (GetMachineKeyByIDsResponse) { option (google.api.http) = { get: "/users/{user_id}/keys/{key_id}" @@ -1734,8 +1776,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get Machine user Key By ID"; - description: "Get a specific Key of a machine user by its id. Machine keys are used to authenticate with jwt profile authentication." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1755,6 +1796,11 @@ service ManagementService { }; } + // Get Machine user Key By ID + // + // Deprecated: use [user service v2 ListKeys](apis/resources/user_service_v2/user-service-list-keys.api.mdx) instead. + // + // Get the list of keys of a machine user. Machine keys are used to authenticate with jwt profile authentication. rpc ListMachineKeys(ListMachineKeysRequest) returns (ListMachineKeysResponse) { option (google.api.http) = { post: "/users/{user_id}/keys/_search" @@ -1766,8 +1812,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get Machine user Key By ID"; - description: "Get the list of keys of a machine user. Machine keys are used to authenticate with jwt profile authentication." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1787,6 +1832,14 @@ service ManagementService { }; } + // Create Key for machine user + // + // Deprecated: use [user service v2 AddKey](apis/resources/user_service_v2/user-service-add-key.api.mdx) instead. + // + // If a public key is not supplied, a new key is generated and will be returned in the response. + // Make sure to store the returned key. + // If an RSA public key is supplied, the private key is omitted from the response. + // Machine keys are used to authenticate with jwt profile. rpc AddMachineKey(AddMachineKeyRequest) returns (AddMachineKeyResponse) { option (google.api.http) = { post: "/users/{user_id}/keys" @@ -1798,8 +1851,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create Key for machine user"; - description: "If a public key is not supplied, a new key is generated and will be returned in the response. Make sure to store the returned key. If an RSA public key is supplied, the private key is omitted from the response. Machine keys are used to authenticate with jwt profile." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1819,6 +1871,12 @@ service ManagementService { }; } + // Delete Key for machine user + // + // Deprecated: use [user service v2 RemoveKey](apis/resources/user_service_v2/user-service-remove-key.api.mdx) instead. + // + // Delete a specific key from a user. + // The user will not be able to authenticate with that key afterward. rpc RemoveMachineKey(RemoveMachineKeyRequest) returns (RemoveMachineKeyResponse) { option (google.api.http) = { delete: "/users/{user_id}/keys/{key_id}" @@ -1829,8 +1887,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Delete Key for machine user"; - description: "Delete a specific key from a user. The user will not be able to authenticate with that key afterward." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1850,6 +1907,11 @@ service ManagementService { }; } + // Get Personal-Access-Token (PAT) by ID + // + // Deprecated: use [user service v2 ListPersonalAccessTokens](apis/resources/user_service_v2/user-service-list-personal-access-tokens.api.mdx) instead. + // + // Returns the PAT for a user, currently only available for machine users/service accounts. PATs are ready-to-use tokens and can be sent directly in the authentication header. rpc GetPersonalAccessTokenByIDs(GetPersonalAccessTokenByIDsRequest) returns (GetPersonalAccessTokenByIDsResponse) { option (google.api.http) = { get: "/users/{user_id}/pats/{token_id}" @@ -1860,8 +1922,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get a Personal-Access-Token (PAT) by ID"; - description: "Returns the PAT for a user, currently only available for machine users/service accounts. PATs are ready-to-use tokens and can be sent directly in the authentication header." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1881,6 +1942,11 @@ service ManagementService { }; } + // List Personal-Access-Tokens (PATs) + // + // Deprecated: use [user service v2 ListPersonalAccessTokens](apis/resources/user_service_v2/user-service-list-personal-access-tokens.api.mdx) instead. + // + // Returns a list of PATs for a user, currently only available for machine users/service accounts. PATs are ready-to-use tokens and can be sent directly in the authentication header. rpc ListPersonalAccessTokens(ListPersonalAccessTokensRequest) returns (ListPersonalAccessTokensResponse) { option (google.api.http) = { post: "/users/{user_id}/pats/_search" @@ -1892,8 +1958,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get a Personal-Access-Token (PAT) by ID"; - description: "Returns a list of PATs for a user, currently only available for machine users/service accounts. PATs are ready-to-use tokens and can be sent directly in the authentication header." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1913,6 +1978,13 @@ service ManagementService { }; } + // Create a Personal-Access-Token (PAT) + // + // Deprecated: use [user service v2 AddPersonalAccessToken](apis/resources/user_service_v2/user-service-add-personal-access-token.api.mdx) instead. + // + // Generates a new PAT for the user. Currently only available for machine users. + // The token will be returned in the response, make sure to store it. + // PATs are ready-to-use tokens and can be sent directly in the authentication header. rpc AddPersonalAccessToken(AddPersonalAccessTokenRequest) returns (AddPersonalAccessTokenResponse) { option (google.api.http) = { post: "/users/{user_id}/pats" @@ -1924,8 +1996,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create a Personal-Access-Token (PAT)"; - description: "Generates a new PAT for the user. Currently only available for machine users. The token will be returned in the response, make sure to store it. PATs are ready-to-use tokens and can be sent directly in the authentication header." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1945,6 +2016,11 @@ service ManagementService { }; } + // Remove a Personal-Access-Token (PAT) by ID + // + // Deprecated: use [user service v2 RemovePersonalAccessToken](apis/resources/user_service_v2/user-service-remove-personal-access-token.api.mdx) instead. + // + // Delete a PAT from a user. Afterward, the user will not be able to authenticate with that token anymore. rpc RemovePersonalAccessToken(RemovePersonalAccessTokenRequest) returns (RemovePersonalAccessTokenResponse) { option (google.api.http) = { delete: "/users/{user_id}/pats/{token_id}" @@ -1955,8 +2031,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get a Personal-Access-Token (PAT) by ID"; - description: "Delete a PAT from a user. Afterward, the user will not be able to authenticate with that token anymore." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -2003,7 +2078,7 @@ service ManagementService { }; } - // Deprecated: please use user service v2 RemoveLinkedIDP + // Deprecated: please use [user service v2 RemoveIDPLink](apis/resources/user_service_v2/user-service-remove-idp-link.api.mdx) rpc RemoveHumanLinkedIDP(RemoveHumanLinkedIDPRequest) returns (RemoveHumanLinkedIDPResponse) { option (google.api.http) = { delete: "/users/{user_id}/idps/{idp_id}/{linked_user_id}" diff --git a/proto/zitadel/project/v2beta/query.proto b/proto/zitadel/project/v2beta/query.proto index f328b65189..9bfde662a3 100644 --- a/proto/zitadel/project/v2beta/query.proto +++ b/proto/zitadel/project/v2beta/query.proto @@ -185,10 +185,10 @@ message ProjectSearchFilter { option (validate.required) = true; ProjectNameFilter project_name_filter = 1; - InProjectIDsFilter in_project_ids_filter = 2; - ProjectResourceOwnerFilter project_resource_owner_filter = 3; - ProjectGrantResourceOwnerFilter project_grant_resource_owner_filter = 4; - ProjectOrganizationIDFilter project_organization_id_filter = 5; + zitadel.filter.v2beta.InIDsFilter in_project_ids_filter = 2; + zitadel.filter.v2beta.IDFilter project_resource_owner_filter = 3; + zitadel.filter.v2beta.IDFilter project_grant_resource_owner_filter = 4; + zitadel.filter.v2beta.IDFilter project_organization_id_filter = 5; } } @@ -210,68 +210,18 @@ message ProjectNameFilter { ]; } -message InProjectIDsFilter { - // Defines the ids to query for. - repeated string project_ids = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "the ids of the projects to include" - example: "[\"69629023906488334\",\"69622366012355662\"]"; - } - ]; -} - -message ProjectResourceOwnerFilter { - // Defines the ID of organization the project belongs to query for. - string project_resource_owner = 1 [ - (validate.rules).string = {max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629023906488334\"" - } - ]; -} - -message ProjectGrantResourceOwnerFilter { - // Defines the ID of organization the project grant belongs to query for. - string project_grant_resource_owner = 1 [ - (validate.rules).string = {max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629023906488334\"" - } - ]; -} - -message ProjectOrganizationIDFilter { - // Defines the ID of organization the project and granted project belong to query for. - string project_organization_id = 1 [ - (validate.rules).string = {max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629023906488334\"" - } - ]; -} - message ProjectGrantSearchFilter { oneof filter { option (validate.required) = true; ProjectNameFilter project_name_filter = 1; ProjectRoleKeyFilter role_key_filter = 2; - InProjectIDsFilter in_project_ids_filter = 3; - ProjectResourceOwnerFilter project_resource_owner_filter = 4; - ProjectGrantResourceOwnerFilter project_grant_resource_owner_filter = 5; + zitadel.filter.v2beta.InIDsFilter in_project_ids_filter = 3; + zitadel.filter.v2beta.IDFilter project_resource_owner_filter = 4; + zitadel.filter.v2beta.IDFilter project_grant_resource_owner_filter = 5; } } -message GrantedOrganizationIDFilter { - // Defines the ID of organization the project is granted to query for. - string granted_organization_id = 1 [ - (validate.rules).string = {max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629023906488334\"" - } - ]; -} - message ProjectRole { // ID of the project. string project_id = 1 [ @@ -344,4 +294,4 @@ message ProjectRoleDisplayNameFilter { zitadel.filter.v2beta.TextFilterMethod method = 2 [ (validate.rules).enum.defined_only = true ]; -} \ No newline at end of file +} diff --git a/proto/zitadel/user/v2/email.proto b/proto/zitadel/user/v2/email.proto index e962707fcf..eb807206a7 100644 --- a/proto/zitadel/user/v2/email.proto +++ b/proto/zitadel/user/v2/email.proto @@ -19,7 +19,7 @@ message SetHumanEmail { example: "\"mini@mouse.com\""; } ]; - // if no verification is specified, an email is sent with the default url + // If no verification is specified, an email is sent with the default url oneof verification { SendEmailVerificationCode send_code = 2; ReturnEmailVerificationCode return_code = 3; diff --git a/proto/zitadel/user/v2/key.proto b/proto/zitadel/user/v2/key.proto new file mode 100644 index 0000000000..ffa83c714e --- /dev/null +++ b/proto/zitadel/user/v2/key.proto @@ -0,0 +1,69 @@ +syntax = "proto3"; + +package zitadel.user.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; +import "zitadel/filter/v2/filter.proto"; + +message Key { + // The timestamp of the key creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change of the key. + google.protobuf.Timestamp change_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The unique identifier of the key. + string id = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The unique identifier of the user the key belongs to. + string user_id = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The unique identifier of the organization the key belongs to. + string organization_id = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The keys expiration date. + google.protobuf.Timestamp expiration_date = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message KeysSearchFilter { + oneof filter { + option (validate.required) = true; + zitadel.filter.v2.IDFilter key_id_filter = 1; + zitadel.filter.v2.IDFilter user_id_filter = 2; + zitadel.filter.v2.IDFilter organization_id_filter = 3; + zitadel.filter.v2.TimestampFilter created_date_filter = 4; + zitadel.filter.v2.TimestampFilter expiration_date_filter = 5; + } +} + +enum KeyFieldName { + KEY_FIELD_NAME_UNSPECIFIED = 0; + KEY_FIELD_NAME_CREATED_DATE = 1; + KEY_FIELD_NAME_ID = 2; + KEY_FIELD_NAME_USER_ID = 3; + KEY_FIELD_NAME_ORGANIZATION_ID = 4; + KEY_FIELD_NAME_KEY_EXPIRATION_DATE = 5; +} diff --git a/proto/zitadel/user/v2/pat.proto b/proto/zitadel/user/v2/pat.proto new file mode 100644 index 0000000000..1d24c4c496 --- /dev/null +++ b/proto/zitadel/user/v2/pat.proto @@ -0,0 +1,70 @@ +syntax = "proto3"; + +package zitadel.user.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; +import "zitadel/filter/v2/filter.proto"; + +message PersonalAccessToken { + // The timestamp of the personal access token creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change of the personal access token. + google.protobuf.Timestamp change_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The unique identifier of the personal access token. + string id = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The unique identifier of the user the personal access token belongs to. + string user_id = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The unique identifier of the organization the personal access token belongs to. + string organization_id = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The personal access tokens expiration date. + google.protobuf.Timestamp expiration_date = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message PersonalAccessTokensSearchFilter { + oneof filter { + option (validate.required) = true; + zitadel.filter.v2.IDFilter token_id_filter = 1; + zitadel.filter.v2.IDFilter user_id_filter = 2; + zitadel.filter.v2.IDFilter organization_id_filter = 3; + zitadel.filter.v2.TimestampFilter created_date_filter = 4; + zitadel.filter.v2.TimestampFilter expiration_date_filter = 5; + } +} + +enum PersonalAccessTokenFieldName { + PERSONAL_ACCESS_TOKEN_FIELD_NAME_UNSPECIFIED = 0; + PERSONAL_ACCESS_TOKEN_FIELD_NAME_CREATED_DATE = 1; + PERSONAL_ACCESS_TOKEN_FIELD_NAME_ID = 2; + PERSONAL_ACCESS_TOKEN_FIELD_NAME_USER_ID = 3; + PERSONAL_ACCESS_TOKEN_FIELD_NAME_ORGANIZATION_ID = 4; + PERSONAL_ACCESS_TOKEN_FIELD_NAME_EXPIRATION_DATE = 5; +} + diff --git a/proto/zitadel/user/v2/user_service.proto b/proto/zitadel/user/v2/user_service.proto index 44d25c07b3..3fc81836d6 100644 --- a/proto/zitadel/user/v2/user_service.proto +++ b/proto/zitadel/user/v2/user_service.proto @@ -2,6 +2,14 @@ syntax = "proto3"; package zitadel.user.v2; +import "google/protobuf/timestamp.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + import "zitadel/object/v2/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; import "zitadel/user/v2/auth.proto"; @@ -10,13 +18,10 @@ import "zitadel/user/v2/phone.proto"; import "zitadel/user/v2/idp.proto"; import "zitadel/user/v2/password.proto"; import "zitadel/user/v2/user.proto"; +import "zitadel/user/v2/key.proto"; +import "zitadel/user/v2/pat.proto"; import "zitadel/user/v2/query.proto"; -import "google/api/annotations.proto"; -import "google/api/field_behavior.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/struct.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; +import "zitadel/filter/v2/filter.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; @@ -85,9 +90,9 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } } responses: { - key: "403"; + key: "400"; value: { - description: "Returned when the user does not have permission to access the resource."; + description: "The request is malformed."; schema: { json_schema: { ref: "#/definitions/rpcStatus"; @@ -96,9 +101,20 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } } responses: { - key: "404"; + key: "401"; value: { - description: "Returned when the resource does not exist."; + description: "Returned when the user is not authenticated."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; schema: { json_schema: { ref: "#/definitions/rpcStatus"; @@ -110,8 +126,51 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { service UserService { + // Create a User + // + // Create a new human or machine user in the specified organization. + // + // Required permission: + // - user.write + rpc CreateUser (CreateUserRequest) returns (CreateUserResponse) { + option (google.api.http) = { + // The /new path segment does not follow Zitadels API design. + // The only reason why it is used here is to avoid a conflict with the ListUsers endpoint, which already handles POST /v2/users. + post: "/v2/users/new" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "OK"; + } + }; + responses: { + key: "409" + value: { + description: "The user already exists."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + // Create a new human user // + // Deprecated: Use [CreateUser](apis/resources/user_service_v2/user-service-create-user.api.mdx) to create a new user of type human instead. + // // Create/import a new user with the type human. The newly created user will get a verification email if either the email address is not marked as verified and you did not request the verification to be returned. rpc AddHumanUser (AddHumanUserRequest) returns (AddHumanUserResponse) { option (google.api.http) = { @@ -125,11 +184,12 @@ service UserService { org_field: "organization" } http_response: { - success_code: 201 + success_code: 200 } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200" value: { @@ -163,6 +223,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -204,6 +270,8 @@ service UserService { // Change the user email // + // Deprecated: [Update the users email field](apis/resources/user_service_v2/user-service-update-user.api.mdx). + // // Change the email address of a user. If the state is set to not verified, a verification code will be generated, which can be either returned or sent to the user by email.. rpc SetEmail (SetEmailRequest) returns (SetEmailResponse) { option (google.api.http) = { @@ -218,18 +286,23 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200" value: { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } // Resend code to verify user email - // - // Resend code to verify user email. rpc ResendEmailCode (ResendEmailCodeRequest) returns (ResendEmailCodeResponse) { option (google.api.http) = { post: "/v2/users/{user_id}/email/resend" @@ -249,12 +322,16 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } // Send code to verify user email - // - // Send code to verify user email. rpc SendEmailCode (SendEmailCodeRequest) returns (SendEmailCodeResponse) { option (google.api.http) = { post: "/v2/users/{user_id}/email/send" @@ -274,6 +351,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -299,11 +382,19 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } // Set the user phone // + // Deprecated: [Update the users phone field](apis/resources/user_service_v2/user-service-update-user.api.mdx). + // // Set the phone number of a user. If the state is set to not verified, a verification code will be generated, which can be either returned or sent to the user by sms.. rpc SetPhone(SetPhoneRequest) returns (SetPhoneResponse) { option (google.api.http) = { @@ -318,18 +409,27 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200" value: { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } - // Remove the user phone + // Delete the user phone // - // Remove the user phone + // Deprecated: [Update the users phone field](apis/resources/user_service_v2/user-service-update-user.api.mdx) to remove the phone number. + // + // Delete the phone number of a user. rpc RemovePhone(RemovePhoneRequest) returns (RemovePhoneResponse) { option (google.api.http) = { delete: "/v2/users/{user_id}/phone" @@ -343,20 +443,23 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Delete the user phone"; - description: "Delete the phone number of a user." + deprecated: true; responses: { key: "200" value: { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } // Resend code to verify user phone - // - // Resend code to verify user phone. rpc ResendPhoneCode (ResendPhoneCodeRequest) returns (ResendPhoneCodeResponse) { option (google.api.http) = { post: "/v2/users/{user_id}/phone/resend" @@ -376,6 +479,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -401,15 +510,27 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } - // Update User + + // Update a User // - // Update all information from a user.. - rpc UpdateHumanUser(UpdateHumanUserRequest) returns (UpdateHumanUserResponse) { + // Partially update an existing user. + // If you change the users email or phone, you can specify how the ownership should be verified. + // If you change the users password, you can specify if the password should be changed again on the users next login. + // + // Required permission: + // - user.write + rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse) { option (google.api.http) = { - put: "/v2/users/human/{user_id}" + patch: "/v2/users/{user_id}" body: "*" }; @@ -426,6 +547,62 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + } + responses: { + key: "409" + value: { + description: "The user already exists."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + + // Update Human User + // + // Deprecated: Use [UpdateUser](apis/resources/user_service_v2/user-service-update-user.api.mdx) to update a user of type human instead. + // + // Update all information from a user.. + rpc UpdateHumanUser(UpdateHumanUserRequest) returns (UpdateHumanUserResponse) { + option (google.api.http) = { + put: "/v2/users/human/{user_id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + responses: { + key: "200" + value: { + description: "OK"; + } + }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -451,6 +628,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -476,6 +659,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -501,6 +690,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -526,6 +721,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -550,6 +751,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -574,6 +781,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -598,6 +811,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -622,6 +841,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -670,6 +895,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -694,6 +925,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -718,6 +955,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -743,6 +986,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -767,6 +1016,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -791,6 +1046,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -814,6 +1075,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -838,6 +1105,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -861,6 +1134,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -885,6 +1164,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -908,6 +1193,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -958,6 +1249,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "Intent ID does not exist."; + } + } }; } @@ -983,6 +1280,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1033,6 +1336,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1058,11 +1367,19 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } // Change password // + // Deprecated: [Update the users password](apis/resources/user_service_v2/user-service-update-user.api.mdx) instead. + // // Change the password of a user with either a verification code or the current password.. rpc SetPassword (SetPasswordRequest) returns (SetPasswordResponse) { option (google.api.http) = { @@ -1077,12 +1394,19 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200" value: { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1155,6 +1479,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1183,6 +1513,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1209,6 +1545,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1234,9 +1576,289 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } + // Add a Users Secret + // + // Generates a client secret for the user. + // The client id is the users username. + // If the user already has a secret, it is overwritten. + // Only users of type machine can have a secret. + // + // Required permission: + // - user.write + rpc AddSecret(AddSecretRequest) returns (AddSecretResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/secret" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "The secret was successfully generated."; + } + }; + responses: { + key: "404" + value: { + description: "The user ID does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + + // Remove a Users Secret + // + // Remove the current client ID and client secret from a machine user. + // + // Required permission: + // - user.write + rpc RemoveSecret(RemoveSecretRequest) returns (RemoveSecretResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}/secret" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "The secret was either successfully removed or it didn't exist in the first place."; + } + }; + }; + } + + // Add a Key + // + // Add a keys that can be used to securely authenticate at the Zitadel APIs using JWT profile authentication using short-lived tokens. + // Make sure you store the returned key safely, as you won't be able to read it from the Zitadel API anymore. + // Only users of type machine can have keys. + // + // Required permission: + // - user.write + rpc AddKey(AddKeyRequest) returns (AddKeyResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/keys" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "The key was successfully created."; + } + }; + responses: { + key: "404" + value: { + description: "The user ID does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + + // Remove a Key + // + // Remove a machine users key by the given key ID and an optionally given user ID. + // + // Required permission: + // - user.write + rpc RemoveKey(RemoveKeyRequest) returns (RemoveKeyResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}/keys/{key_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "The key was either successfully removed or it not found in the first place."; + } + }; + }; + } + + // Search Keys + // + // List all matching keys. By default all keys of the instance on which the caller has permission to read the owning users are returned. + // Make sure to include a limit and sorting for pagination. + // + // Required permission: + // - user.read + rpc ListKeys(ListKeysRequest) returns (ListKeysResponse) { + option (google.api.http) = { + post: "/v2/users/keys/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all machine user keys matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } + + // Add a Personal Access Token + // + // Personal access tokens (PAT) are the easiest way to authenticate to the Zitadel APIs. + // Make sure you store the returned PAT safely, as you won't be able to read it from the Zitadel API anymore. + // Only users of type machine can have personal access tokens. + // + // Required permission: + // - user.write + rpc AddPersonalAccessToken(AddPersonalAccessTokenRequest) returns (AddPersonalAccessTokenResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/pats" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "The personal access token was successfully created."; + } + }; + responses: { + key: "404" + value: { + description: "The user ID does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + + // Remove a Personal Access Token + // + // Removes a machine users personal access token by the given token ID and an optionally given user ID. + // + // Required permission: + // - user.write + rpc RemovePersonalAccessToken(RemovePersonalAccessTokenRequest) returns (RemovePersonalAccessTokenResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}/pats/{token_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "The personal access token was either successfully removed or it was not found in the first place."; + } + }; + }; + } + + // Search Personal Access Tokens + // + // List all personal access tokens. By default all personal access tokens of the instance on which the caller has permission to read the owning users are returned. + // Make sure to include a limit and sorting for pagination. + // + // Required permission: + // - user.read + rpc ListPersonalAccessTokens(ListPersonalAccessTokensRequest) returns (ListPersonalAccessTokensResponse) { + option (google.api.http) = { + post: "/v2/users/pats/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all personal access tokens matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } } message AddHumanUserRequest{ @@ -1296,6 +1918,149 @@ message AddHumanUserResponse { optional string phone_code = 4; } + +message CreateUserRequest{ + message Human { + // Set the users profile information. + SetHumanProfile profile = 1 [ + (validate.rules).message.required = true, + (google.api.field_behavior) = REQUIRED + ]; + // Set the users email address and optionally send a verification email. + SetHumanEmail email = 2 [ + (validate.rules).message.required = true, + (google.api.field_behavior) = REQUIRED + ]; + // Set the users phone number and optionally send a verification SMS. + optional SetHumanPhone phone = 3; + // Set the users initial password and optionally require the user to set a new password. + oneof password_type { + Password password = 4; + HashedPassword hashed_password = 5; + } + // Create the user with a list of links to identity providers. + // This can be useful in migration-scenarios. + // For example, if a user already has an account in an external identity provider or another Zitadel instance, an IDP link allows the user to authenticate as usual. + // Sessions, second factors, hardware keys registered externally are still available for authentication. + // Use the following endpoints to manage identity provider links: + // - [AddIDPLink](apis/resources/user_service_v2/user-service-add-idp-link.api.mdx) + // - [RemoveIDPLink](apis/resources/user_service_v2/user-service-remove-idp-link.api.mdx) + repeated IDPLink idp_links = 7; + // An Implementation of RFC 6238 is used, with HMAC-SHA-1 and time-step of 30 seconds. + // Currently no other options are supported, and if anything different is used the validation will fail. + optional string totp_secret = 8 [ + (validate.rules).string = {min_len: 1 max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"TJOPWSDYILLHXFV4MLKNNJOWFG7VSDCK\""; + } + ]; + } + message Machine { + // The machine users name is a human readable field that helps identifying the user. + string name = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"Acceptance Test User\""; + } + ]; + // The description is a field that helps to remember the purpose of the user. + optional string description = 2 [ + (validate.rules).string = {min_len: 1, max_len: 500}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 500, + example: "\"The user calls the session API in the continuous integration pipeline for acceptance tests.\""; + } + ]; + } + // The unique identifier of the organization the user belongs to. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + // The ID is a unique identifier for the user in the instance. + // If not specified, it will be generated. + // You can set your own user id that is unique within the instance. + // This is useful in migration scenarios, for example if the user already has an ID in another Zitadel system. + // If not specified, it will be generated. + // It can't be changed after creation. + optional string user_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"163840776835432345\""; + } + ]; + + // The username is a unique identifier for the user in the organization. + // If not specified, Zitadel sets the username to the email for users of type human and to the user_id for users of type machine. + // It is used to identify the user in the organization and can be used for login. + optional string username = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"minnie-mouse\""; + } + ]; + + // The type of the user. + oneof user_type { + option (validate.required) = true; + // Users of type human are users that are meant to be used by a person. + // They can log in interactively using a login UI. + // By default, new users will receive a verification email and, if a phone is configured, a verification SMS. + // To make sure these messages are sent, configure and activate valid SMTP and Twilio configurations. + // Read more about your options for controlling this behaviour in the email and phone field documentations. + Human human = 4; + // Users of type machine are users that are meant to be used by a machine. + // In order to authenticate, [add a secret](apis/resources/user_service_v2/user-service-add-secret.api.mdx), [a key](apis/resources/user_service_v2/user-service-add-key.api.mdx) or [a personal access token](apis/resources/user_service_v2/user-service-add-personal-access-token.api.mdx) to the user. + // Tokens generated for new users of type machine will be of an opaque Bearer type. + // You can change the users token type to JWT by using the [management v1 service method UpdateMachine](apis/resources/mgmt/management-service-update-machine.api.mdx). + Machine machine = 5; + } + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"organizationId\":\"69629026806489455\",\"userId\":\"163840776835432345\",\"username\":\"minnie-mouse\",\"human\":{\"profile\":{\"givenName\":\"Minnie\",\"familyName\":\"Mouse\",\"nickName\":\"Mini\",\"displayName\":\"Minnie Mouse\",\"preferredLanguage\":\"en\",\"gender\":\"GENDER_FEMALE\"},\"email\":{\"email\":\"mini@mouse.com\",\"sendCode\":{\"urlTemplate\":\"https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}\"}},\"phone\":{\"phone\":\"+41791234567\",\"isVerified\":true},\"password\":{\"password\":\"Secr3tP4ssw0rd!\",\"changeRequired\":true},\"idpLinks\":[{\"idpId\":\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\",\"userId\":\"6516849804890468048461403518\",\"userName\":\"user@external.com\"}],\"totpSecret\":\"TJOPWSDYILLHXFV4MLKNNJOWFG7VSDCK\"}}"; + }; +} + +message CreateUserResponse { + // The unique identifier of the newly created user. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the user creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + // The email verification code if it was requested by setting the email verification to return_code. + optional string email_code = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"XTA6BC\""; + } + ]; + // The phone verification code if it was requested by setting the phone verification to return_code. + optional string phone_code = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"XTA6BC\""; + } + ]; +} + message GetUserByIDRequest { reserved 2; reserved "organization"; @@ -1550,6 +2315,142 @@ message DeleteUserResponse { zitadel.object.v2.Details details = 1; } + +message UpdateUserRequest{ + message Human { + message Profile { + // The given name is the first name of the user. + // For example, it can be used to personalize notifications and login UIs. + optional string given_name = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Minnie\""; + } + ]; + // The family name is the last name of the user. + // For example, it can be used to personalize user interfaces and notifications. + optional string family_name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Mouse\""; + } + ]; + // The nick name is the users short name. + // For example, it can be used to personalize user interfaces and notifications. + optional string nick_name = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Mini\""; + } + ]; + // The display name is how a user should primarily be displayed in lists. + // It can also for example be used to personalize user interfaces and notifications. + optional string display_name = 4 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Minnie Mouse\""; + } + ]; + // The users preferred language is the language that systems should use to interact with the user. + // It has the format of a [BCP-47 language tag](https://datatracker.ietf.org/doc/html/rfc3066). + // It is used by Zitadel where no higher prioritized preferred language can be used. + // For example, browser settings can overwrite a users preferred_language. + // Notification messages and standard login UIs use the users preferred language if it is supported and allowed on the instance. + // Else, the default language of the instance is used. + optional string preferred_language = 5 [ + (validate.rules).string = {min_len: 1, max_len: 10}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 10; + example: "\"en-US\""; + } + ]; + // The users gender can for example be used to personalize user interfaces and notifications. + optional Gender gender = 6 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"GENDER_FEMALE\""; + } + ]; + } + // Change the users profile information + optional Profile profile = 1; + // Change the users email address and/or trigger a verification email + optional SetHumanEmail email = 2; + // Change the users phone number and/or trigger a verification SMS + // To delete the users phone number, leave the phone field empty and omit the verification field. + optional SetHumanPhone phone = 3; + // Change the users password. + // You can optionally require the current password or the verification code to be correct. + optional SetPassword password = 4; + } + message Machine { + // The machine users name is a human readable field that helps identifying the user. + optional string name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Acceptance Test User\""; + } + ]; + // The description is a field that helps to remember the purpose of the user. + optional string description = 2 [ + (validate.rules).string = {max_len: 500}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"The user calls the session API in the continuous integration pipeline for acceptance tests.\""; + } + ]; + } + // The user id is the users unique identifier in the instance. + // It can't be changed. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // Set a new username that is unique within the instance. + // Beware that active tokens and sessions are invalidated when the username is changed. + optional string username = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"minnie-mouse\""; + } + ]; + // Change type specific properties of the user. + oneof user_type { + Human human = 3; + Machine machine = 4; + } + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"username\":\"minnie-mouse\",\"human\":{\"profile\":{\"givenName\":\"Minnie\",\"familyName\":\"Mouse\",\"displayName\":\"Minnie Mouse\"},\"email\":{\"email\":\"mini@mouse.com\",\"returnCode\":{}},\"phone\":{\"phone\":\"+41791234567\",\"isVerified\":true},\"password\":{\"password\":{\"password\":\"Secr3tP4ssw0rd!\",\"changeRequired\":true},\"verificationCode\":\"SKJd342k\"},\"totpSecret\":\"TJOPWSDYILLHXFV4MLKNNJOWFG7VSDCK\"}}"; + }; +} + +message UpdateUserResponse { + // The timestamp of the change of the user. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // In case the email verification was set to return_code, the code will be returned + optional string email_code = 2; + // In case the phone verification was set to return_code, the code will be returned + optional string phone_code = 3; +} + message UpdateHumanUserRequest{ string user_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -1595,7 +2496,6 @@ message DeactivateUserResponse { zitadel.object.v2.Details details = 1; } - message ReactivateUserRequest { string user_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -2384,3 +3284,237 @@ message HumanMFAInitSkippedRequest { message HumanMFAInitSkippedResponse { zitadel.object.v2.Details details = 1; } + +message AddSecretRequest { + // The users resource ID. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"163840776835432345\""; + } + ]; +} + +message AddSecretResponse { + // The timestamp of the secret creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The client secret. + // Store this secret in a secure place. + // It is not possible to retrieve it again. + string client_secret = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"WoYLHB23HAZaCSxeMJGEzbu8urHICVdFp2IegVr6Q5U4lZHKAtRvmaalNDWfCuHV\""; + } + ]; +} + +message RemoveSecretRequest { + // The users resource ID. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"163840776835432345\""; + } + ]; +} + +message RemoveSecretResponse { + // The timestamp of the secret deletion. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message AddKeyRequest { + // The users resource ID. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // The date the key will expire and no logins will be possible anymore. + google.protobuf.Timestamp expiration_date = 2 [ + (validate.rules).timestamp.required = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2519-04-01T08:45:00.000000Z\""; + } + ]; + // Optionally provide a public key of your own generated RSA private key. + bytes public_key = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1...\""; + } + ]; +} + +message AddKeyResponse { + // The timestamp of the key creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The keys ID. + string key_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"163840776835432345\""; + } + ]; + // The key which is usable to authenticate against the API. + bytes key_content = 3; +} + + +message RemoveKeyRequest { + // The users resource ID. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // The keys ID. + string key_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; +} + +message RemoveKeyResponse { + // The timestamp of the key deletion. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message ListKeysRequest { + // List limitations and ordering. + optional zitadel.filter.v2.PaginationRequest pagination = 1; + // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. + optional KeyFieldName sorting_column = 2 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"KEY_FIELD_NAME_CREATED_DATE\"" + } + ]; + // Define the criteria to query for. + repeated KeysSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":10,\"asc\":false},\"sortingColumn\":\"KEY_FIELD_NAME_CREATED_DATE\",\"filters\":[{\"andFilter\":{\"filters\":[{\"organizationIdFilter\":{\"organizationId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"notFilter\":{\"filter\":{\"userIdFilter\":{\"userId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}}}},{\"orFilter\":{\"filters\":[{\"keyIdFilter\":{\"keyId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"keyIdFilter\":{\"keyId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}}]}}]}}]}"; + }; +} + +message ListKeysResponse { + zitadel.filter.v2.PaginationResponse pagination = 1; + repeated Key result = 2; +} + +message AddPersonalAccessTokenRequest { + // The users resource ID. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // The timestamp when the token will expire. + google.protobuf.Timestamp expiration_date = 2 [ + (validate.rules).timestamp.required = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2519-04-01T08:45:00.000000Z\""; + } + ]; +} + +message AddPersonalAccessTokenResponse { + // The timestamp of the personal access token creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The tokens ID. + string token_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"163840776835432345\""; + } + ]; + // The personal access token that can be used to authenticate against the API + string token = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c\""; + } + ]; +} + +message RemovePersonalAccessTokenRequest { + // The users resource ID. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // The tokens ID. + string token_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; +} + +message RemovePersonalAccessTokenResponse { + // The timestamp of the personal access token deletion. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + + +message ListPersonalAccessTokensRequest { + // List limitations and ordering. + optional zitadel.filter.v2.PaginationRequest pagination = 1; + // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. + optional PersonalAccessTokenFieldName sorting_column = 2 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"PERSONAL_ACCESS_TOKEN_FIELD_NAME_CREATED_DATE\"" + } + ]; + // Define the criteria to query for. + repeated PersonalAccessTokensSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":10,\"asc\":false},\"sortingColumn\":\"PERSONAL_ACCESS_TOKEN_FIELD_NAME_CREATED_DATE\",\"filters\":[{\"andFilter\":{\"filters\":[{\"organizationIdFilter\":{\"organizationId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"notFilter\":{\"filter\":{\"userIdFilter\":{\"userId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}}}},{\"orFilter\":{\"filters\":[{\"personalAccessTokenIdFilter\":{\"personalAccessTokenId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"personalAccessTokenIdFilter\":{\"personalAccessTokenId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}}]}}]}}]}"; + }; +} + +message ListPersonalAccessTokensResponse { + zitadel.filter.v2.PaginationResponse pagination = 1; + repeated PersonalAccessToken result = 2; +} From 1a80e265020844a08586691bea135a69796745c4 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 4 Jun 2025 11:04:52 +0200 Subject: [PATCH 21/35] fix(console): org context for V2 user creation (#9971) # Which Problems Are Solved This PR addresses a bug in Console V2 APIs, specifically when the feature toggle is enabled, which caused incorrect organization context assignment during new user creation. Co-authored-by: Ramon --- .../user-create/user-create-v2/user-create-v2.component.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts index 9fd765264d..b92c112357 100644 --- a/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts +++ b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts @@ -32,6 +32,7 @@ import { withLatestFromSynchronousFix } from 'src/app/utils/withLatestFromSynchr import { PasswordComplexityValidatorFactoryService } from 'src/app/services/password-complexity-validator-factory.service'; import { NewFeatureService } from 'src/app/services/new-feature.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; type PwdForm = ReturnType; type AuthenticationFactor = @@ -65,6 +66,7 @@ export class UserCreateV2Component implements OnInit { private readonly destroyRef: DestroyRef, private readonly route: ActivatedRoute, protected readonly location: Location, + private readonly authService: GrpcAuthService, ) { this.userForm = this.buildUserForm(); @@ -180,9 +182,12 @@ export class UserCreateV2Component implements OnInit { private async createUserV2Try(authenticationFactor: AuthenticationFactor) { this.loading.set(true); + const org = await this.authService.getActiveOrg(); + const userValues = this.userForm.getRawValue(); const humanReq: MessageInitShape = { + organization: { org: { case: 'orgId', value: org.id } }, username: userValues.username, profile: { givenName: userValues.givenName, From 839c761357f17f51ede7f0a3f0f4a4c96a3863ab Mon Sep 17 00:00:00 2001 From: AnthonyKot Date: Wed, 4 Jun 2025 11:26:53 +0200 Subject: [PATCH 22/35] fix(FE): allow only enabled factors to be displayed on user page (#9313) # Which Problems Are Solved - Hides for users MFA options are not allowed by org policy. - Fix for "ng test" across "console" # How the Problems Are Solved - Before displaying MFA options we call "listMyMultiFactors" from parent component to filter MFA allowed by org # Additional Changes - Dependency Injection was fixed around ng unit tests # Additional Context admin view Screenshot 2025-02-06 at 00 26 50 user view Screenshot 2025-02-06 at 00 27 16 test Screenshot 2025-02-06 at 00 01 36 The issue: https://github.com/zitadel/zitadel/issues/9176 The bug report: https://discord.com/channels/927474939156643850/1307006457815896094 --------- Co-authored-by: a k Co-authored-by: a k Co-authored-by: a k Co-authored-by: Ramon --- console/package.json | 1 + .../oidc-configuration.component.spec.ts | 10 +- .../modules/domains/domains.component.spec.ts | 10 +- .../filter-project.component.spec.ts | 10 +- .../app/modules/input/input.directive.spec.ts | 45 +++++- .../app/modules/label/label.component.spec.ts | 10 +- .../login-policy/login-policy.component.ts | 7 +- .../message-texts.component.spec.ts | 10 +- .../notification-policy.component.spec.ts | 10 +- ...word-dialog-sms-provider.component.spec.ts | 10 +- .../provider-github-es.component.spec.ts | 10 +- ...vider-gitlab-self-hosted.component.spec.ts | 10 +- .../provider-gitlab.component.spec.ts | 10 +- .../show-token-dialog.component.spec.ts | 10 +- .../smtp-table/smtp-table.component.spec.ts | 10 +- .../add-action-dialog.component.spec.ts | 10 +- .../add-flow-dialog.component.spec.ts | 10 +- .../auth-factor-dialog.component.html | 13 +- .../auth-factor-dialog.component.ts | 14 +- .../auth-user-mfa.component.spec.ts | 133 +++++++++++++++++- .../auth-user-mfa/auth-user-mfa.component.ts | 127 +++++++++-------- .../user-detail/contact/contact.component.ts | 8 +- .../detail-form-machine.component.spec.ts | 10 +- .../passwordless.component.spec.ts | 10 +- console/src/app/services/grpc-auth.service.ts | 45 ------ console/src/app/services/new-auth.service.ts | 37 +++++ console/yarn.lock | 25 +++- 27 files changed, 411 insertions(+), 204 deletions(-) diff --git a/console/package.json b/console/package.json index 77a8a40147..0095360017 100644 --- a/console/package.json +++ b/console/package.json @@ -82,6 +82,7 @@ "jasmine-spec-reporter": "~7.0.0", "karma": "^6.4.4", "karma-chrome-launcher": "^3.2.0", + "karma-coverage": "^2.2.1", "karma-coverage-istanbul-reporter": "^3.0.3", "karma-jasmine": "^5.1.0", "karma-jasmine-html-reporter": "^2.1.0", diff --git a/console/src/app/components/oidc-configuration/oidc-configuration.component.spec.ts b/console/src/app/components/oidc-configuration/oidc-configuration.component.spec.ts index 6155ba5693..e99aee357c 100644 --- a/console/src/app/components/oidc-configuration/oidc-configuration.component.spec.ts +++ b/console/src/app/components/oidc-configuration/oidc-configuration.component.spec.ts @@ -1,16 +1,16 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { QuickstartComponent } from './quickstart.component'; +import { OIDCConfigurationComponent } from './oidc-configuration.component'; describe('QuickstartComponent', () => { - let component: QuickstartComponent; - let fixture: ComponentFixture; + let component: OIDCConfigurationComponent; + let fixture: ComponentFixture; beforeEach(() => { TestBed.configureTestingModule({ - declarations: [QuickstartComponent], + declarations: [OIDCConfigurationComponent], }); - fixture = TestBed.createComponent(QuickstartComponent); + fixture = TestBed.createComponent(OIDCConfigurationComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/domains/domains.component.spec.ts b/console/src/app/modules/domains/domains.component.spec.ts index 127bae48b5..f3d75fb12b 100644 --- a/console/src/app/modules/domains/domains.component.spec.ts +++ b/console/src/app/modules/domains/domains.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { OrgDomainsComponent } from './org-domains.component'; +import { DomainsComponent } from './domains.component'; describe('OrgDomainsComponent', () => { - let component: OrgDomainsComponent; - let fixture: ComponentFixture; + let component: DomainsComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [OrgDomainsComponent], + declarations: [DomainsComponent], }).compileComponents(); }); beforeEach(() => { - fixture = TestBed.createComponent(OrgDomainsComponent); + fixture = TestBed.createComponent(DomainsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/filter-project/filter-project.component.spec.ts b/console/src/app/modules/filter-project/filter-project.component.spec.ts index 0ed0436db8..ff465d8705 100644 --- a/console/src/app/modules/filter-project/filter-project.component.spec.ts +++ b/console/src/app/modules/filter-project/filter-project.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FilterUserComponent } from './filter-user.component'; +import { FilterProjectComponent } from './filter-project.component'; describe('FilterUserComponent', () => { - let component: FilterUserComponent; - let fixture: ComponentFixture; + let component: FilterProjectComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [FilterUserComponent], + declarations: [FilterProjectComponent], }).compileComponents(); }); beforeEach(() => { - fixture = TestBed.createComponent(FilterUserComponent); + fixture = TestBed.createComponent(FilterProjectComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/input/input.directive.spec.ts b/console/src/app/modules/input/input.directive.spec.ts index 463fed5431..46544ca096 100644 --- a/console/src/app/modules/input/input.directive.spec.ts +++ b/console/src/app/modules/input/input.directive.spec.ts @@ -1,8 +1,49 @@ +import { Component, ElementRef, NgZone } from '@angular/core'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; import { InputDirective } from './input.directive'; +import { Platform } from '@angular/cdk/platform'; +import { NgControl, NgForm, FormGroupDirective } from '@angular/forms'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { AutofillMonitor } from '@angular/cdk/text-field'; +import { MatFormField } from '@angular/material/form-field'; +import { MAT_INPUT_VALUE_ACCESSOR } from '@angular/material/input'; +import { of } from 'rxjs'; +import { By } from '@angular/platform-browser'; + +@Component({ + template: ``, +}) +class TestHostComponent {} describe('InputDirective', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [InputDirective, TestHostComponent], + providers: [ + { provide: ElementRef, useValue: new ElementRef(document.createElement('input')) }, + Platform, + { provide: NgControl, useValue: null }, + { provide: NgForm, useValue: null }, + { provide: FormGroupDirective, useValue: null }, + ErrorStateMatcher, + { provide: MAT_INPUT_VALUE_ACCESSOR, useValue: null }, + { + provide: AutofillMonitor, + useValue: { monitor: () => of(), stopMonitoring: () => {} }, + }, + NgZone, + { provide: MatFormField, useValue: null }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + }); + it('should create an instance', () => { - const directive = new InputDirective(); - expect(directive).toBeTruthy(); + const directiveEl = fixture.debugElement.query(By.directive(InputDirective)); + expect(directiveEl).toBeTruthy(); }); }); diff --git a/console/src/app/modules/label/label.component.spec.ts b/console/src/app/modules/label/label.component.spec.ts index 2b29b30873..e719aa3775 100644 --- a/console/src/app/modules/label/label.component.spec.ts +++ b/console/src/app/modules/label/label.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { AvatarComponent } from './avatar.component'; +import { LabelComponent } from './label.component'; describe('AvatarComponent', () => { - let component: AvatarComponent; - let fixture: ComponentFixture; + let component: LabelComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [AvatarComponent], + declarations: [LabelComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(AvatarComponent); + fixture = TestBed.createComponent(LabelComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/policies/login-policy/login-policy.component.ts b/console/src/app/modules/policies/login-policy/login-policy.component.ts index b4e4557f00..5dd85b8b38 100644 --- a/console/src/app/modules/policies/login-policy/login-policy.component.ts +++ b/console/src/app/modules/policies/login-policy/login-policy.component.ts @@ -2,14 +2,12 @@ import { Component, Injector, Input, OnDestroy, OnInit, Type } from '@angular/co import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { Duration } from 'google-protobuf/google/protobuf/duration_pb'; -import { firstValueFrom, forkJoin, from, Observable, of, Subject, take } from 'rxjs'; +import { forkJoin, from, of, Subject, take } from 'rxjs'; import { GetLoginPolicyResponse as AdminGetLoginPolicyResponse, UpdateLoginPolicyRequest, - UpdateLoginPolicyResponse, } from 'src/app/proto/generated/zitadel/admin_pb'; import { - AddCustomLoginPolicyRequest, GetLoginPolicyResponse as MgmtGetLoginPolicyResponse, UpdateCustomLoginPolicyRequest, } from 'src/app/proto/generated/zitadel/management_pb'; @@ -24,8 +22,7 @@ import { InfoSectionType } from '../../info-section/info-section.component'; import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component'; import { PolicyComponentServiceType } from '../policy-component-types.enum'; import { LoginMethodComponentType } from './factor-table/factor-table.component'; -import { catchError, map, takeUntil } from 'rxjs/operators'; -import { error } from 'console'; +import { map, takeUntil } from 'rxjs/operators'; import { LoginPolicyService } from '../../../services/login-policy.service'; const minValueValidator = (minValue: number) => (control: AbstractControl) => { diff --git a/console/src/app/modules/policies/message-texts/message-texts.component.spec.ts b/console/src/app/modules/policies/message-texts/message-texts.component.spec.ts index 71dd427da5..e5569f9ed3 100644 --- a/console/src/app/modules/policies/message-texts/message-texts.component.spec.ts +++ b/console/src/app/modules/policies/message-texts/message-texts.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { LoginPolicyComponent } from './login-policy.component'; +import { MessageTextsComponent } from './message-texts.component'; describe('LoginPolicyComponent', () => { - let component: LoginPolicyComponent; - let fixture: ComponentFixture; + let component: MessageTextsComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [LoginPolicyComponent], + declarations: [MessageTextsComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(LoginPolicyComponent); + fixture = TestBed.createComponent(MessageTextsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/policies/notification-policy/notification-policy.component.spec.ts b/console/src/app/modules/policies/notification-policy/notification-policy.component.spec.ts index c323d884f1..f529e143a5 100644 --- a/console/src/app/modules/policies/notification-policy/notification-policy.component.spec.ts +++ b/console/src/app/modules/policies/notification-policy/notification-policy.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { PasswordComplexityPolicyComponent } from './password-complexity-policy.component'; +import { NotificationPolicyComponent } from './notification-policy.component'; describe('PasswordComplexityPolicyComponent', () => { - let component: PasswordComplexityPolicyComponent; - let fixture: ComponentFixture; + let component: NotificationPolicyComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [PasswordComplexityPolicyComponent], + declarations: [NotificationPolicyComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(PasswordComplexityPolicyComponent); + fixture = TestBed.createComponent(NotificationPolicyComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/policies/notification-sms-provider/password-dialog-sms-provider/password-dialog-sms-provider.component.spec.ts b/console/src/app/modules/policies/notification-sms-provider/password-dialog-sms-provider/password-dialog-sms-provider.component.spec.ts index 034bbe8de0..b009b03757 100644 --- a/console/src/app/modules/policies/notification-sms-provider/password-dialog-sms-provider/password-dialog-sms-provider.component.spec.ts +++ b/console/src/app/modules/policies/notification-sms-provider/password-dialog-sms-provider/password-dialog-sms-provider.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { PasswordDialogComponent } from './password-dialog-sms-provider.component'; +import { PasswordDialogSMSProviderComponent } from './password-dialog-sms-provider.component'; describe('PasswordDialogComponent', () => { - let component: PasswordDialogComponent; - let fixture: ComponentFixture; + let component: PasswordDialogSMSProviderComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [PasswordDialogComponent], + declarations: [PasswordDialogSMSProviderComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(PasswordDialogComponent); + fixture = TestBed.createComponent(PasswordDialogSMSProviderComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/providers/provider-github-es/provider-github-es.component.spec.ts b/console/src/app/modules/providers/provider-github-es/provider-github-es.component.spec.ts index 0086bf0ce3..304004d0cf 100644 --- a/console/src/app/modules/providers/provider-github-es/provider-github-es.component.spec.ts +++ b/console/src/app/modules/providers/provider-github-es/provider-github-es.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ProviderOAuthComponent } from './provider-oauth.component'; +import { ProviderGithubESComponent } from './provider-github-es.component'; describe('ProviderOAuthComponent', () => { - let component: ProviderOAuthComponent; - let fixture: ComponentFixture; + let component: ProviderGithubESComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ProviderOAuthComponent], + declarations: [ProviderGithubESComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(ProviderOAuthComponent); + fixture = TestBed.createComponent(ProviderGithubESComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/providers/provider-gitlab-self-hosted/provider-gitlab-self-hosted.component.spec.ts b/console/src/app/modules/providers/provider-gitlab-self-hosted/provider-gitlab-self-hosted.component.spec.ts index 3b6fdadce3..5a0bbf6d08 100644 --- a/console/src/app/modules/providers/provider-gitlab-self-hosted/provider-gitlab-self-hosted.component.spec.ts +++ b/console/src/app/modules/providers/provider-gitlab-self-hosted/provider-gitlab-self-hosted.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ProviderGoogleComponent } from './provider-google.component'; +import { ProviderGitlabSelfHostedComponent } from './provider-gitlab-self-hosted.component'; describe('ProviderGoogleComponent', () => { - let component: ProviderGoogleComponent; - let fixture: ComponentFixture; + let component: ProviderGitlabSelfHostedComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ProviderGoogleComponent], + declarations: [ProviderGitlabSelfHostedComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(ProviderGoogleComponent); + fixture = TestBed.createComponent(ProviderGitlabSelfHostedComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/providers/provider-gitlab/provider-gitlab.component.spec.ts b/console/src/app/modules/providers/provider-gitlab/provider-gitlab.component.spec.ts index 3b6fdadce3..7b5becd782 100644 --- a/console/src/app/modules/providers/provider-gitlab/provider-gitlab.component.spec.ts +++ b/console/src/app/modules/providers/provider-gitlab/provider-gitlab.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ProviderGoogleComponent } from './provider-google.component'; +import { ProviderGitlabComponent } from './provider-gitlab.component'; describe('ProviderGoogleComponent', () => { - let component: ProviderGoogleComponent; - let fixture: ComponentFixture; + let component: ProviderGitlabComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ProviderGoogleComponent], + declarations: [ProviderGitlabComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(ProviderGoogleComponent); + fixture = TestBed.createComponent(ProviderGitlabComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/show-token-dialog/show-token-dialog.component.spec.ts b/console/src/app/modules/show-token-dialog/show-token-dialog.component.spec.ts index de74dc7522..35f4dbcf77 100644 --- a/console/src/app/modules/show-token-dialog/show-token-dialog.component.spec.ts +++ b/console/src/app/modules/show-token-dialog/show-token-dialog.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ShowKeyDialogComponent } from './show-key-dialog.component'; +import { ShowTokenDialogComponent } from './show-token-dialog.component'; describe('ShowKeyDialogComponent', () => { - let component: ShowKeyDialogComponent; - let fixture: ComponentFixture; + let component: ShowTokenDialogComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ShowKeyDialogComponent], + declarations: [ShowTokenDialogComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(ShowKeyDialogComponent); + fixture = TestBed.createComponent(ShowTokenDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/smtp-table/smtp-table.component.spec.ts b/console/src/app/modules/smtp-table/smtp-table.component.spec.ts index 8095d73255..fe4719482c 100644 --- a/console/src/app/modules/smtp-table/smtp-table.component.spec.ts +++ b/console/src/app/modules/smtp-table/smtp-table.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { IdpTableComponent } from './smtp-table.component'; +import { SMTPTableComponent } from './smtp-table.component'; describe('UserTableComponent', () => { - let component: IdpTableComponent; - let fixture: ComponentFixture; + let component: SMTPTableComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [IdpTableComponent], + declarations: [SMTPTableComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(IdpTableComponent); + fixture = TestBed.createComponent(SMTPTableComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/pages/actions/add-action-dialog/add-action-dialog.component.spec.ts b/console/src/app/pages/actions/add-action-dialog/add-action-dialog.component.spec.ts index f4b79bee55..b7c7069728 100644 --- a/console/src/app/pages/actions/add-action-dialog/add-action-dialog.component.spec.ts +++ b/console/src/app/pages/actions/add-action-dialog/add-action-dialog.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { AddKeyDialogComponent } from './add-key-dialog.component'; +import { AddActionDialogComponent } from './add-action-dialog.component'; describe('AddKeyDialogComponent', () => { - let component: AddKeyDialogComponent; - let fixture: ComponentFixture; + let component: AddActionDialogComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [AddKeyDialogComponent], + declarations: [AddActionDialogComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(AddKeyDialogComponent); + fixture = TestBed.createComponent(AddActionDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.spec.ts b/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.spec.ts index f4b79bee55..ca9b7c4507 100644 --- a/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.spec.ts +++ b/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { AddKeyDialogComponent } from './add-key-dialog.component'; +import { AddFlowDialogComponent } from './add-flow-dialog.component'; describe('AddKeyDialogComponent', () => { - let component: AddKeyDialogComponent; - let fixture: ComponentFixture; + let component: AddFlowDialogComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [AddKeyDialogComponent], + declarations: [AddFlowDialogComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(AddKeyDialogComponent); + fixture = TestBed.createComponent(AddFlowDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-factor-dialog/auth-factor-dialog.component.html b/console/src/app/pages/users/user-detail/auth-user-detail/auth-factor-dialog/auth-factor-dialog.component.html index e36ec2bcbb..22a4498090 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-factor-dialog/auth-factor-dialog.component.html +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-factor-dialog/auth-factor-dialog.component.html @@ -1,5 +1,5 @@

- {{ 'USER.MFA.DIALOG.ADD_MFA_TITLE' | translate }} {{ data?.number }} + {{ 'USER.MFA.DIALOG.ADD_MFA_TITLE' | translate }}

@@ -7,6 +7,7 @@
-
-
@@ -304,22 +317,22 @@

{{ 'IDP.DETAIL.DATECREATED' | translate }}

-

- {{ idp.details.creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }} +

+ {{ creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}

{{ 'IDP.DETAIL.DATECHANGED' | translate }}

-

- {{ idp.details.changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }} +

+ {{ changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}

{{ 'IDP.STATE' | translate }}

From 85e3b7449c417a238e12bac2b312ce4d58905804 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Wed, 4 Jun 2025 13:46:10 +0200 Subject: [PATCH 24/35] fix: correct permissions for projects on v2 api (#9973) # Which Problems Are Solved Permission checks in project v2beta API did not cover projects and granted projects correctly. # How the Problems Are Solved Add permission checks v1 correctly to the list queries, add correct permission checks v2 for projects. # Additional Changes Correct Pre-Checks for project grants that the right resource owner is used. # Additional Context Permission checks v2 for project grants is still outstanding under #9972. --- internal/api/grpc/management/project_grant.go | 4 +- .../v2beta/integration/project_grant_test.go | 65 ++- .../v2beta/integration/project_test.go | 65 +++ .../project/v2beta/integration/query_test.go | 182 ++++++- .../api/grpc/project/v2beta/project_grant.go | 10 +- internal/command/permission_checks.go | 18 +- internal/command/project_grant.go | 116 +++-- internal/command/project_grant_model.go | 62 +-- internal/command/project_grant_test.go | 444 +++++++++++++++++- internal/command/project_old.go | 2 +- internal/domain/roles.go | 1 + internal/integration/client.go | 18 + internal/query/project.go | 48 +- internal/query/project_grant.go | 15 +- internal/query/project_role.go | 2 +- 15 files changed, 950 insertions(+), 102 deletions(-) diff --git a/internal/api/grpc/management/project_grant.go b/internal/api/grpc/management/project_grant.go index e9313c1327..d84375818d 100644 --- a/internal/api/grpc/management/project_grant.go +++ b/internal/api/grpc/management/project_grant.go @@ -109,7 +109,7 @@ func (s *Server) UpdateProjectGrant(ctx context.Context, req *mgmt_pb.UpdateProj } func (s *Server) DeactivateProjectGrant(ctx context.Context, req *mgmt_pb.DeactivateProjectGrantRequest) (*mgmt_pb.DeactivateProjectGrantResponse, error) { - details, err := s.command.DeactivateProjectGrant(ctx, req.ProjectId, req.GrantId, authz.GetCtxData(ctx).OrgID) + details, err := s.command.DeactivateProjectGrant(ctx, req.ProjectId, req.GrantId, "", authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } @@ -119,7 +119,7 @@ func (s *Server) DeactivateProjectGrant(ctx context.Context, req *mgmt_pb.Deacti } func (s *Server) ReactivateProjectGrant(ctx context.Context, req *mgmt_pb.ReactivateProjectGrantRequest) (*mgmt_pb.ReactivateProjectGrantResponse, error) { - details, err := s.command.ReactivateProjectGrant(ctx, req.ProjectId, req.GrantId, authz.GetCtxData(ctx).OrgID) + details, err := s.command.ReactivateProjectGrant(ctx, req.ProjectId, req.GrantId, "", authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } diff --git a/internal/api/grpc/project/v2beta/integration/project_grant_test.go b/internal/api/grpc/project/v2beta/integration/project_grant_test.go index 8500f24d56..34fa10e3de 100644 --- a/internal/api/grpc/project/v2beta/integration/project_grant_test.go +++ b/internal/api/grpc/project/v2beta/integration/project_grant_test.go @@ -169,6 +169,12 @@ func TestServer_CreateProjectGrant_Permission(t *testing.T) { iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectResp := createProject(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + type want struct { creationDate bool } @@ -206,6 +212,33 @@ func TestServer_CreateProjectGrant_Permission(t *testing.T) { req: &project.CreateProjectGrantRequest{}, wantErr: true, }, + { + name: "project owner, other project", + ctx: projectOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "project owner, ok", + ctx: projectOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, { name: "organization owner, other org", ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), @@ -405,6 +438,13 @@ func TestServer_UpdateProjectGrant_Permission(t *testing.T) { iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false).GetId() + instance.CreateProjectGrant(iamOwnerCtx, t, projectID, orgResp.GetOrganizationId()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectID, orgResp.GetOrganizationId(), userResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + type args struct { ctx context.Context req *project.UpdateProjectGrantRequest @@ -458,6 +498,25 @@ func TestServer_UpdateProjectGrant_Permission(t *testing.T) { }, wantErr: true, }, + { + name: "project grant owner, no permission", + prepare: func(request *project.UpdateProjectGrantRequest) { + roles := []string{gofakeit.Animal(), gofakeit.Animal(), gofakeit.Animal()} + request.ProjectId = projectID + request.GrantedOrganizationId = orgResp.GetOrganizationId() + + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectID, role, role, "") + } + + request.RoleKeys = roles + }, + args: args{ + ctx: projectGrantOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + wantErr: true, + }, { name: "organization owner, other org", prepare: func(request *project.UpdateProjectGrantRequest) { @@ -598,7 +657,7 @@ func TestServer_DeleteProjectGrant(t *testing.T) { ProjectId: "notexisting", GrantedOrganizationId: "notexisting", }, - wantErr: true, + wantDeletionDate: false, }, { name: "delete", @@ -650,8 +709,8 @@ func TestServer_DeleteProjectGrant(t *testing.T) { instance.DeleteProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) return creationDate, time.Now().UTC() }, - req: &project.DeleteProjectGrantRequest{}, - wantErr: true, + req: &project.DeleteProjectGrantRequest{}, + wantDeletionDate: true, }, } for _, tt := range tests { diff --git a/internal/api/grpc/project/v2beta/integration/project_test.go b/internal/api/grpc/project/v2beta/integration/project_test.go index 6c0a5c96f6..5412f6eb58 100644 --- a/internal/api/grpc/project/v2beta/integration/project_test.go +++ b/internal/api/grpc/project/v2beta/integration/project_test.go @@ -300,6 +300,12 @@ func TestServer_UpdateProject_Permission(t *testing.T) { iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + instance.CreateProjectMembership(t, iamOwnerCtx, projectID, userResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + type args struct { ctx context.Context req *project.UpdateProjectRequest @@ -343,6 +349,36 @@ func TestServer_UpdateProject_Permission(t *testing.T) { }, wantErr: true, }, + { + name: "project owner, no permission", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: projectOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + }, + }, + wantErr: true, + }, + { + name: " roject owner, ok", + prepare: func(request *project.UpdateProjectRequest) { + request.Id = projectID + }, + args: args{ + ctx: projectOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, { name: "missing permission, other organization", prepare: func(request *project.UpdateProjectRequest) { @@ -499,6 +535,12 @@ func TestServer_DeleteProject_Permission(t *testing.T) { iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + instance.CreateProjectMembership(t, iamOwnerCtx, projectID, userResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + tests := []struct { name string ctx context.Context @@ -531,6 +573,29 @@ func TestServer_DeleteProject_Permission(t *testing.T) { req: &project.DeleteProjectRequest{}, wantErr: true, }, + { + name: "project owner, no permission", + ctx: projectOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantErr: true, + }, + { + name: "project owner, ok", + ctx: projectOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantDeletionDate: true, + }, { name: "organization owner, other org", ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), diff --git a/internal/api/grpc/project/v2beta/integration/query_test.go b/internal/api/grpc/project/v2beta/integration/query_test.go index 517f103628..fc153f0130 100644 --- a/internal/api/grpc/project/v2beta/integration/query_test.go +++ b/internal/api/grpc/project/v2beta/integration/query_test.go @@ -148,6 +148,14 @@ func TestServer_GetProject(t *testing.T) { func TestServer_ListProjects(t *testing.T) { iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectResp := createProject(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetUserId()) + grantedProjectResp := createGrantedProject(iamOwnerCtx, instance, t, projectResp) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + type args struct { ctx context.Context dep func(*project.ListProjectsRequest, *project.ListProjectsResponse) @@ -370,6 +378,39 @@ func TestServer_ListProjects(t *testing.T) { }, }, }, + { + name: "list multiple id, limited permissions, project owner", + args: args{ + ctx: projectOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + resp1 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, false) + resp2 := createProject(iamOwnerCtx, instance, t, orgID, true, false) + resp3 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId(), projectResp.GetId()}, + }, + } + response.Projects[0] = grantedProjectResp + response.Projects[1] = projectResp + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 5, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + }, + }, + }, { name: "list project and granted projects", args: args{ @@ -462,6 +503,51 @@ func TestServer_ListProjects(t *testing.T) { }, }, }, + { + name: "list granted project, project id", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + + orgName := gofakeit.AppName() + projectName := gofakeit.AppName() + orgResp := instance.CreateOrganization(iamOwnerCtx, orgName, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), projectName, true, true) + projectGrantResp := instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgID) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ProjectIds: []string{projectResp.GetId()}}, + } + response.Projects[0] = &project.Project{ + Id: projectResp.GetId(), + Name: projectName, + OrganizationId: orgResp.GetOrganizationId(), + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: true, + AuthorizationRequired: true, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + GrantedOrganizationId: gu.Ptr(orgID), + GrantedOrganizationName: gu.Ptr(instance.DefaultOrg.GetName()), + GrantedState: 1, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -791,6 +877,53 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { }, }, }, + // TODO: correct when permission check is added for project grants https://github.com/zitadel/zitadel/issues/9972 + { + name: "list granted project, project id", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instancePermissionV2.DefaultOrg.GetId() + + orgName := gofakeit.AppName() + projectName := gofakeit.AppName() + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, orgName, gofakeit.Email()) + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), projectName, true, true) + // projectGrantResp := + instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgID) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ProjectIds: []string{projectResp.GetId()}}, + } + /* + response.Projects[0] = &project.Project{ + Id: projectResp.GetId(), + Name: projectName, + OrganizationId: orgResp.GetOrganizationId(), + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: true, + AuthorizationRequired: true, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + GrantedOrganizationId: gu.Ptr(orgID), + GrantedOrganizationName: gu.Ptr(instancePermissionV2.DefaultOrg.GetName()), + GrantedState: 1, + } + */ + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + Projects: []*project.Project{}, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -865,6 +998,14 @@ func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationRes func TestServer_ListProjectGrants(t *testing.T) { iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectResp := createProject(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), false, false) + projectGrantResp := createProjectGrant(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectResp.GetName()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), projectGrantResp.GetGrantedOrganizationId(), userResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + type args struct { ctx context.Context dep func(*project.ListProjectGrantsRequest, *project.ListProjectGrantsResponse) @@ -1071,7 +1212,46 @@ func TestServer_ListProjectGrants(t *testing.T) { {}, }, }, - }, { + }, + { + name: "list multiple id, limited permissions, project grant owner", + args: args{ + ctx: projectGrantOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name1 := gofakeit.AppName() + name2 := gofakeit.AppName() + name3 := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + project1Resp := instance.CreateProject(iamOwnerCtx, t, orgID, name1, false, false) + project2Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false) + project3Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId(), projectResp.GetId()}, + }, + } + + createProjectGrant(iamOwnerCtx, instance, t, orgID, project1Resp.GetId(), name1) + createProjectGrant(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), project2Resp.GetId(), name2) + createProjectGrant(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), project3Resp.GetId(), name3) + response.ProjectGrants[0] = projectGrantResp + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + { name: "list single id with role", args: args{ ctx: iamOwnerCtx, diff --git a/internal/api/grpc/project/v2beta/project_grant.go b/internal/api/grpc/project/v2beta/project_grant.go index c1c20d9cbc..6c3b195c66 100644 --- a/internal/api/grpc/project/v2beta/project_grant.go +++ b/internal/api/grpc/project/v2beta/project_grant.go @@ -56,13 +56,13 @@ func projectGrantUpdateToCommand(req *project_pb.UpdateProjectGrantRequest) *com ObjectRoot: models.ObjectRoot{ AggregateID: req.ProjectId, }, - GrantID: req.GrantedOrganizationId, - RoleKeys: req.RoleKeys, + GrantedOrgID: req.GrantedOrganizationId, + RoleKeys: req.RoleKeys, } } func (s *Server) DeactivateProjectGrant(ctx context.Context, req *project_pb.DeactivateProjectGrantRequest) (*project_pb.DeactivateProjectGrantResponse, error) { - details, err := s.command.DeactivateProjectGrant(ctx, req.ProjectId, req.GrantedOrganizationId, "") + details, err := s.command.DeactivateProjectGrant(ctx, req.ProjectId, "", req.GrantedOrganizationId, "") if err != nil { return nil, err } @@ -76,7 +76,7 @@ func (s *Server) DeactivateProjectGrant(ctx context.Context, req *project_pb.Dea } func (s *Server) ActivateProjectGrant(ctx context.Context, req *project_pb.ActivateProjectGrantRequest) (*project_pb.ActivateProjectGrantResponse, error) { - details, err := s.command.ReactivateProjectGrant(ctx, req.ProjectId, req.GrantedOrganizationId, "") + details, err := s.command.ReactivateProjectGrant(ctx, req.ProjectId, "", req.GrantedOrganizationId, "") if err != nil { return nil, err } @@ -94,7 +94,7 @@ func (s *Server) DeleteProjectGrant(ctx context.Context, req *project_pb.DeleteP if err != nil { return nil, err } - details, err := s.command.RemoveProjectGrant(ctx, req.ProjectId, req.GrantedOrganizationId, "", userGrantIDs...) + details, err := s.command.DeleteProjectGrant(ctx, req.ProjectId, "", req.GrantedOrganizationId, "", userGrantIDs...) if err != nil { return nil, err } diff --git a/internal/command/permission_checks.go b/internal/command/permission_checks.go index 253b6ee72a..6bfeaae219 100644 --- a/internal/command/permission_checks.go +++ b/internal/command/permission_checks.go @@ -68,6 +68,20 @@ func (c *Commands) checkPermissionUpdateProject(ctx context.Context, resourceOwn return c.newPermissionCheck(ctx, domain.PermissionProjectWrite, project.AggregateType)(resourceOwner, projectID) } -func (c *Commands) checkPermissionWriteProjectGrant(ctx context.Context, resourceOwner, projectGrantID string) error { - return c.newPermissionCheck(ctx, domain.PermissionProjectGrantWrite, project.AggregateType)(resourceOwner, projectGrantID) +func (c *Commands) checkPermissionUpdateProjectGrant(ctx context.Context, resourceOwner, projectID, projectGrantID string) (err error) { + if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantWrite, project.AggregateType)(resourceOwner, projectGrantID); err != nil { + if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantWrite, project.AggregateType)(resourceOwner, projectID); err != nil { + return err + } + } + return nil +} + +func (c *Commands) checkPermissionDeleteProjectGrant(ctx context.Context, resourceOwner, projectID, projectGrantID string) (err error) { + if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantDelete, project.AggregateType)(resourceOwner, projectGrantID); err != nil { + if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantDelete, project.AggregateType)(resourceOwner, projectID); err != nil { + return err + } + } + return nil } diff --git a/internal/command/project_grant.go b/internal/command/project_grant.go index 763ea7ab67..b613974b7e 100644 --- a/internal/command/project_grant.go +++ b/internal/command/project_grant.go @@ -58,11 +58,11 @@ func (c *Commands) AddProjectGrant(ctx context.Context, grant *AddProjectGrant) if grant.ResourceOwner == "" { grant.ResourceOwner = projectResourceOwner } - if err := c.checkPermissionWriteProjectGrant(ctx, grant.ResourceOwner, grant.GrantID); err != nil { + if err := c.checkPermissionUpdateProjectGrant(ctx, grant.ResourceOwner, grant.AggregateID, grant.GrantID); err != nil { return nil, err } - wm := NewProjectGrantWriteModel(grant.GrantID, grant.AggregateID, grant.ResourceOwner) + wm := NewProjectGrantWriteModel(grant.GrantID, grant.GrantedOrgID, grant.AggregateID, grant.ResourceOwner) // error if provided resourceowner is not equal to the resourceowner of the project or the project grant is for the same organization if projectResourceOwner != wm.ResourceOwner || wm.ResourceOwner == grant.GrantedOrgID { return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-ckUpbvboAH", "Errors.Project.Grant.Invalid") @@ -83,19 +83,24 @@ func (c *Commands) AddProjectGrant(ctx context.Context, grant *AddProjectGrant) type ChangeProjectGrant struct { es_models.ObjectRoot - GrantID string - RoleKeys []string + GrantID string + GrantedOrgID string + RoleKeys []string } func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *ChangeProjectGrant, cascadeUserGrantIDs ...string) (_ *domain.ObjectDetails, err error) { - if grant.GrantID == "" { + if grant.GrantID == "" && grant.GrantedOrgID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-1j83s", "Errors.IDMissing") } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grant.GrantID, grant.AggregateID, grant.ResourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grant.GrantID, grant.GrantedOrgID, grant.AggregateID, grant.ResourceOwner) if err != nil { return nil, err } - if err := c.checkPermissionWriteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.GrantID); err != nil { + if !existingGrant.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") + } + + if err := c.checkPermissionUpdateProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { return nil, err } projectResourceOwner, err := c.checkProjectGrantPreCondition(ctx, existingGrant.AggregateID, existingGrant.GrantedOrgID, existingGrant.ResourceOwner, grant.RoleKeys) @@ -152,12 +157,12 @@ func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *ChangeProjectG } func (c *Commands) removeRoleFromProjectGrant(ctx context.Context, projectAgg *eventstore.Aggregate, projectID, projectGrantID, roleKey string, cascade bool) (_ eventstore.Command, _ *ProjectGrantWriteModel, err error) { - existingProjectGrant, err := c.projectGrantWriteModelByID(ctx, projectGrantID, projectID, "") + existingProjectGrant, err := c.projectGrantWriteModelByID(ctx, projectGrantID, "", projectID, "") if err != nil { return nil, nil, err } - if existingProjectGrant.State == domain.ProjectGrantStateUnspecified || existingProjectGrant.State == domain.ProjectGrantStateRemoved { - return nil, nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.Project.Grant.NotFound") + if !existingProjectGrant.State.Exists() { + return nil, nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") } keyExists := false for i, key := range existingProjectGrant.RoleKeys { @@ -172,7 +177,7 @@ func (c *Commands) removeRoleFromProjectGrant(ctx context.Context, projectAgg *e if !keyExists { return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5m8g9", "Errors.Project.Grant.RoleKeyNotFound") } - changedProjectGrant := NewProjectGrantWriteModel(projectGrantID, projectID, existingProjectGrant.ResourceOwner) + changedProjectGrant := NewProjectGrantWriteModel(projectGrantID, projectID, "", existingProjectGrant.ResourceOwner) if cascade { return project.NewGrantCascadeChangedEvent(ctx, projectAgg, projectGrantID, existingProjectGrant.RoleKeys), changedProjectGrant, nil @@ -181,8 +186,8 @@ func (c *Commands) removeRoleFromProjectGrant(ctx context.Context, projectAgg *e return project.NewGrantChangedEvent(ctx, projectAgg, projectGrantID, existingProjectGrant.RoleKeys), changedProjectGrant, nil } -func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantID, resourceOwner string) (details *domain.ObjectDetails, err error) { - if grantID == "" || projectID == "" { +func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string) (details *domain.ObjectDetails, err error) { + if (grantID == "" && grantedOrgID == "") || projectID == "" { return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing") } @@ -191,10 +196,13 @@ func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantI return nil, err } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, projectID, resourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner) if err != nil { return details, err } + if !existingGrant.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") + } // error if provided resourceowner is not equal to the resourceowner of the project if projectResourceOwner != existingGrant.ResourceOwner { return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-0l10S9OmZV", "Errors.Project.Grant.Invalid") @@ -207,13 +215,13 @@ func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantI if existingGrant.State != domain.ProjectGrantStateActive { return details, zerrors.ThrowPreconditionFailed(nil, "PROJECT-47fu8", "Errors.Project.Grant.NotActive") } - if err := c.checkPermissionWriteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.GrantID); err != nil { + if err := c.checkPermissionUpdateProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { return nil, err } pushedEvents, err := c.eventstore.Push(ctx, project.NewGrantDeactivateEvent(ctx, ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), - grantID, + existingGrant.GrantID, ), ) if err != nil { @@ -226,8 +234,8 @@ func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantI return writeModelToObjectDetails(&existingGrant.WriteModel), nil } -func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantID, resourceOwner string) (details *domain.ObjectDetails, err error) { - if grantID == "" || projectID == "" { +func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string) (details *domain.ObjectDetails, err error) { + if (grantID == "" && grantedOrgID == "") || projectID == "" { return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing") } @@ -236,10 +244,13 @@ func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantI return nil, err } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, projectID, resourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner) if err != nil { return details, err } + if !existingGrant.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") + } // error if provided resourceowner is not equal to the resourceowner of the project if projectResourceOwner != existingGrant.ResourceOwner { return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-byscAarAST", "Errors.Project.Grant.Invalid") @@ -252,13 +263,13 @@ func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantI if existingGrant.State != domain.ProjectGrantStateInactive { return details, zerrors.ThrowPreconditionFailed(nil, "PROJECT-47fu8", "Errors.Project.Grant.NotInactive") } - if err := c.checkPermissionWriteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.GrantID); err != nil { + if err := c.checkPermissionUpdateProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { return nil, err } pushedEvents, err := c.eventstore.Push(ctx, project.NewGrantReactivatedEvent(ctx, ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), - grantID, + existingGrant.GrantID, ), ) if err != nil { @@ -271,25 +282,25 @@ func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantI return writeModelToObjectDetails(&existingGrant.WriteModel), nil } +// Deprecated: use commands.DeleteProjectGrant func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, resourceOwner string, cascadeUserGrantIDs ...string) (details *domain.ObjectDetails, err error) { if grantID == "" || projectID == "" { return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-1m9fJ", "Errors.IDMissing") } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, projectID, resourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, "", projectID, resourceOwner) if err != nil { return details, err } - // return if project grant does not exist, or was removed already if !existingGrant.State.Exists() { - return writeModelToObjectDetails(&existingGrant.WriteModel), nil + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") } - if err := c.checkPermissionDeleteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.GrantID); err != nil { + if err := c.checkPermissionDeleteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { return nil, err } events := make([]eventstore.Command, 0) events = append(events, project.NewGrantRemovedEvent(ctx, ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), - grantID, + existingGrant.GrantID, existingGrant.GrantedOrgID, ), ) @@ -297,7 +308,7 @@ func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, r for _, userGrantID := range cascadeUserGrantIDs { event, _, err := c.removeUserGrant(ctx, userGrantID, "", true) if err != nil { - logging.LogWithFields("COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") + logging.WithFields("id", "COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") continue } events = append(events, event) @@ -313,24 +324,57 @@ func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, r return writeModelToObjectDetails(&existingGrant.WriteModel), nil } -func (c *Commands) checkPermissionDeleteProjectGrant(ctx context.Context, resourceOwner, projectGrantID string) error { - return c.checkPermission(ctx, domain.PermissionProjectGrantDelete, resourceOwner, projectGrantID) +func (c *Commands) DeleteProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string, cascadeUserGrantIDs ...string) (details *domain.ObjectDetails, err error) { + if (grantID == "" && grantedOrgID == "") || projectID == "" { + return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-1m9fJ", "Errors.IDMissing") + } + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner) + if err != nil { + return details, err + } + // return if project grant does not exist, or was removed already + if !existingGrant.State.Exists() { + return writeModelToObjectDetails(&existingGrant.WriteModel), nil + } + if err := c.checkPermissionDeleteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { + return nil, err + } + events := make([]eventstore.Command, 0) + events = append(events, project.NewGrantRemovedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), + existingGrant.GrantID, + existingGrant.GrantedOrgID, + ), + ) + + for _, userGrantID := range cascadeUserGrantIDs { + event, _, err := c.removeUserGrant(ctx, userGrantID, "", true) + if err != nil { + logging.WithFields("id", "COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") + continue + } + events = append(events, event) + } + pushedEvents, err := c.eventstore.Push(ctx, events...) + if err != nil { + return nil, err + } + err = AppendAndReduce(existingGrant, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&existingGrant.WriteModel), nil } -func (c *Commands) projectGrantWriteModelByID(ctx context.Context, grantID, projectID, resourceOwner string) (member *ProjectGrantWriteModel, err error) { +func (c *Commands) projectGrantWriteModelByID(ctx context.Context, grantID, grantedOrgID, projectID, resourceOwner string) (member *ProjectGrantWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - writeModel := NewProjectGrantWriteModel(grantID, projectID, resourceOwner) + writeModel := NewProjectGrantWriteModel(grantID, grantedOrgID, projectID, resourceOwner) err = c.eventstore.FilterToQueryReducer(ctx, writeModel) if err != nil { return nil, err } - - if writeModel.State == domain.ProjectGrantStateUnspecified || writeModel.State == domain.ProjectGrantStateRemoved { - return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") - } - return writeModel, nil } diff --git a/internal/command/project_grant_model.go b/internal/command/project_grant_model.go index a8c1fe2850..15950d4f3d 100644 --- a/internal/command/project_grant_model.go +++ b/internal/command/project_grant_model.go @@ -16,13 +16,14 @@ type ProjectGrantWriteModel struct { State domain.ProjectGrantState } -func NewProjectGrantWriteModel(grantID, projectID, resourceOwner string) *ProjectGrantWriteModel { +func NewProjectGrantWriteModel(grantID, grantedOrgID, projectID, resourceOwner string) *ProjectGrantWriteModel { return &ProjectGrantWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: projectID, ResourceOwner: resourceOwner, }, - GrantID: grantID, + GrantID: grantID, + GrantedOrgID: grantedOrgID, } } @@ -30,27 +31,28 @@ func (wm *ProjectGrantWriteModel) AppendEvents(events ...eventstore.Event) { for _, event := range events { switch e := event.(type) { case *project.GrantAddedEvent: - if e.GrantID == wm.GrantID { + if (wm.GrantID != "" && e.GrantID == wm.GrantID) || + (wm.GrantedOrgID != "" && e.GrantedOrgID == wm.GrantedOrgID) { wm.WriteModel.AppendEvents(e) } case *project.GrantChangedEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.GrantCascadeChangedEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.GrantDeactivateEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.GrantReactivatedEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.GrantRemovedEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.ProjectRemovedEvent: @@ -114,18 +116,20 @@ func (wm *ProjectGrantWriteModel) Query() *eventstore.SearchQueryBuilder { type ProjectGrantPreConditionReadModel struct { eventstore.WriteModel - ProjectID string - GrantedOrgID string - ProjectExists bool - GrantedOrgExists bool - ExistingRoleKeys []string + ProjectResourceOwner string + ProjectID string + GrantedOrgID string + ProjectExists bool + GrantedOrgExists bool + ExistingRoleKeys []string } func NewProjectGrantPreConditionReadModel(projectID, grantedOrgID, resourceOwner string) *ProjectGrantPreConditionReadModel { return &ProjectGrantPreConditionReadModel{ - WriteModel: eventstore.WriteModel{ResourceOwner: resourceOwner}, - ProjectID: projectID, - GrantedOrgID: grantedOrgID, + WriteModel: eventstore.WriteModel{}, + ProjectResourceOwner: resourceOwner, + ProjectID: projectID, + GrantedOrgID: grantedOrgID, } } @@ -133,26 +137,26 @@ func (wm *ProjectGrantPreConditionReadModel) Reduce() error { for _, event := range wm.Events { switch e := event.(type) { case *project.ProjectAddedEvent: - if wm.ResourceOwner == "" { - wm.ResourceOwner = e.Aggregate().ResourceOwner + if wm.ProjectResourceOwner == "" { + wm.ProjectResourceOwner = e.Aggregate().ResourceOwner } - if wm.ResourceOwner != e.Aggregate().ResourceOwner { + if wm.ProjectResourceOwner != e.Aggregate().ResourceOwner { continue } wm.ProjectExists = true case *project.ProjectRemovedEvent: - if wm.ResourceOwner != e.Aggregate().ResourceOwner { + if wm.ProjectResourceOwner != e.Aggregate().ResourceOwner { continue } - wm.ResourceOwner = "" + wm.ProjectResourceOwner = "" wm.ProjectExists = false case *project.RoleAddedEvent: - if e.Aggregate().ResourceOwner != wm.ResourceOwner { + if e.Aggregate().ResourceOwner != wm.ProjectResourceOwner { continue } wm.ExistingRoleKeys = append(wm.ExistingRoleKeys, e.Key) case *project.RoleRemovedEvent: - if e.Aggregate().ResourceOwner != wm.ResourceOwner { + if e.Aggregate().ResourceOwner != wm.ProjectResourceOwner { continue } for i, key := range wm.ExistingRoleKeys { @@ -175,12 +179,6 @@ func (wm *ProjectGrantPreConditionReadModel) Reduce() error { func (wm *ProjectGrantPreConditionReadModel) Query() *eventstore.SearchQueryBuilder { query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). AddQuery(). - AggregateTypes(org.AggregateType). - AggregateIDs(wm.GrantedOrgID). - EventTypes( - org.OrgAddedEventType, - org.OrgRemovedEventType). - Or(). AggregateTypes(project.AggregateType). AggregateIDs(wm.ProjectID). EventTypes( @@ -188,6 +186,12 @@ func (wm *ProjectGrantPreConditionReadModel) Query() *eventstore.SearchQueryBuil project.ProjectRemovedType, project.RoleAddedType, project.RoleRemovedType). + Or(). + AggregateTypes(org.AggregateType). + AggregateIDs(wm.GrantedOrgID). + EventTypes( + org.OrgAddedEventType, + org.OrgRemovedEventType). Builder() return query diff --git a/internal/command/project_grant_test.go b/internal/command/project_grant_test.go index f1befa0de2..7a3bb98e7d 100644 --- a/internal/command/project_grant_test.go +++ b/internal/command/project_grant_test.go @@ -720,6 +720,76 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { }, }, }, + { + name: "projectgrant only added roles, grantedOrgID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("grantedorg1").Aggregate, + "granted org", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key2", + "key2", + "", + ), + ), + ), + expectPush( + project.NewGrantChangedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + []string{"key1", "key2"}, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectGrant: &ChangeProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + GrantedOrgID: "grantedorg1", + RoleKeys: []string{"key1", "key2"}, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, { name: "projectgrant remove roles, usergrant not found, ok", fields: fields{ @@ -907,6 +977,7 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { ctx context.Context projectID string grantID string + grantedOrgID string resourceOwner string } type res struct { @@ -1076,6 +1147,48 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { }, }, }, + { + name: "projectgrant deactivate, grantedOrgID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectPush( + project.NewGrantDeactivateEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantedOrgID: "grantedorg1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1083,7 +1196,7 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { eventstore: tt.fields.eventstore(t), checkPermission: tt.fields.checkPermission, } - got, err := r.DeactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.resourceOwner) + got, err := r.DeactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.grantedOrgID, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } @@ -1106,6 +1219,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { ctx context.Context projectID string grantID string + grantedOrgID string resourceOwner string } type res struct { @@ -1275,6 +1389,52 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { }, }, }, + { + name: "projectgrant reactivate, grantedOrgID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + eventFromEventPusher(project.NewGrantDeactivateEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + )), + ), + expectPush( + project.NewGrantReactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantedOrgID: "grantedorg1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1282,7 +1442,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { eventstore: tt.fields.eventstore(t), checkPermission: tt.fields.checkPermission, } - got, err := r.ReactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.resourceOwner) + got, err := r.ReactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.grantedOrgID, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } @@ -1536,3 +1696,283 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { }) } } + +func TestCommandSide_DeleteProjectGrant(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + ctx context.Context + projectID string + grantID string + grantedOrgID string + resourceOwner string + cascadeUserGrantIDs []string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "missing projectid, invalid error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "missing grantid, invalid error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "project already removed, precondition failed error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + eventFromEventPusher( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + nil, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant not existing, precondition error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectPush( + project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove, grantedOrgID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectPush( + project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantedOrgID: "grantedorg1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove, cascading usergrant not found, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter(), + expectPush( + project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + cascadeUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove with cascading usergrants, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter( + eventFromEventPusher(usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", + []string{"key1"}))), + expectPush( + project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + ), + usergrant.NewUserGrantCascadeRemovedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + cascadeUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + got, err := r.DeleteProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.grantedOrgID, tt.args.resourceOwner, tt.args.cascadeUserGrantIDs...) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.want, got) + } + }) + } +} diff --git a/internal/command/project_old.go b/internal/command/project_old.go index e1b6f02721..99d7dd2e34 100644 --- a/internal/command/project_old.go +++ b/internal/command/project_old.go @@ -94,5 +94,5 @@ func (c *Commands) checkProjectGrantPreConditionOld(ctx context.Context, project if domain.HasInvalidRoles(preConditions.ExistingRoleKeys, roles) { return "", zerrors.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound") } - return preConditions.ResourceOwner, nil + return preConditions.ProjectResourceOwner, nil } diff --git a/internal/domain/roles.go b/internal/domain/roles.go index b6bf2ffadd..c40eef6120 100644 --- a/internal/domain/roles.go +++ b/internal/domain/roles.go @@ -16,6 +16,7 @@ const ( RoleIAMOwner = "IAM_OWNER" RoleProjectOwner = "PROJECT_OWNER" RoleProjectOwnerGlobal = "PROJECT_OWNER_GLOBAL" + RoleProjectGrantOwner = "PROJECT_GRANT_OWNER" RoleSelfManagementGlobal = "SELF_MANAGEMENT_GLOBAL" ) diff --git a/internal/integration/client.go b/internal/integration/client.go index 3bf794f5f6..838a728faf 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -269,6 +269,14 @@ func (i *Instance) CreateUserTypeMachine(ctx context.Context) *user_v2.CreateUse return resp } +func (i *Instance) CreatePersonalAccessToken(ctx context.Context, userID string) *user_v2.AddPersonalAccessTokenResponse { + resp, err := i.Client.UserV2.AddPersonalAccessToken(ctx, &user_v2.AddPersonalAccessTokenRequest{ + UserId: userID, + }) + logging.OnError(err).Panic("create pat") + return resp +} + // TriggerUserByID makes sure the user projection gets triggered after creation. func (i *Instance) TriggerUserByID(ctx context.Context, users ...string) { var wg sync.WaitGroup @@ -903,6 +911,16 @@ func (i *Instance) CreateProjectMembership(t *testing.T, ctx context.Context, pr require.NoError(t, err) } +func (i *Instance) CreateProjectGrantMembership(t *testing.T, ctx context.Context, projectID, grantID, userID string) { + _, err := i.Client.Mgmt.AddProjectGrantMember(ctx, &mgmt.AddProjectGrantMemberRequest{ + ProjectId: projectID, + GrantId: grantID, + UserId: userID, + Roles: []string{domain.RoleProjectGrantOwner}, + }) + require.NoError(t, err) +} + func (i *Instance) CreateTarget(ctx context.Context, t *testing.T, name, endpoint string, ty domain.TargetType, interrupt bool) *action.CreateTargetResponse { if name == "" { name = gofakeit.Name() diff --git a/internal/query/project.go b/internal/query/project.go index ab58bd11a8..728731f7cb 100644 --- a/internal/query/project.go +++ b/internal/query/project.go @@ -126,6 +126,10 @@ var ( name: "project_grant_resource_owner", table: grantedProjectsAlias, } + grantedProjectColumnGrantID = Column{ + name: projection.ProjectGrantColumnGrantID, + table: grantedProjectsAlias, + } grantedProjectColumnGrantedOrganization = Column{ name: projection.ProjectGrantColumnGrantedOrgID, table: grantedProjectsAlias, @@ -157,20 +161,6 @@ func projectCheckPermission(ctx context.Context, resourceOwner string, projectID return permissionCheck(ctx, domain.PermissionProjectRead, resourceOwner, projectID) } -func projectPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *ProjectAndGrantedProjectSearchQueries) sq.SelectBuilder { - if !enabled { - return query - } - join, args := PermissionClause( - ctx, - grantedProjectColumnResourceOwner, - domain.PermissionProjectRead, - SingleOrgPermissionOption(queries.Queries), - OwnedRowsPermissionOption(GrantedProjectColumnID), - ) - return query.JoinClause(join, args...) -} - type Project struct { ID string CreationDate time.Time @@ -277,6 +267,20 @@ func (q *ProjectAndGrantedProjectSearchQueries) toQuery(query sq.SelectBuilder) return query } +func projectPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *ProjectAndGrantedProjectSearchQueries) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + grantedProjectColumnResourceOwner, + domain.PermissionProjectRead, + SingleOrgPermissionOption(queries.Queries), + WithProjectsPermissionOption(GrantedProjectColumnID), + ) + return query.JoinClause(join, args...) +} + func (q *Queries) SearchGrantedProjects(ctx context.Context, queries *ProjectAndGrantedProjectSearchQueries, permissionCheck domain.PermissionCheck) (*GrantedProjects, error) { permissionCheckV2 := PermissionV2(ctx, permissionCheck) projects, err := q.searchGrantedProjects(ctx, queries, permissionCheckV2) @@ -328,11 +332,11 @@ func NewGrantedProjectIDSearchQuery(ids []string) (SearchQuery, error) { } func NewGrantedProjectOrganizationIDSearchQuery(value string) (SearchQuery, error) { - project, err := NewTextQuery(grantedProjectColumnResourceOwner, value, TextEquals) + project, err := NewGrantedProjectResourceOwnerSearchQuery(value) if err != nil { return nil, err } - grant, err := NewTextQuery(grantedProjectColumnGrantedOrganization, value, TextEquals) + grant, err := NewGrantedProjectGrantedOrganizationIDSearchQuery(value) if err != nil { return nil, err } @@ -494,6 +498,9 @@ type GrantedProjects struct { func grantedProjectsCheckPermission(ctx context.Context, grantedProjects *GrantedProjects, permissionCheck domain.PermissionCheck) { grantedProjects.GrantedProjects = slices.DeleteFunc(grantedProjects.GrantedProjects, func(grantedProject *GrantedProject) bool { + if grantedProject.GrantedOrgID != "" { + return projectGrantCheckPermission(ctx, grantedProject.ResourceOwner, grantedProject.ProjectID, grantedProject.GrantID, grantedProject.GrantedOrgID, permissionCheck) != nil + } return projectCheckPermission(ctx, grantedProject.ResourceOwner, grantedProject.ProjectID, permissionCheck) != nil }, ) @@ -513,6 +520,7 @@ type GrantedProject struct { HasProjectCheck bool PrivateLabelingSetting domain.PrivateLabelingSetting + GrantID string GrantedOrgID string OrgName string ProjectGrantState domain.ProjectGrantState @@ -531,6 +539,7 @@ func prepareGrantedProjectsQuery() (sq.SelectBuilder, func(*sql.Rows) (*GrantedP grantedProjectColumnProjectRoleCheck.identifier(), grantedProjectColumnHasProjectCheck.identifier(), grantedProjectColumnPrivateLabelingSetting.identifier(), + grantedProjectColumnGrantID.identifier(), grantedProjectColumnGrantedOrganization.identifier(), grantedProjectColumnGrantedOrganizationName.identifier(), grantedProjectColumnGrantState.identifier(), @@ -541,6 +550,7 @@ func prepareGrantedProjectsQuery() (sq.SelectBuilder, func(*sql.Rows) (*GrantedP projects := make([]*GrantedProject, 0) var ( count uint64 + grantID = sql.NullString{} orgID = sql.NullString{} orgName = sql.NullString{} projectGrantState = sql.NullInt16{} @@ -559,6 +569,7 @@ func prepareGrantedProjectsQuery() (sq.SelectBuilder, func(*sql.Rows) (*GrantedP &grantedProject.ProjectRoleCheck, &grantedProject.HasProjectCheck, &grantedProject.PrivateLabelingSetting, + &grantID, &orgID, &orgName, &projectGrantState, @@ -567,6 +578,9 @@ func prepareGrantedProjectsQuery() (sq.SelectBuilder, func(*sql.Rows) (*GrantedP if err != nil { return nil, err } + if grantID.Valid { + grantedProject.GrantID = grantID.String + } if orgID.Valid { grantedProject.GrantedOrgID = orgID.String } @@ -614,6 +628,7 @@ func prepareProjects() string { ProjectColumnHasProjectCheck.identifier()+" AS "+grantedProjectColumnHasProjectCheck.name, ProjectColumnPrivateLabelingSetting.identifier()+" AS "+grantedProjectColumnPrivateLabelingSetting.name, "NULL::TEXT AS "+grantedProjectColumnGrantResourceOwner.name, + "NULL::TEXT AS "+grantedProjectColumnGrantID.name, "NULL::TEXT AS "+grantedProjectColumnGrantedOrganization.name, "NULL::TEXT AS "+grantedProjectColumnGrantedOrganizationName.name, "NULL::SMALLINT AS "+grantedProjectColumnGrantState.name, @@ -641,6 +656,7 @@ func prepareGrantedProjects() string { ProjectColumnHasProjectCheck.identifier()+" AS "+grantedProjectColumnHasProjectCheck.name, ProjectColumnPrivateLabelingSetting.identifier()+" AS "+grantedProjectColumnPrivateLabelingSetting.name, ProjectGrantColumnResourceOwner.identifier()+" AS "+grantedProjectColumnGrantResourceOwner.name, + ProjectGrantColumnGrantID.identifier()+" AS "+grantedProjectColumnGrantID.name, ProjectGrantColumnGrantedOrgID.identifier()+" AS "+grantedProjectColumnGrantedOrganization.name, ProjectGrantColumnGrantedOrgName.identifier()+" AS "+grantedProjectColumnGrantedOrganizationName.name, ProjectGrantColumnState.identifier()+" AS "+grantedProjectColumnGrantState.name, diff --git a/internal/query/project_grant.go b/internal/query/project_grant.go index a0dbd7c121..3093d26f30 100644 --- a/internal/query/project_grant.go +++ b/internal/query/project_grant.go @@ -108,15 +108,23 @@ type ProjectGrantSearchQueries struct { func projectGrantsCheckPermission(ctx context.Context, projectGrants *ProjectGrants, permissionCheck domain.PermissionCheck) { projectGrants.ProjectGrants = slices.DeleteFunc(projectGrants.ProjectGrants, func(projectGrant *ProjectGrant) bool { - return projectGrantCheckPermission(ctx, projectGrant.ResourceOwner, projectGrant.GrantID, permissionCheck) != nil + return projectGrantCheckPermission(ctx, projectGrant.ResourceOwner, projectGrant.ProjectID, projectGrant.GrantID, projectGrant.GrantedOrgID, permissionCheck) != nil }, ) } -func projectGrantCheckPermission(ctx context.Context, resourceOwner string, grantID string, permissionCheck domain.PermissionCheck) error { - return permissionCheck(ctx, domain.PermissionProjectGrantRead, resourceOwner, grantID) +func projectGrantCheckPermission(ctx context.Context, resourceOwner, projectID, grantID, grantedOrgID string, permissionCheck domain.PermissionCheck) error { + if err := permissionCheck(ctx, domain.PermissionProjectGrantRead, resourceOwner, grantID); err != nil { + if err := permissionCheck(ctx, domain.PermissionProjectGrantRead, grantedOrgID, grantID); err != nil { + if err := permissionCheck(ctx, domain.PermissionProjectGrantRead, resourceOwner, projectID); err != nil { + return err + } + } + } + return nil } +// TODO: add permission check on project grant level func projectGrantPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *ProjectGrantSearchQueries) sq.SelectBuilder { if !enabled { return query @@ -126,7 +134,6 @@ func projectGrantPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, ProjectGrantColumnResourceOwner, domain.PermissionProjectGrantRead, SingleOrgPermissionOption(queries.Queries), - OwnedRowsPermissionOption(ProjectGrantColumnGrantID), ) return query.JoinClause(join, args...) } diff --git a/internal/query/project_role.go b/internal/query/project_role.go index 15ae806cd4..e70fcf277e 100644 --- a/internal/query/project_role.go +++ b/internal/query/project_role.go @@ -103,7 +103,7 @@ func projectRolePermissionCheckV2(ctx context.Context, query sq.SelectBuilder, e ProjectRoleColumnResourceOwner, domain.PermissionProjectRoleRead, SingleOrgPermissionOption(queries.Queries), - OwnedRowsPermissionOption(ProjectRoleColumnKey), + WithProjectsPermissionOption(ProjectRoleColumnProjectID), ) return query.JoinClause(join, args...) } From 7df4f76f3c6d41320bc4639446afa08d9c631032 Mon Sep 17 00:00:00 2001 From: Iraq <66622793+kkrime@users.noreply.github.com> Date: Thu, 5 Jun 2025 11:05:35 +0200 Subject: [PATCH 25/35] feat(api): reworking AddOrganization() API call to return all admins (#9900) --- internal/api/grpc/admin/org.go | 6 +- internal/api/grpc/org/v2/org.go | 15 ++-- internal/api/grpc/org/v2/org_test.go | 4 +- internal/api/grpc/org/v2beta/helper.go | 33 +++++-- .../org/v2beta/integration_test/org_test.go | 90 +++++++++++++------ internal/api/grpc/org/v2beta/org_test.go | 16 ++-- internal/command/org.go | 25 +++++- internal/command/org_test.go | 16 ++-- proto/zitadel/org/v2beta/org_service.proto | 18 +++- 9 files changed, 160 insertions(+), 63 deletions(-) diff --git a/internal/api/grpc/admin/org.go b/internal/api/grpc/admin/org.go index 293e7c74d7..ef97e47bb0 100644 --- a/internal/api/grpc/admin/org.go +++ b/internal/api/grpc/admin/org.go @@ -78,7 +78,7 @@ func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (* if err != nil { return nil, err } - human := setUpOrgHumanToCommand(req.User.(*admin_pb.SetUpOrgRequest_Human_).Human) //TODO: handle machine + human := setUpOrgHumanToCommand(req.User.(*admin_pb.SetUpOrgRequest_Human_).Human) // TODO: handle machine createdOrg, err := s.command.SetUpOrg(ctx, &command.OrgSetup{ Name: req.Org.Name, CustomDomain: req.Org.Domain, @@ -93,8 +93,8 @@ func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (* return nil, err } var userID string - if len(createdOrg.CreatedAdmins) == 1 { - userID = createdOrg.CreatedAdmins[0].ID + if len(createdOrg.OrgAdmins) == 1 { + userID = createdOrg.OrgAdmins[0].GetID() } return &admin_pb.SetUpOrgResponse{ Details: object.DomainToAddDetailsPb(createdOrg.ObjectDetails), diff --git a/internal/api/grpc/org/v2/org.go b/internal/api/grpc/org/v2/org.go index bbc3caca85..b876826365 100644 --- a/internal/api/grpc/org/v2/org.go +++ b/internal/api/grpc/org/v2/org.go @@ -69,12 +69,15 @@ func addOrganizationRequestAdminToCommand(admin *org.AddOrganizationRequest_Admi } func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *org.AddOrganizationResponse, err error) { - admins := make([]*org.AddOrganizationResponse_CreatedAdmin, len(createdOrg.CreatedAdmins)) - for i, admin := range createdOrg.CreatedAdmins { - admins[i] = &org.AddOrganizationResponse_CreatedAdmin{ - UserId: admin.ID, - EmailCode: admin.EmailCode, - PhoneCode: admin.PhoneCode, + admins := make([]*org.AddOrganizationResponse_CreatedAdmin, 0, len(createdOrg.OrgAdmins)) + for _, admin := range createdOrg.OrgAdmins { + admin, ok := admin.(*command.CreatedOrgAdmin) + if ok { + admins = append(admins, &org.AddOrganizationResponse_CreatedAdmin{ + UserId: admin.GetID(), + EmailCode: admin.EmailCode, + PhoneCode: admin.PhoneCode, + }) } } return &org.AddOrganizationResponse{ diff --git a/internal/api/grpc/org/v2/org_test.go b/internal/api/grpc/org/v2/org_test.go index 7ae252a209..37a3dca41a 100644 --- a/internal/api/grpc/org/v2/org_test.go +++ b/internal/api/grpc/org/v2/org_test.go @@ -150,8 +150,8 @@ func Test_createdOrganizationToPb(t *testing.T) { EventDate: now, ResourceOwner: "orgID", }, - CreatedAdmins: []*command.CreatedOrgAdmin{ - { + OrgAdmins: []command.OrgAdmin{ + &command.CreatedOrgAdmin{ ID: "id", EmailCode: gu.Ptr("emailCode"), PhoneCode: gu.Ptr("phoneCode"), diff --git a/internal/api/grpc/org/v2beta/helper.go b/internal/api/grpc/org/v2beta/helper.go index 39bad0dae2..6f47819bb4 100644 --- a/internal/api/grpc/org/v2beta/helper.go +++ b/internal/api/grpc/org/v2beta/helper.go @@ -72,18 +72,33 @@ func OrgStateToPb(state domain.OrgState) v2beta_org.OrgState { } func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *org.CreateOrganizationResponse, err error) { - admins := make([]*org.CreatedAdmin, len(createdOrg.CreatedAdmins)) - for i, admin := range createdOrg.CreatedAdmins { - admins[i] = &org.CreatedAdmin{ - UserId: admin.ID, - EmailCode: admin.EmailCode, - PhoneCode: admin.PhoneCode, + admins := make([]*org.OrganizationAdmin, len(createdOrg.OrgAdmins)) + for i, admin := range createdOrg.OrgAdmins { + switch admin := admin.(type) { + case *command.CreatedOrgAdmin: + admins[i] = &org.OrganizationAdmin{ + OrganizationAdmin: &org.OrganizationAdmin_CreatedAdmin{ + CreatedAdmin: &org.CreatedAdmin{ + UserId: admin.ID, + EmailCode: admin.EmailCode, + PhoneCode: admin.PhoneCode, + }, + }, + } + case *command.AssignedOrgAdmin: + admins[i] = &org.OrganizationAdmin{ + OrganizationAdmin: &org.OrganizationAdmin_AssignedAdmin{ + AssignedAdmin: &org.AssignedAdmin{ + UserId: admin.ID, + }, + }, + } } } return &org.CreateOrganizationResponse{ - CreationDate: timestamppb.New(createdOrg.ObjectDetails.EventDate), - Id: createdOrg.ObjectDetails.ResourceOwner, - CreatedAdmins: admins, + CreationDate: timestamppb.New(createdOrg.ObjectDetails.EventDate), + Id: createdOrg.ObjectDetails.ResourceOwner, + OrganizationAdmins: admins, }, nil } diff --git a/internal/api/grpc/org/v2beta/integration_test/org_test.go b/internal/api/grpc/org/v2beta/integration_test/org_test.go index 4e0ec26121..0d3b920afe 100644 --- a/internal/api/grpc/org/v2beta/integration_test/org_test.go +++ b/internal/api/grpc/org/v2beta/integration_test/org_test.go @@ -18,7 +18,6 @@ import ( "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/admin" v2beta_object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" "github.com/zitadel/zitadel/pkg/grpc/user/v2" user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" @@ -85,6 +84,29 @@ func TestServer_CreateOrganization(t *testing.T) { }, wantErr: true, }, + { + name: "existing user as admin", + ctx: CTX, + req: &v2beta_org.CreateOrganizationRequest{ + Name: gofakeit.AppName(), + Admins: []*v2beta_org.CreateOrganizationRequest_Admin{ + { + UserType: &v2beta_org.CreateOrganizationRequest_Admin_UserId{UserId: User.GetUserId()}, + }, + }, + }, + want: &v2beta_org.CreateOrganizationResponse{ + OrganizationAdmins: []*v2beta_org.OrganizationAdmin{ + { + OrganizationAdmin: &v2beta_org.OrganizationAdmin_AssignedAdmin{ + AssignedAdmin: &v2beta_org.AssignedAdmin{ + UserId: User.GetUserId(), + }, + }, + }, + }, + }, + }, { name: "admin with init", ctx: CTX, @@ -111,11 +133,15 @@ func TestServer_CreateOrganization(t *testing.T) { }, want: &v2beta_org.CreateOrganizationResponse{ Id: integration.NotEmpty, - CreatedAdmins: []*v2beta_org.CreatedAdmin{ + OrganizationAdmins: []*v2beta_org.OrganizationAdmin{ { - UserId: integration.NotEmpty, - EmailCode: gu.Ptr(integration.NotEmpty), - PhoneCode: nil, + OrganizationAdmin: &v2beta_org.OrganizationAdmin_CreatedAdmin{ + CreatedAdmin: &v2beta_org.CreatedAdmin{ + UserId: integration.NotEmpty, + EmailCode: gu.Ptr(integration.NotEmpty), + PhoneCode: nil, + }, + }, }, }, }, @@ -155,10 +181,21 @@ func TestServer_CreateOrganization(t *testing.T) { }, }, want: &v2beta_org.CreateOrganizationResponse{ - CreatedAdmins: []*v2beta_org.CreatedAdmin{ - // a single admin is expected, because the first provided already exists + // OrganizationId: integration.NotEmpty, + OrganizationAdmins: []*v2beta_org.OrganizationAdmin{ { - UserId: integration.NotEmpty, + OrganizationAdmin: &v2beta_org.OrganizationAdmin_AssignedAdmin{ + AssignedAdmin: &v2beta_org.AssignedAdmin{ + UserId: User.GetUserId(), + }, + }, + }, + { + OrganizationAdmin: &v2beta_org.OrganizationAdmin_CreatedAdmin{ + CreatedAdmin: &v2beta_org.CreatedAdmin{ + UserId: integration.NotEmpty, + }, + }, }, }, }, @@ -192,13 +229,16 @@ func TestServer_CreateOrganization(t *testing.T) { now := time.Now() assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) - // organization id must be the same as the resourceOwner - // check the admins - require.Len(t, got.GetCreatedAdmins(), len(tt.want.GetCreatedAdmins())) - for i, admin := range tt.want.GetCreatedAdmins() { - gotAdmin := got.GetCreatedAdmins()[i] - assertCreatedAdmin(t, admin, gotAdmin) + require.Equal(t, len(tt.want.GetOrganizationAdmins()), len(got.GetOrganizationAdmins())) + for i, admin := range tt.want.GetOrganizationAdmins() { + gotAdmin := got.GetOrganizationAdmins()[i].OrganizationAdmin + switch admin := admin.OrganizationAdmin.(type) { + case *v2beta_org.OrganizationAdmin_CreatedAdmin: + assertCreatedAdmin(t, admin.CreatedAdmin, gotAdmin.(*v2beta_org.OrganizationAdmin_CreatedAdmin).CreatedAdmin) + case *v2beta_org.OrganizationAdmin_AssignedAdmin: + assert.Equal(t, admin.AssignedAdmin.GetUserId(), gotAdmin.(*v2beta_org.OrganizationAdmin_AssignedAdmin).AssignedAdmin.GetUserId()) + } } }) } @@ -472,8 +512,8 @@ func TestServer_ListOrganizations(t *testing.T) { ctx: listOrgIAmOwnerCtx, query: []*v2beta_org.OrganizationSearchFilter{ { - Filter: &org.OrganizationSearchFilter_DomainFilter{ - DomainFilter: &org.OrgDomainFilter{ + Filter: &v2beta_org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &v2beta_org.OrgDomainFilter{ Domain: func() string { listOrgRes, err := listOrgClient.ListOrganizations(listOrgIAmOwnerCtx, &v2beta_org.ListOrganizationsRequest{ Filter: []*v2beta_org.OrganizationSearchFilter{ @@ -507,8 +547,8 @@ func TestServer_ListOrganizations(t *testing.T) { ctx: listOrgIAmOwnerCtx, query: []*v2beta_org.OrganizationSearchFilter{ { - Filter: &org.OrganizationSearchFilter_DomainFilter{ - DomainFilter: &org.OrgDomainFilter{ + Filter: &v2beta_org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &v2beta_org.OrgDomainFilter{ Domain: func() string { domain := strings.ToLower(strings.ReplaceAll(orgsName[1][1:len(orgsName[1])-2], " ", "-")) return domain @@ -530,8 +570,8 @@ func TestServer_ListOrganizations(t *testing.T) { ctx: listOrgIAmOwnerCtx, query: []*v2beta_org.OrganizationSearchFilter{ { - Filter: &org.OrganizationSearchFilter_DomainFilter{ - DomainFilter: &org.OrgDomainFilter{ + Filter: &v2beta_org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &v2beta_org.OrgDomainFilter{ Domain: func() string { domain := strings.ToUpper(strings.ReplaceAll(orgsName[1][1:len(orgsName[1])-2], " ", "-")) return domain @@ -1374,7 +1414,7 @@ func TestServer_ValidateOrganizationDomain(t *testing.T) { req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ OrganizationId: orgId, Domain: domain, - Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, }, }, { @@ -1383,7 +1423,7 @@ func TestServer_ValidateOrganizationDomain(t *testing.T) { req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ OrganizationId: "non existent org id", Domain: domain, - Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, }, // BUG: this should be 'organization does not exist' err: errors.New("Domain doesn't exist on organization"), @@ -1394,7 +1434,7 @@ func TestServer_ValidateOrganizationDomain(t *testing.T) { req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ OrganizationId: orgId, Domain: domain, - Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS, + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS, }, }, { @@ -1403,7 +1443,7 @@ func TestServer_ValidateOrganizationDomain(t *testing.T) { req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ OrganizationId: "non existent org id", Domain: domain, - Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS, + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS, }, // BUG: this should be 'organization does not exist' err: errors.New("Domain doesn't exist on organization"), @@ -1414,7 +1454,7 @@ func TestServer_ValidateOrganizationDomain(t *testing.T) { req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ OrganizationId: orgId, Domain: "non existent domain", - Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, }, err: errors.New("Domain doesn't exist on organization"), }, diff --git a/internal/api/grpc/org/v2beta/org_test.go b/internal/api/grpc/org/v2beta/org_test.go index 2047f665a1..346d6b88c1 100644 --- a/internal/api/grpc/org/v2beta/org_test.go +++ b/internal/api/grpc/org/v2beta/org_test.go @@ -150,8 +150,8 @@ func Test_createdOrganizationToPb(t *testing.T) { EventDate: now, ResourceOwner: "orgID", }, - CreatedAdmins: []*command.CreatedOrgAdmin{ - { + OrgAdmins: []command.OrgAdmin{ + &command.CreatedOrgAdmin{ ID: "id", EmailCode: gu.Ptr("emailCode"), PhoneCode: gu.Ptr("phoneCode"), @@ -162,11 +162,15 @@ func Test_createdOrganizationToPb(t *testing.T) { want: &org.CreateOrganizationResponse{ CreationDate: timestamppb.New(now), Id: "orgID", - CreatedAdmins: []*org.CreatedAdmin{ + OrganizationAdmins: []*org.OrganizationAdmin{ { - UserId: "id", - EmailCode: gu.Ptr("emailCode"), - PhoneCode: gu.Ptr("phoneCode"), + OrganizationAdmin: &org.OrganizationAdmin_CreatedAdmin{ + CreatedAdmin: &org.CreatedAdmin{ + UserId: "id", + EmailCode: gu.Ptr("emailCode"), + PhoneCode: gu.Ptr("phoneCode"), + }, + }, }, }, }, diff --git a/internal/command/org.go b/internal/command/org.go index b6650ef7f2..ddf99797e3 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -54,7 +54,11 @@ type orgSetupCommands struct { type CreatedOrg struct { ObjectDetails *domain.ObjectDetails - CreatedAdmins []*CreatedOrgAdmin + OrgAdmins []OrgAdmin +} + +type OrgAdmin interface { + GetID() string } type CreatedOrgAdmin struct { @@ -65,6 +69,18 @@ type CreatedOrgAdmin struct { MachineKey *MachineKey } +func (a *CreatedOrgAdmin) GetID() string { + return a.ID +} + +type AssignedOrgAdmin struct { + ID string +} + +func (a *AssignedOrgAdmin) GetID() string { + return a.ID +} + func (o *OrgSetup) Validate() (err error) { if o.OrgID != "" && strings.TrimSpace(o.OrgID) == "" { return zerrors.ThrowInvalidArgument(nil, "ORG-4ABd3", "Errors.Invalid.Argument") @@ -188,14 +204,15 @@ func (c *orgSetupCommands) push(ctx context.Context) (_ *CreatedOrg, err error) EventDate: events[len(events)-1].CreatedAt(), ResourceOwner: c.aggregate.ID, }, - CreatedAdmins: c.createdAdmins(), + OrgAdmins: c.createdAdmins(), }, nil } -func (c *orgSetupCommands) createdAdmins() []*CreatedOrgAdmin { - users := make([]*CreatedOrgAdmin, 0, len(c.admins)) +func (c *orgSetupCommands) createdAdmins() []OrgAdmin { + users := make([]OrgAdmin, 0, len(c.admins)) for _, admin := range c.admins { if admin.ID != "" && admin.Human == nil { + users = append(users, &AssignedOrgAdmin{ID: admin.ID}) continue } if admin.Human != nil { diff --git a/internal/command/org_test.go b/internal/command/org_test.go index 4b6fd7afe5..4239be760a 100644 --- a/internal/command/org_test.go +++ b/internal/command/org_test.go @@ -1531,8 +1531,8 @@ func TestCommandSide_SetUpOrg(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ ResourceOwner: "orgID", }, - CreatedAdmins: []*CreatedOrgAdmin{ - { + OrgAdmins: []OrgAdmin{ + &CreatedOrgAdmin{ ID: "userID", }, }, @@ -1574,7 +1574,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ ResourceOwner: "custom-org-ID", }, - CreatedAdmins: []*CreatedOrgAdmin{}, + OrgAdmins: []OrgAdmin{}, }, }, }, @@ -1641,7 +1641,11 @@ func TestCommandSide_SetUpOrg(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ ResourceOwner: "orgID", }, - CreatedAdmins: []*CreatedOrgAdmin{}, + OrgAdmins: []OrgAdmin{ + &AssignedOrgAdmin{ + ID: "userID", + }, + }, }, }, }, @@ -1751,8 +1755,8 @@ func TestCommandSide_SetUpOrg(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ ResourceOwner: "orgID", }, - CreatedAdmins: []*CreatedOrgAdmin{ - { + OrgAdmins: []OrgAdmin{ + &CreatedOrgAdmin{ ID: "userID", PAT: &PersonalAccessToken{ ObjectRoot: models.ObjectRoot{ diff --git a/proto/zitadel/org/v2beta/org_service.proto b/proto/zitadel/org/v2beta/org_service.proto index 28c823a89b..387b2cb825 100644 --- a/proto/zitadel/org/v2beta/org_service.proto +++ b/proto/zitadel/org/v2beta/org_service.proto @@ -558,6 +558,18 @@ message CreatedAdmin { optional string phone_code = 3; } +message AssignedAdmin { + string user_id = 1; +} + +message OrganizationAdmin { + // The admins created/assigned for the Organization. + oneof OrganizationAdmin { + CreatedAdmin created_admin = 1; + AssignedAdmin assigned_admin = 2; + } +} + message CreateOrganizationResponse{ // The timestamp of the organization was created. google.protobuf.Timestamp creation_date = 1 [ @@ -577,8 +589,8 @@ message CreateOrganizationResponse{ } ]; - // The admins created for the Organization - repeated CreatedAdmin created_admins = 3; + // The admins created/assigned for the Organization + repeated OrganizationAdmin organization_admins = 3; } message UpdateOrganizationRequest { @@ -939,3 +951,5 @@ message DeleteOrganizationMetadataResponse{ } ]; } + + From 63c92104baec2d326a3f296cd0572a98b56be199 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Thu, 5 Jun 2025 12:13:26 +0200 Subject: [PATCH 26/35] chore: service ping api design (#9984) # Which Problems Are Solved Add the possibility to report information and analytical data from (self-hosted) ZITADEL systems to a central endpoint. To be able to do so an API has to be designed to receive the different reports and information. # How the Problems Are Solved - Telemetry service definition added, which currently has two endpoints: - ReportBaseInformation: To gather the zitadel version and instance information such as id and creation date - ReportResourceCounts: Dynamically report (based on #9979) different resources (orgs, users per org, ...) - To be able to paginate and send multiple pages to the endpoint a `report_id` is returned on the first page / request from the server, which needs to be passed by the client on the following pages. - Base error handling is described in the proto and is based on gRPC standards and best practices. # Additional Changes none # Additional Context Public documentation of the behaviour / error handling and what data is collected, resp. how to configure will be provided in https://github.com/zitadel/zitadel/issues/9869. Closes https://github.com/zitadel/zitadel/issues/9872 --- .../zitadel/analytics/v2beta/telemetry.proto | 48 +++++++++++ .../analytics/v2beta/telemetry_service.proto | 79 +++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 proto/zitadel/analytics/v2beta/telemetry.proto create mode 100644 proto/zitadel/analytics/v2beta/telemetry_service.proto diff --git a/proto/zitadel/analytics/v2beta/telemetry.proto b/proto/zitadel/analytics/v2beta/telemetry.proto new file mode 100644 index 0000000000..f0e1537f9a --- /dev/null +++ b/proto/zitadel/analytics/v2beta/telemetry.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; + +package zitadel.analytics.v2beta; + +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta;analytics"; + + +message InstanceInformation { + // The unique identifier of the instance. + string id = 1; + // The custom domains (incl. generated ones) of the instance. + repeated string domains = 2; + // The creation date of the instance. + google.protobuf.Timestamp created_at = 3; +} + +message ResourceCount { + // The ID of the instance for which the resource counts are reported. + string instance_id = 3; + // The parent type of the resource counts (e.g. organization or instance). + // For example, reporting the amount of users per organization would use + // `COUNT_PARENT_TYPE_ORGANIZATION` as parent type and the organization ID as parent ID. + CountParentType parent_type = 4; + // The parent ID of the resource counts (e.g. organization or instance ID). + // For example, reporting the amount of users per organization would use + // `COUNT_PARENT_TYPE_ORGANIZATION` as parent type and the organization ID as parent ID. + string parent_id = 5; + // The resource counts to report, e.g. amount of `users`, `organizations`, etc. + string resource_name = 6; + // The name of the table in the database, which was used to calculate the counts. + // This can be used to deduplicate counts in case of multiple reports. + // For example, if the counts were calculated from the `users14` table, + // the table name would be `users14`, where there could also be a `users15` table + // reported at the same time as the system is rolling out a new version. + string table_name = 7; + // The timestamp when the count was last updated. + google.protobuf.Timestamp updated_at = 8; + // The actual amount of the resource. + uint32 amount = 9; +} + +enum CountParentType { + COUNT_PARENT_TYPE_UNSPECIFIED = 0; + COUNT_PARENT_TYPE_INSTANCE = 1; + COUNT_PARENT_TYPE_ORGANIZATION = 2; +} diff --git a/proto/zitadel/analytics/v2beta/telemetry_service.proto b/proto/zitadel/analytics/v2beta/telemetry_service.proto new file mode 100644 index 0000000000..e71536a811 --- /dev/null +++ b/proto/zitadel/analytics/v2beta/telemetry_service.proto @@ -0,0 +1,79 @@ +syntax = "proto3"; + +package zitadel.analytics.v2beta; + +import "google/protobuf/timestamp.proto"; +import "zitadel/analytics/v2beta/telemetry.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta;analytics"; + +// The TelemetryService is used to report telemetry such as usage statistics of the ZITADEL instance(s). +// back to a central storage. +// It is used to collect anonymized data about the usage of ZITADEL features, capabilities, and configurations. +// ZITADEL acts as a client of the TelemetryService. +// +// Reports are sent periodically based on the system's runtime configuration. +// The content of the reports, respectively the data collected, can be configured in the system's runtime configuration. +// +// All endpoints follow the same error and retry handling: +// In case of a failure to report the usage, ZITADEL will retry to report the usage +// based on the configured retry policy and error type: +// - Client side errors will not be retried, as they indicate a misconfiguration or an invalid request: +// - `INVALID_ARGUMENT`: The request was malformed. +// - `NOT_FOUND`: The TelemetryService's endpoint is likely misconfigured. +// - Connection / transfer errors will be retried based on the retry policy configured in the system's runtime configuration: +// - `DEADLINE_EXCEEDED`: The request took too long to complete, it will be retried. +// - `RESOURCE_EXHAUSTED`: The request was rejected due to resource exhaustion, it will be retried after a backoff period. +// - `UNAVAILABLE`: The TelemetryService is currently unavailable, it will be retried after a backoff period. +// Server side errors will also be retried based on the information provided by the server: +// - `FAILED_PRECONDITION`: The request failed due to a precondition, e.g. the report ID does not exists, +// does not correspond to the same system ID or previous reporting is too old, do not retry. +// - `INTERNAL`: An internal error occurred. Check details and logs. +service TelemetryService { + + // ReportBaseInformation is used to report the base information of the ZITADEL system, + // including the version, instances, their creation date and domains. + // The response contains a report ID to link it to the resource counts or other reports. + // The report ID is only valid for the same system ID. + rpc ReportBaseInformation (ReportBaseInformationRequest) returns (ReportBaseInformationResponse) {} + + // ReportResourceCounts is used to report the resource counts such as amount of organizations + // or users per organization and much more. + // Since the resource counts can be reported in multiple batches, + // the response contains a report ID to continue reporting. + // The report ID is only valid for the same system ID. + rpc ReportResourceCounts (ReportResourceCountsRequest) returns (ReportResourceCountsResponse) {} +} + +message ReportBaseInformationRequest { + // The system ID is a unique identifier for the ZITADEL system. + string system_id = 1; + // The current version of the ZITADEL system. + string version = 2; + // A list of instances in the ZITADEL system and their information. + repeated InstanceInformation instances = 3; +} + +message ReportBaseInformationResponse { + // The report ID is a unique identifier for the report. + // It is used to identify the report to be able to link it to the resource counts or other reports. + // Note that the report ID is only valid for the same system ID. + string report_id = 1; +} + +message ReportResourceCountsRequest { + // The system ID is a unique identifier for the ZITADEL system. + string system_id = 1; + // The previously returned report ID from the server to continue reporting. + // Note that the report ID is only valid for the same system ID. + optional string report_id = 2; + // A list of resource counts to report. + repeated ResourceCount resource_counts = 3; +} + +message ReportResourceCountsResponse { + // The report ID is a unique identifier for the report. + // It is used to identify the report in case of additional data / pagination. + // Note that the report ID is only valid for the same system ID. + string report_id = 1; +} \ No newline at end of file From 6c309d65c6d6367959d012a9b97a46a26cc8be6a Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Thu, 5 Jun 2025 13:42:59 +0200 Subject: [PATCH 27/35] fix(fields): project by id and resource owner (#10034) # Which Problems Are Solved If the `IMPROVED_PERFORMANCE_PROJECT` feature flag was enabled it was not possible to remove organizations anymore because the project was searched in the `eventstore.fields` table without resource owner. # How the Problems Are Solved Search now includes resource owner. --- internal/command/project.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/internal/command/project.go b/internal/command/project.go index bf72306417..40aa79f186 100644 --- a/internal/command/project.go +++ b/internal/command/project.go @@ -138,14 +138,14 @@ func projectWriteModel(ctx context.Context, filter preparation.FilterToQueryRedu return project, nil } -func (c *Commands) projectAggregateByID(ctx context.Context, projectID string) (*eventstore.Aggregate, domain.ProjectState, error) { - result, err := c.projectState(ctx, projectID) +func (c *Commands) projectAggregateByID(ctx context.Context, projectID, resourceOwner string) (*eventstore.Aggregate, domain.ProjectState, error) { + result, err := c.projectState(ctx, projectID, resourceOwner) if err != nil { return nil, domain.ProjectStateUnspecified, zerrors.ThrowNotFound(err, "COMMA-NDQoF", "Errors.Project.NotFound") } if len(result) == 0 { _ = projection.ProjectGrantFields.Trigger(ctx) - result, err = c.projectState(ctx, projectID) + result, err = c.projectState(ctx, projectID, resourceOwner) if err != nil || len(result) == 0 { return nil, domain.ProjectStateUnspecified, zerrors.ThrowNotFound(err, "COMMA-U1nza", "Errors.Project.NotFound") } @@ -159,7 +159,7 @@ func (c *Commands) projectAggregateByID(ctx context.Context, projectID string) ( return &result[0].Aggregate, state, nil } -func (c *Commands) projectState(ctx context.Context, projectID string) ([]*eventstore.SearchResult, error) { +func (c *Commands) projectState(ctx context.Context, projectID, resourceOwner string) ([]*eventstore.SearchResult, error) { return c.eventstore.Search( ctx, map[eventstore.FieldType]any{ @@ -167,6 +167,7 @@ func (c *Commands) projectState(ctx context.Context, projectID string) ([]*event eventstore.FieldTypeObjectID: projectID, eventstore.FieldTypeObjectRevision: project.ProjectObjectRevision, eventstore.FieldTypeFieldName: project.ProjectStateSearchField, + eventstore.FieldTypeResourceOwner: resourceOwner, }, ) } @@ -179,7 +180,7 @@ func (c *Commands) checkProjectExists(ctx context.Context, projectID, resourceOw return c.checkProjectExistsOld(ctx, projectID, resourceOwner) } - agg, state, err := c.projectAggregateByID(ctx, projectID) + agg, state, err := c.projectAggregateByID(ctx, projectID, resourceOwner) if err != nil || !state.Valid() { return "", zerrors.ThrowPreconditionFailed(err, "COMMA-VCnwD", "Errors.Project.NotFound") } @@ -249,7 +250,7 @@ func (c *Commands) DeactivateProject(ctx context.Context, projectID string, reso return c.deactivateProjectOld(ctx, projectID, resourceOwner) } - projectAgg, state, err := c.projectAggregateByID(ctx, projectID) + projectAgg, state, err := c.projectAggregateByID(ctx, projectID, resourceOwner) if err != nil { return nil, err } @@ -285,7 +286,7 @@ func (c *Commands) ReactivateProject(ctx context.Context, projectID string, reso return c.reactivateProjectOld(ctx, projectID, resourceOwner) } - projectAgg, state, err := c.projectAggregateByID(ctx, projectID) + projectAgg, state, err := c.projectAggregateByID(ctx, projectID, resourceOwner) if err != nil { return nil, err } From 647b3b57cffe4c34d3ae9d0d66e635a967f7eea9 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Thu, 5 Jun 2025 15:50:21 +0200 Subject: [PATCH 28/35] fix: correct id filter for project service (#10035) # Which Problems Are Solved IDs filter definition was changed in another PR and not changed in the Project service. # How the Problems Are Solved Correctly use the IDs filter. # Additional Changes Add timeout to the integration tests. # Additional Context None --- .../grpc/project/v2beta/integration/query_test.go | 12 ++++-------- internal/integration/client.go | 4 +++- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/internal/api/grpc/project/v2beta/integration/query_test.go b/internal/api/grpc/project/v2beta/integration/query_test.go index fc153f0130..b648e8c1d7 100644 --- a/internal/api/grpc/project/v2beta/integration/query_test.go +++ b/internal/api/grpc/project/v2beta/integration/query_test.go @@ -389,9 +389,7 @@ func TestServer_ListProjects(t *testing.T) { resp2 := createProject(iamOwnerCtx, instance, t, orgID, true, false) resp3 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, true) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId(), projectResp.GetId()}, - }, + InProjectIdsFilter: &filter.InIDsFilter{Ids: []string{resp1.GetId(), resp2.GetId(), resp3.GetId(), projectResp.GetId()}}, } response.Projects[0] = grantedProjectResp response.Projects[1] = projectResp @@ -516,7 +514,7 @@ func TestServer_ListProjects(t *testing.T) { projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), projectName, true, true) projectGrantResp := instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgID) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ProjectIds: []string{projectResp.GetId()}}, + InProjectIdsFilter: &filter.InIDsFilter{Ids: []string{projectResp.GetId()}}, } response.Projects[0] = &project.Project{ Id: projectResp.GetId(), @@ -892,7 +890,7 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { // projectGrantResp := instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgID) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ProjectIds: []string{projectResp.GetId()}}, + InProjectIdsFilter: &filter.InIDsFilter{Ids: []string{projectResp.GetId()}}, } /* response.Projects[0] = &project.Project{ @@ -1227,9 +1225,7 @@ func TestServer_ListProjectGrants(t *testing.T) { project2Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false) project3Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId(), projectResp.GetId()}, - }, + InProjectIdsFilter: &filter.InIDsFilter{Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId(), projectResp.GetId()}}, } createProjectGrant(iamOwnerCtx, instance, t, orgID, project1Resp.GetId(), name1) diff --git a/internal/integration/client.go b/internal/integration/client.go index 838a728faf..20c98b5628 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -16,6 +16,7 @@ import ( "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/integration/scim" @@ -271,7 +272,8 @@ func (i *Instance) CreateUserTypeMachine(ctx context.Context) *user_v2.CreateUse func (i *Instance) CreatePersonalAccessToken(ctx context.Context, userID string) *user_v2.AddPersonalAccessTokenResponse { resp, err := i.Client.UserV2.AddPersonalAccessToken(ctx, &user_v2.AddPersonalAccessTokenRequest{ - UserId: userID, + UserId: userID, + ExpirationDate: timestamppb.New(time.Now().Add(30 * time.Minute)), }) logging.OnError(err).Panic("create pat") return resp From 4df138286b673255319b20b685f1cba31c05b47d Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Fri, 6 Jun 2025 10:48:29 +0200 Subject: [PATCH 29/35] perf(query): reduce user query duration (#10037) # Which Problems Are Solved The resource usage to query user(s) on the database was high and therefore could have performance impact. # How the Problems Are Solved Database queries involving the users and loginnames table were improved and an index was added for user by email query. # Additional Changes - spellchecks - updated apis on load tests # additional info needs cherry pick to v3 --- cmd/setup/58.go | 49 +++ cmd/setup/58/01_update_login_names3_view.sql | 36 ++ cmd/setup/58/02_create_index.sql | 1 + cmd/setup/config.go | 1 + cmd/setup/setup.go | 2 + internal/query/oidc_settings.go | 2 +- internal/query/org_metadata.go | 4 +- internal/query/project.go | 2 +- internal/query/project_grant.go | 4 +- internal/query/projection/login_name.go | 111 +----- .../query/projection/login_name_query.sql | 35 ++ internal/query/projection/user.go | 1 + internal/query/restrictions.go | 2 +- internal/query/search_query.go | 13 +- internal/query/search_query_test.go | 22 +- internal/query/secret_generators.go | 2 +- internal/query/security_policy.go | 2 +- internal/query/user.go | 158 ++------ internal/query/user_by_id.sql | 52 +-- internal/query/user_metadata.go | 6 +- internal/query/user_notify_by_id.sql | 52 +-- internal/query/user_personal_access_token.go | 2 +- internal/query/user_test.go | 345 +----------------- load-test/src/org.ts | 2 +- load-test/src/use_cases/manipulate_user.ts | 2 +- load-test/src/user.ts | 6 +- 26 files changed, 225 insertions(+), 689 deletions(-) create mode 100644 cmd/setup/58.go create mode 100644 cmd/setup/58/01_update_login_names3_view.sql create mode 100644 cmd/setup/58/02_create_index.sql create mode 100644 internal/query/projection/login_name_query.sql diff --git a/cmd/setup/58.go b/cmd/setup/58.go new file mode 100644 index 0000000000..c46b30f548 --- /dev/null +++ b/cmd/setup/58.go @@ -0,0 +1,49 @@ +package setup + +import ( + "context" + "database/sql" + "embed" + "fmt" + + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" +) + +var ( + //go:embed 58/*.sql + replaceLoginNames3View embed.FS +) + +type ReplaceLoginNames3View struct { + dbClient *database.DB +} + +func (mig *ReplaceLoginNames3View) Execute(ctx context.Context, _ eventstore.Event) error { + var exists bool + err := mig.dbClient.QueryRowContext(ctx, func(r *sql.Row) error { + return r.Scan(&exists) + }, "SELECT exists(SELECT 1 from information_schema.views WHERE table_schema = 'projections' AND table_name = 'login_names3')") + + if err != nil || !exists { + return err + } + + statements, err := readStatements(replaceLoginNames3View, "58") + if err != nil { + return err + } + for _, stmt := range statements { + logging.WithFields("file", stmt.file, "migration", mig.String()).Info("execute statement") + if _, err := mig.dbClient.ExecContext(ctx, stmt.query); err != nil { + return fmt.Errorf("%s %s: %w", mig.String(), stmt.file, err) + } + } + return nil +} + +func (mig *ReplaceLoginNames3View) String() string { + return "58_replace_login_names3_view" +} diff --git a/cmd/setup/58/01_update_login_names3_view.sql b/cmd/setup/58/01_update_login_names3_view.sql new file mode 100644 index 0000000000..4499296152 --- /dev/null +++ b/cmd/setup/58/01_update_login_names3_view.sql @@ -0,0 +1,36 @@ +CREATE OR REPLACE VIEW projections.login_names3 AS + SELECT + u.id AS user_id + , CASE + WHEN p.must_be_domain THEN CONCAT(u.user_name, '@', d.name) + ELSE u.user_name + END AS login_name + , COALESCE(d.is_primary, TRUE) AS is_primary + , u.instance_id + FROM + projections.login_names3_users AS u + LEFT JOIN LATERAL ( + SELECT + must_be_domain + , is_default + FROM + projections.login_names3_policies AS p + WHERE + ( + p.instance_id = u.instance_id + AND NOT p.is_default + AND p.resource_owner = u.resource_owner + ) OR ( + p.instance_id = u.instance_id + AND p.is_default + ) + ORDER BY + p.is_default -- custom first + LIMIT 1 + ) AS p ON TRUE + LEFT JOIN + projections.login_names3_domains d + ON + p.must_be_domain + AND u.resource_owner = d.resource_owner + AND u.instance_id = d.instance_id diff --git a/cmd/setup/58/02_create_index.sql b/cmd/setup/58/02_create_index.sql new file mode 100644 index 0000000000..ed3627b427 --- /dev/null +++ b/cmd/setup/58/02_create_index.sql @@ -0,0 +1 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS login_names3_policies_is_default_owner_idx ON projections.login_names3_policies (instance_id, is_default, resource_owner) INCLUDE (must_be_domain) diff --git a/cmd/setup/config.go b/cmd/setup/config.go index dd59ba3f07..0c3f726902 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -154,6 +154,7 @@ type Steps struct { s55ExecutionHandlerStart *ExecutionHandlerStart s56IDPTemplate6SAMLFederatedLogout *IDPTemplate6SAMLFederatedLogout s57CreateResourceCounts *CreateResourceCounts + s58ReplaceLoginNames3View *ReplaceLoginNames3View } func MustNewSteps(v *viper.Viper) *Steps { diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index 1465180a6b..8ee8d7fc68 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -216,6 +216,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s55ExecutionHandlerStart = &ExecutionHandlerStart{dbClient: dbClient} steps.s56IDPTemplate6SAMLFederatedLogout = &IDPTemplate6SAMLFederatedLogout{dbClient: dbClient} steps.s57CreateResourceCounts = &CreateResourceCounts{dbClient: dbClient} + steps.s58ReplaceLoginNames3View = &ReplaceLoginNames3View{dbClient: dbClient} err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil) logging.OnError(err).Fatal("unable to start projections") @@ -262,6 +263,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s55ExecutionHandlerStart, steps.s56IDPTemplate6SAMLFederatedLogout, steps.s57CreateResourceCounts, + steps.s58ReplaceLoginNames3View, } { setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed") if setupErr != nil { diff --git a/internal/query/oidc_settings.go b/internal/query/oidc_settings.go index bdd21cfd15..4ecd6cdad2 100644 --- a/internal/query/oidc_settings.go +++ b/internal/query/oidc_settings.go @@ -84,7 +84,7 @@ func (q *Queries) OIDCSettingsByAggID(ctx context.Context, aggregateID string) ( OIDCSettingsColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), }).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-s9nle", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-s9nle", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { diff --git a/internal/query/org_metadata.go b/internal/query/org_metadata.go index fe61ad51d9..e67c7222cd 100644 --- a/internal/query/org_metadata.go +++ b/internal/query/org_metadata.go @@ -103,7 +103,7 @@ func (q *Queries) GetOrgMetadataByKey(ctx context.Context, shouldTriggerBulk boo } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-aDaG2", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-aDaG2", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -133,7 +133,7 @@ func (q *Queries) SearchOrgMetadata(ctx context.Context, shouldTriggerBulk bool, query, scan := prepareOrgMetadataListQuery() stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Egbld", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Egbld", "Errors.Query.SQLStatement") } err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { diff --git a/internal/query/project.go b/internal/query/project.go index 728731f7cb..59e2dd95c0 100644 --- a/internal/query/project.go +++ b/internal/query/project.go @@ -211,7 +211,7 @@ func (q *Queries) ProjectByID(ctx context.Context, shouldTriggerBulk bool, id st } query, args, err := stmt.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-2m00Q", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-2m00Q", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { diff --git a/internal/query/project_grant.go b/internal/query/project_grant.go index 3093d26f30..1931cad0f5 100644 --- a/internal/query/project_grant.go +++ b/internal/query/project_grant.go @@ -167,7 +167,7 @@ func (q *Queries) ProjectGrantByID(ctx context.Context, shouldTriggerBulk bool, } query, args, err := stmt.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Nf93d", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Nf93d", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -189,7 +189,7 @@ func (q *Queries) ProjectGrantByIDAndGrantedOrg(ctx context.Context, id, granted } query, args, err := stmt.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-MO9fs", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-MO9fs", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { diff --git a/internal/query/projection/login_name.go b/internal/query/projection/login_name.go index 3c31928af4..e60f725dc7 100644 --- a/internal/query/projection/login_name.go +++ b/internal/query/projection/login_name.go @@ -2,9 +2,7 @@ package projection import ( "context" - "strings" - - sq "github.com/Masterminds/squirrel" + _ "embed" "github.com/zitadel/zitadel/internal/eventstore" old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" @@ -58,105 +56,8 @@ const ( LoginNamePoliciesInstanceIDCol = "instance_id" ) -var ( - policyUsers = sq.Select( - alias( - col(usersAlias, LoginNameUserIDCol), - LoginNameUserCol, - ), - col(usersAlias, LoginNameUserUserNameCol), - col(usersAlias, LoginNameUserInstanceIDCol), - col(usersAlias, LoginNameUserResourceOwnerCol), - alias( - coalesce(col(policyCustomAlias, LoginNamePoliciesMustBeDomainCol), col(policyDefaultAlias, LoginNamePoliciesMustBeDomainCol)), - LoginNamePoliciesMustBeDomainCol, - ), - ).From(alias(LoginNameUserProjectionTable, usersAlias)). - LeftJoin( - leftJoin(LoginNamePolicyProjectionTable, policyCustomAlias, - eq(col(policyCustomAlias, LoginNamePoliciesResourceOwnerCol), col(usersAlias, LoginNameUserResourceOwnerCol)), - eq(col(policyCustomAlias, LoginNamePoliciesInstanceIDCol), col(usersAlias, LoginNameUserInstanceIDCol)), - ), - ). - LeftJoin( - leftJoin(LoginNamePolicyProjectionTable, policyDefaultAlias, - eq(col(policyDefaultAlias, LoginNamePoliciesIsDefaultCol), "true"), - eq(col(policyDefaultAlias, LoginNamePoliciesInstanceIDCol), col(usersAlias, LoginNameUserInstanceIDCol)), - ), - ) - - loginNamesTable = sq.Select( - col(policyUsersAlias, LoginNameUserCol), - col(policyUsersAlias, LoginNameUserUserNameCol), - col(policyUsersAlias, LoginNameUserResourceOwnerCol), - alias(col(policyUsersAlias, LoginNameUserInstanceIDCol), - LoginNameInstanceIDCol), - col(policyUsersAlias, LoginNamePoliciesMustBeDomainCol), - alias(col(domainsAlias, LoginNameDomainNameCol), - domainAlias), - col(domainsAlias, LoginNameDomainIsPrimaryCol), - ).FromSelect(policyUsers, policyUsersAlias). - LeftJoin( - leftJoin(LoginNameDomainProjectionTable, domainsAlias, - col(policyUsersAlias, LoginNamePoliciesMustBeDomainCol), - eq(col(policyUsersAlias, LoginNameUserResourceOwnerCol), col(domainsAlias, LoginNameDomainResourceOwnerCol)), - eq(col(policyUsersAlias, LoginNamePoliciesInstanceIDCol), col(domainsAlias, LoginNameDomainInstanceIDCol)), - ), - ) - - viewStmt, _ = sq.Select( - LoginNameUserCol, - alias( - whenThenElse( - LoginNamePoliciesMustBeDomainCol, - concat(LoginNameUserUserNameCol, "'@'", domainAlias), - LoginNameUserUserNameCol), - LoginNameCol), - alias(coalesce(LoginNameDomainIsPrimaryCol, "true"), - LoginNameIsPrimaryCol), - LoginNameInstanceIDCol, - ).FromSelect(loginNamesTable, LoginNameTableAlias).MustSql() -) - -func col(table, name string) string { - return table + "." + name -} - -func alias(col, alias string) string { - return col + " AS " + alias -} - -func coalesce(values ...string) string { - str := "COALESCE(" - for i, value := range values { - if i > 0 { - str += ", " - } - str += value - } - str += ")" - return str -} - -func eq(first, second string) string { - return first + " = " + second -} - -func leftJoin(table, alias, on string, and ...string) string { - st := table + " " + alias + " ON " + on - for _, a := range and { - st += " AND " + a - } - return st -} - -func concat(strs ...string) string { - return "CONCAT(" + strings.Join(strs, ", ") + ")" -} - -func whenThenElse(when, then, el string) string { - return "(CASE WHEN " + when + " THEN " + then + " ELSE " + el + " END)" -} +//go:embed login_name_query.sql +var loginNameViewStmt string type loginNameProjection struct{} @@ -170,7 +71,7 @@ func (*loginNameProjection) Name() string { func (*loginNameProjection) Init() *old_handler.Check { return handler.NewViewCheck( - viewStmt, + loginNameViewStmt, handler.NewSuffixedTable( []*handler.InitColumn{ handler.NewColumn(LoginNameUserIDCol, handler.ColumnTypeText), @@ -229,7 +130,9 @@ func (*loginNameProjection) Init() *old_handler.Check { }, handler.NewPrimaryKey(LoginNamePoliciesInstanceIDCol, LoginNamePoliciesResourceOwnerCol), loginNamePolicySuffix, - handler.WithIndex(handler.NewIndex("is_default", []string{LoginNamePoliciesResourceOwnerCol, LoginNamePoliciesIsDefaultCol})), + // this index is not used anymore, but kept for understanding why the default exists on existing systems, TODO: remove in login_names4 + // handler.WithIndex(handler.NewIndex("is_default", []string{LoginNamePoliciesResourceOwnerCol, LoginNamePoliciesIsDefaultCol})), + handler.WithIndex(handler.NewIndex("is_default_owner", []string{LoginNamePoliciesInstanceIDCol, LoginNamePoliciesIsDefaultCol, LoginNamePoliciesResourceOwnerCol}, handler.WithInclude(LoginNamePoliciesMustBeDomainCol))), ), ) } diff --git a/internal/query/projection/login_name_query.sql b/internal/query/projection/login_name_query.sql new file mode 100644 index 0000000000..89dc803feb --- /dev/null +++ b/internal/query/projection/login_name_query.sql @@ -0,0 +1,35 @@ +SELECT + u.id AS user_id + , CASE + WHEN p.must_be_domain THEN CONCAT(u.user_name, '@', d.name) + ELSE u.user_name + END AS login_name + , COALESCE(d.is_primary, TRUE) AS is_primary + , u.instance_id +FROM + projections.login_names3_users AS u +LEFT JOIN LATERAL ( + SELECT + must_be_domain + , is_default + FROM + projections.login_names3_policies AS p + WHERE + ( + p.instance_id = u.instance_id + AND NOT p.is_default + AND p.resource_owner = u.resource_owner + ) OR ( + p.instance_id = u.instance_id + AND p.is_default + ) + ORDER BY + p.is_default -- custom first + LIMIT 1 +) AS p ON TRUE +LEFT JOIN + projections.login_names3_domains d + ON + p.must_be_domain + AND u.resource_owner = d.resource_owner + AND u.instance_id = d.instance_id \ No newline at end of file diff --git a/internal/query/projection/user.go b/internal/query/projection/user.go index f1e0613287..d11c4855f7 100644 --- a/internal/query/projection/user.go +++ b/internal/query/projection/user.go @@ -124,6 +124,7 @@ func (*userProjection) Init() *old_handler.Check { handler.NewPrimaryKey(HumanUserInstanceIDCol, HumanUserIDCol), UserHumanSuffix, handler.WithForeignKey(handler.NewForeignKeyOfPublicKeys()), + handler.WithIndex(handler.NewIndex("email", []string{HumanUserInstanceIDCol, "LOWER(" + HumanEmailCol + ")"})), ), handler.NewSuffixedTable([]*handler.InitColumn{ handler.NewColumn(MachineUserIDCol, handler.ColumnTypeText), diff --git a/internal/query/restrictions.go b/internal/query/restrictions.go index 8cff5737f7..93d435278c 100644 --- a/internal/query/restrictions.go +++ b/internal/query/restrictions.go @@ -78,7 +78,7 @@ func (q *Queries) GetInstanceRestrictions(ctx context.Context) (restrictions Res RestrictionsColumnResourceOwner.identifier(): instanceID, }).ToSql() if err != nil { - return restrictions, zitade_errors.ThrowInternal(err, "QUERY-XnLMQ", "Errors.Query.SQLStatment") + return restrictions, zitade_errors.ThrowInternal(err, "QUERY-XnLMQ", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { restrictions, err = scan(row) diff --git a/internal/query/search_query.go b/internal/query/search_query.go index d5e09027c4..d6dd710d1e 100644 --- a/internal/query/search_query.go +++ b/internal/query/search_query.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "reflect" + "strings" "time" sq "github.com/Masterminds/squirrel" @@ -334,23 +335,23 @@ func (q *textQuery) comp() sq.Sqlizer { case TextNotEquals: return sq.NotEq{q.Column.identifier(): q.Text} case TextEqualsIgnoreCase: - return sq.ILike{q.Column.identifier(): q.Text} + return sq.Like{"LOWER(" + q.Column.identifier() + ")": strings.ToLower(q.Text)} case TextNotEqualsIgnoreCase: - return sq.NotILike{q.Column.identifier(): q.Text} + return sq.NotLike{"LOWER(" + q.Column.identifier() + ")": strings.ToLower(q.Text)} case TextStartsWith: return sq.Like{q.Column.identifier(): q.Text + "%"} case TextStartsWithIgnoreCase: - return sq.ILike{q.Column.identifier(): q.Text + "%"} + return sq.Like{"LOWER(" + q.Column.identifier() + ")": strings.ToLower(q.Text) + "%"} case TextEndsWith: return sq.Like{q.Column.identifier(): "%" + q.Text} case TextEndsWithIgnoreCase: - return sq.ILike{q.Column.identifier(): "%" + q.Text} + return sq.Like{"LOWER(" + q.Column.identifier() + ")": "%" + strings.ToLower(q.Text)} case TextContains: return sq.Like{q.Column.identifier(): "%" + q.Text + "%"} case TextContainsIgnoreCase: - return sq.ILike{q.Column.identifier(): "%" + q.Text + "%"} + return sq.Like{"LOWER(" + q.Column.identifier() + ")": "%" + strings.ToLower(q.Text) + "%"} case TextListContains: - return &listContains{col: q.Column, args: []interface{}{q.Text}} + return &listContains{col: q.Column, args: []any{q.Text}} case textCompareMax: return nil } diff --git a/internal/query/search_query_test.go b/internal/query/search_query_test.go index 13142a0158..7f6672b279 100644 --- a/internal/query/search_query_test.go +++ b/internal/query/search_query_test.go @@ -1204,7 +1204,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextEqualsIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "Hurst"}, + query: sq.Like{"LOWER(test_table.test_col)": "hurst"}, }, }, { @@ -1226,7 +1226,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextNotEqualsIgnoreCase, }, want: want{ - query: sq.NotILike{"test_table.test_col": "Hurst"}, + query: sq.NotLike{"LOWER(test_table.test_col)": "hurst"}, }, }, { @@ -1237,7 +1237,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextEqualsIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "Hu\\%\\%rst"}, + query: sq.Like{"LOWER(test_table.test_col)": "hu\\%\\%rst"}, }, }, { @@ -1270,7 +1270,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextStartsWithIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "Hurst%"}, + query: sq.Like{"LOWER(test_table.test_col)": "hurst%"}, }, }, { @@ -1281,7 +1281,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextStartsWithIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "Hurst\\%%"}, + query: sq.Like{"LOWER(test_table.test_col)": "hurst\\%%"}, }, }, { @@ -1314,7 +1314,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextEndsWithIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "%Hurst"}, + query: sq.Like{"LOWER(test_table.test_col)": "%hurst"}, }, }, { @@ -1325,7 +1325,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextEndsWithIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "%\\%Hurst"}, + query: sq.Like{"LOWER(test_table.test_col)": "%\\%hurst"}, }, }, { @@ -1351,14 +1351,14 @@ func TestTextQuery_comp(t *testing.T) { }, }, { - name: "containts ignore case", + name: "contains ignore case", fields: fields{ Column: testCol, Text: "Hurst", Compare: TextContainsIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "%Hurst%"}, + query: sq.Like{"LOWER(test_table.test_col)": "%hurst%"}, }, }, { @@ -1369,11 +1369,11 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextContainsIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "%\\%Hurst\\%%"}, + query: sq.Like{"LOWER(test_table.test_col)": "%\\%hurst\\%%"}, }, }, { - name: "list containts", + name: "list contains", fields: fields{ Column: testCol, Text: "Hurst", diff --git a/internal/query/secret_generators.go b/internal/query/secret_generators.go index c267d7b290..ca77bc35b5 100644 --- a/internal/query/secret_generators.go +++ b/internal/query/secret_generators.go @@ -132,7 +132,7 @@ func (q *Queries) SecretGeneratorByType(ctx context.Context, generatorType domai SecretGeneratorColumnInstanceID.identifier(): instanceID, }).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-3k99f", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-3k99f", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { diff --git a/internal/query/security_policy.go b/internal/query/security_policy.go index 7a3fb3fa89..5a2450258e 100644 --- a/internal/query/security_policy.go +++ b/internal/query/security_policy.go @@ -67,7 +67,7 @@ func (q *Queries) SecurityPolicy(ctx context.Context) (policy *SecurityPolicy, e SecurityPolicyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), }).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Sf6d1", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Sf6d1", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { diff --git a/internal/query/user.go b/internal/query/user.go index 6844982f07..ac3eb79fc9 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -200,21 +200,15 @@ var ( userLoginNamesTable = loginNameTable.setAlias("login_names") userLoginNamesUserIDCol = LoginNameUserIDCol.setTable(userLoginNamesTable) - userLoginNamesNameCol = LoginNameNameCol.setTable(userLoginNamesTable) userLoginNamesInstanceIDCol = LoginNameInstanceIDCol.setTable(userLoginNamesTable) userLoginNamesListCol = Column{ - name: "loginnames", + name: "login_names", table: userLoginNamesTable, } - userLoginNamesLowerListCol = Column{ - name: "loginnames_lower", + userPreferredLoginNameCol = Column{ + name: "preferred_login_name", table: userLoginNamesTable, } - userPreferredLoginNameTable = loginNameTable.setAlias("preferred_login_name") - userPreferredLoginNameUserIDCol = LoginNameUserIDCol.setTable(userPreferredLoginNameTable) - userPreferredLoginNameCol = LoginNameNameCol.setTable(userPreferredLoginNameTable) - userPreferredLoginNameIsPrimaryCol = LoginNameIsPrimaryCol.setTable(userPreferredLoginNameTable) - userPreferredLoginNameInstanceIDCol = LoginNameInstanceIDCol.setTable(userPreferredLoginNameTable) ) var ( @@ -459,7 +453,7 @@ func (q *Queries) GetHumanProfile(ctx context.Context, userID string, queries .. } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -483,7 +477,7 @@ func (q *Queries) GetHumanEmail(ctx context.Context, userID string, queries ...S } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-BHhj3", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-BHhj3", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -507,7 +501,7 @@ func (q *Queries) GetHumanPhone(ctx context.Context, userID string, queries ...S } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Dg43g", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Dg43g", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -593,7 +587,7 @@ func (q *Queries) GetNotifyUser(ctx context.Context, shouldTriggered bool, queri } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Err3g", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Err3g", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -611,7 +605,7 @@ func (q *Queries) CountUsers(ctx context.Context, queries *UserSearchQueries) (c eq := sq.Eq{UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { - return 0, zerrors.ThrowInternal(err, "QUERY-w3Dx", "Errors.Query.SQLStatment") + return 0, zerrors.ThrowInternal(err, "QUERY-w3Dx", "Errors.Query.SQLStatement") } err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { @@ -646,7 +640,7 @@ func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries, p UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), }).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatement") } err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { @@ -693,7 +687,7 @@ func (q *Queries) IsUserUnique(ctx context.Context, username, email, resourceOwn eq := sq.Eq{UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := query.Where(eq).ToSql() if err != nil { - return false, zerrors.ThrowInternal(err, "QUERY-Dg43g", "Errors.Query.SQLStatment") + return false, zerrors.ThrowInternal(err, "QUERY-Dg43g", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -792,12 +786,8 @@ func NewUserPreferredLoginNameSearchQuery(value string, comparison TextCompariso return NewTextQuery(userPreferredLoginNameCol, value, comparison) } -func NewUserLoginNamesSearchQuery(value string) (SearchQuery, error) { - return NewTextQuery(userLoginNamesLowerListCol, strings.ToLower(value), TextListContains) -} - func NewUserLoginNameExistsQuery(value string, comparison TextComparison) (SearchQuery, error) { - // linking queries for the subselect + // linking queries for the sub select instanceQuery, err := NewColumnComparisonQuery(LoginNameInstanceIDCol, UserInstanceIDCol, ColumnEquals) if err != nil { return nil, err @@ -828,30 +818,16 @@ func triggerUserProjections(ctx context.Context) { triggerBatch(ctx, projection.UserProjection, projection.LoginNameProjection) } -func prepareLoginNamesQuery() (string, []interface{}, error) { - return sq.Select( - userLoginNamesUserIDCol.identifier(), - "ARRAY_AGG("+userLoginNamesNameCol.identifier()+")::TEXT[] AS "+userLoginNamesListCol.name, - "ARRAY_AGG(LOWER("+userLoginNamesNameCol.identifier()+"))::TEXT[] AS "+userLoginNamesLowerListCol.name, - userLoginNamesInstanceIDCol.identifier(), - ).From(userLoginNamesTable.identifier()). - GroupBy( - userLoginNamesUserIDCol.identifier(), - userLoginNamesInstanceIDCol.identifier(), - ).ToSql() -} - -func preparePreferredLoginNamesQuery() (string, []interface{}, error) { - return sq.Select( - userPreferredLoginNameUserIDCol.identifier(), - userPreferredLoginNameCol.identifier(), - userPreferredLoginNameInstanceIDCol.identifier(), - ).From(userPreferredLoginNameTable.identifier()). - Where(sq.Eq{ - userPreferredLoginNameIsPrimaryCol.identifier(): true, - }, - ).ToSql() -} +var joinLoginNames = `LEFT JOIN LATERAL (` + + `SELECT` + + ` ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names,` + + ` MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name` + + ` FROM` + + ` projections.login_names3 AS ln` + + ` WHERE` + + ` ln.user_id = ` + UserIDCol.identifier() + + ` AND ln.instance_id = ` + UserInstanceIDCol.identifier() + + `) AS login_names ON TRUE` func scanUser(row *sql.Row) (*User, error) { u := new(User) @@ -951,64 +927,6 @@ func scanUser(row *sql.Row) (*User, error) { return u, nil } -func prepareUserQuery() (sq.SelectBuilder, func(*sql.Row) (*User, error)) { - loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery() - if err != nil { - return sq.SelectBuilder{}, nil - } - preferredLoginNameQuery, preferredLoginNameArgs, err := preparePreferredLoginNamesQuery() - if err != nil { - return sq.SelectBuilder{}, nil - } - return sq.Select( - UserIDCol.identifier(), - UserCreationDateCol.identifier(), - UserChangeDateCol.identifier(), - UserResourceOwnerCol.identifier(), - UserSequenceCol.identifier(), - UserStateCol.identifier(), - UserTypeCol.identifier(), - UserUsernameCol.identifier(), - userLoginNamesListCol.identifier(), - userPreferredLoginNameCol.identifier(), - HumanUserIDCol.identifier(), - HumanFirstNameCol.identifier(), - HumanLastNameCol.identifier(), - HumanNickNameCol.identifier(), - HumanDisplayNameCol.identifier(), - HumanPreferredLanguageCol.identifier(), - HumanGenderCol.identifier(), - HumanAvatarURLCol.identifier(), - HumanEmailCol.identifier(), - HumanIsEmailVerifiedCol.identifier(), - HumanPhoneCol.identifier(), - HumanIsPhoneVerifiedCol.identifier(), - HumanPasswordChangeRequiredCol.identifier(), - HumanPasswordChangedCol.identifier(), - HumanMFAInitSkippedCol.identifier(), - MachineUserIDCol.identifier(), - MachineNameCol.identifier(), - MachineDescriptionCol.identifier(), - MachineSecretCol.identifier(), - MachineAccessTokenTypeCol.identifier(), - countColumn.identifier(), - ). - From(userTable.identifier()). - LeftJoin(join(HumanUserIDCol, UserIDCol)). - LeftJoin(join(MachineUserIDCol, UserIDCol)). - LeftJoin("("+loginNamesQuery+") AS "+userLoginNamesTable.alias+" ON "+ - userLoginNamesUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userLoginNamesInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), - loginNamesArgs...). - LeftJoin("("+preferredLoginNameQuery+") AS "+userPreferredLoginNameTable.alias+" ON "+ - userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), - preferredLoginNameArgs...). - PlaceholderFormat(sq.Dollar), - - scanUser -} - func prepareProfileQuery() (sq.SelectBuilder, func(*sql.Row) (*Profile, error)) { return sq.Select( UserIDCol.identifier(), @@ -1170,14 +1088,6 @@ func preparePhoneQuery() (sq.SelectBuilder, func(*sql.Row) (*Phone, error)) { } func prepareNotifyUserQuery() (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, error)) { - loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery() - if err != nil { - return sq.SelectBuilder{}, nil - } - preferredLoginNameQuery, preferredLoginNameArgs, err := preparePreferredLoginNamesQuery() - if err != nil { - return sq.SelectBuilder{}, nil - } return sq.Select( UserIDCol.identifier(), UserCreationDateCol.identifier(), @@ -1208,14 +1118,7 @@ func prepareNotifyUserQuery() (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, er From(userTable.identifier()). LeftJoin(join(HumanUserIDCol, UserIDCol)). LeftJoin(join(NotifyUserIDCol, UserIDCol)). - LeftJoin("("+loginNamesQuery+") AS "+userLoginNamesTable.alias+" ON "+ - userLoginNamesUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userLoginNamesInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), - loginNamesArgs...). - LeftJoin("("+preferredLoginNameQuery+") AS "+userPreferredLoginNameTable.alias+" ON "+ - userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), - preferredLoginNameArgs...). + JoinClause(joinLoginNames). PlaceholderFormat(sq.Dollar), scanNotifyUser } @@ -1359,14 +1262,6 @@ func prepareUserUniqueQuery() (sq.SelectBuilder, func(*sql.Row) (bool, error)) { } func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { - loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery() - if err != nil { - return sq.SelectBuilder{}, nil - } - preferredLoginNameQuery, preferredLoginNameArgs, err := preparePreferredLoginNamesQuery() - if err != nil { - return sq.SelectBuilder{}, nil - } return sq.Select( UserIDCol.identifier(), UserCreationDateCol.identifier(), @@ -1401,14 +1296,7 @@ func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { From(userTable.identifier()). LeftJoin(join(HumanUserIDCol, UserIDCol)). LeftJoin(join(MachineUserIDCol, UserIDCol)). - LeftJoin("("+loginNamesQuery+") AS "+userLoginNamesTable.alias+" ON "+ - userLoginNamesUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userLoginNamesInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), - loginNamesArgs...). - LeftJoin("("+preferredLoginNameQuery+") AS "+userPreferredLoginNameTable.alias+" ON "+ - userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), - preferredLoginNameArgs...). + JoinClause(joinLoginNames). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Users, error) { users := make([]*User, 0) diff --git a/internal/query/user_by_id.sql b/internal/query/user_by_id.sql index 2ce741f9b7..a89e701698 100644 --- a/internal/query/user_by_id.sql +++ b/internal/query/user_by_id.sql @@ -1,41 +1,3 @@ -WITH login_names AS (SELECT - u.id user_id - , u.instance_id - , u.resource_owner - , u.user_name - , d.name domain_name - , d.is_primary - , p.must_be_domain - , CASE WHEN p.must_be_domain - THEN concat(u.user_name, '@', d.name) - ELSE u.user_name - END login_name - FROM - projections.login_names3_users u - JOIN lateral ( - SELECT - p.must_be_domain - FROM - projections.login_names3_policies p - WHERE - u.instance_id = p.instance_id - AND ( - (p.is_default IS TRUE AND p.instance_id = $3) - OR (p.instance_id = $3 AND p.resource_owner = u.resource_owner) - ) - ORDER BY is_default - LIMIT 1 - ) p ON TRUE - JOIN - projections.login_names3_domains d - ON - u.instance_id = d.instance_id - AND u.resource_owner = d.resource_owner - WHERE - u.id = $1 - AND (u.resource_owner = $2 OR $2 = '') - AND u.instance_id = $3 -) SELECT u.id , u.creation_date @@ -45,8 +7,8 @@ SELECT , u.state , u.type , u.username - , (SELECT array_agg(ln.login_name)::TEXT[] login_names FROM login_names ln GROUP BY ln.user_id, ln.instance_id) login_names - , (SELECT ln.login_name login_names_lower FROM login_names ln WHERE ln.is_primary IS TRUE) preferred_login_name + , login_names.login_names AS login_names + , login_names.preferred_login_name AS preferred_login_name , h.user_id , h.first_name , h.last_name @@ -79,6 +41,16 @@ LEFT JOIN ON u.id = m.user_id AND u.instance_id = m.instance_id +LEFT JOIN LATERAL ( + SELECT + ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names, + MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name + FROM + projections.login_names3 AS ln + WHERE + ln.user_id = u.id + AND ln.instance_id = u.instance_id +) AS login_names ON TRUE WHERE u.id = $1 AND (u.resource_owner = $2 OR $2 = '') diff --git a/internal/query/user_metadata.go b/internal/query/user_metadata.go index ff612f82c8..534c707593 100644 --- a/internal/query/user_metadata.go +++ b/internal/query/user_metadata.go @@ -97,7 +97,7 @@ func (q *Queries) GetUserMetadataByKey(ctx context.Context, shouldTriggerBulk bo } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-aDGG2", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-aDGG2", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -125,7 +125,7 @@ func (q *Queries) SearchUserMetadataForUsers(ctx context.Context, shouldTriggerB } stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Egbgd", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Egbgd", "Errors.Query.SQLStatement") } err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { @@ -157,7 +157,7 @@ func (q *Queries) SearchUserMetadata(ctx context.Context, shouldTriggerBulk bool } stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Egbgd", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Egbgd", "Errors.Query.SQLStatement") } err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { diff --git a/internal/query/user_notify_by_id.sql b/internal/query/user_notify_by_id.sql index 10aa60ee60..6322229a91 100644 --- a/internal/query/user_notify_by_id.sql +++ b/internal/query/user_notify_by_id.sql @@ -1,41 +1,3 @@ -WITH login_names AS ( - SELECT - u.id user_id - , u.instance_id - , u.resource_owner - , u.user_name - , d.name domain_name - , d.is_primary - , p.must_be_domain - , CASE WHEN p.must_be_domain - THEN concat(u.user_name, '@', d.name) - ELSE u.user_name - END login_name - FROM - projections.login_names3_users u - JOIN lateral ( - SELECT - p.must_be_domain - FROM - projections.login_names3_policies p - WHERE - u.instance_id = p.instance_id - AND ( - (p.is_default IS TRUE AND p.instance_id = $2) - OR (p.instance_id = $2 AND p.resource_owner = u.resource_owner) - ) - ORDER BY is_default - LIMIT 1 - ) p ON TRUE - JOIN - projections.login_names3_domains d - ON - u.instance_id = d.instance_id - AND u.resource_owner = d.resource_owner - WHERE - u.instance_id = $2 - AND u.id = $1 -) SELECT u.id , u.creation_date @@ -45,8 +7,8 @@ SELECT , u.state , u.type , u.username - , (SELECT array_agg(ln.login_name)::TEXT[] login_names FROM login_names ln GROUP BY ln.user_id, ln.instance_id) login_names - , (SELECT ln.login_name login_names_lower FROM login_names ln WHERE ln.is_primary IS TRUE) preferred_login_name + , login_names.login_names AS login_names + , login_names.preferred_login_name AS preferred_login_name , h.user_id , h.first_name , h.last_name @@ -73,6 +35,16 @@ LEFT JOIN ON u.id = n.user_id AND u.instance_id = n.instance_id +LEFT JOIN LATERAL ( + SELECT + ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names, + MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name + FROM + projections.login_names3 AS ln + WHERE + ln.user_id = u.id + AND ln.instance_id = u.instance_id +) AS login_names ON TRUE WHERE u.id = $1 AND u.instance_id = $2 diff --git a/internal/query/user_personal_access_token.go b/internal/query/user_personal_access_token.go index 61d349961c..49281d9f90 100644 --- a/internal/query/user_personal_access_token.go +++ b/internal/query/user_personal_access_token.go @@ -118,7 +118,7 @@ func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Dgfb4", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Dgfb4", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { diff --git a/internal/query/user_test.go b/internal/query/user_test.go index 50d65cc1ec..ae5f6be207 100644 --- a/internal/query/user_test.go +++ b/internal/query/user_test.go @@ -222,87 +222,6 @@ func TestUser_userCheckPermission(t *testing.T) { } var ( - loginNamesQuery = `SELECT login_names.user_id, ARRAY_AGG(login_names.login_name)::TEXT[] AS loginnames, ARRAY_AGG(LOWER(login_names.login_name))::TEXT[] AS loginnames_lower, login_names.instance_id` + - ` FROM projections.login_names3 AS login_names` + - ` GROUP BY login_names.user_id, login_names.instance_id` - preferredLoginNameQuery = `SELECT preferred_login_name.user_id, preferred_login_name.login_name, preferred_login_name.instance_id` + - ` FROM projections.login_names3 AS preferred_login_name` + - ` WHERE preferred_login_name.is_primary = $1` - userQuery = `SELECT projections.users14.id,` + - ` projections.users14.creation_date,` + - ` projections.users14.change_date,` + - ` projections.users14.resource_owner,` + - ` projections.users14.sequence,` + - ` projections.users14.state,` + - ` projections.users14.type,` + - ` projections.users14.username,` + - ` login_names.loginnames,` + - ` preferred_login_name.login_name,` + - ` projections.users14_humans.user_id,` + - ` projections.users14_humans.first_name,` + - ` projections.users14_humans.last_name,` + - ` projections.users14_humans.nick_name,` + - ` projections.users14_humans.display_name,` + - ` projections.users14_humans.preferred_language,` + - ` projections.users14_humans.gender,` + - ` projections.users14_humans.avatar_key,` + - ` projections.users14_humans.email,` + - ` projections.users14_humans.is_email_verified,` + - ` projections.users14_humans.phone,` + - ` projections.users14_humans.is_phone_verified,` + - ` projections.users14_humans.password_change_required,` + - ` projections.users14_humans.password_changed,` + - ` projections.users14_humans.mfa_init_skipped,` + - ` projections.users14_machines.user_id,` + - ` projections.users14_machines.name,` + - ` projections.users14_machines.description,` + - ` projections.users14_machines.secret,` + - ` projections.users14_machines.access_token_type,` + - ` COUNT(*) OVER ()` + - ` FROM projections.users14` + - ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + - ` LEFT JOIN projections.users14_machines ON projections.users14.id = projections.users14_machines.user_id AND projections.users14.instance_id = projections.users14_machines.instance_id` + - ` LEFT JOIN` + - ` (` + loginNamesQuery + `) AS login_names` + - ` ON login_names.user_id = projections.users14.id AND login_names.instance_id = projections.users14.instance_id` + - ` LEFT JOIN` + - ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` - userCols = []string{ - "id", - "creation_date", - "change_date", - "resource_owner", - "sequence", - "state", - "type", - "username", - "loginnames", - "login_name", - // human - "user_id", - "first_name", - "last_name", - "nick_name", - "display_name", - "preferred_language", - "gender", - "avatar_key", - "email", - "is_email_verified", - "phone", - "is_phone_verified", - "password_change_required", - "password_changed", - "mfa_init_skipped", - // machine - "user_id", - "name", - "description", - "secret", - "access_token_type", - "count", - } profileQuery = `SELECT projections.users14.id,` + ` projections.users14.creation_date,` + ` projections.users14.change_date,` + @@ -397,8 +316,8 @@ var ( ` projections.users14.state,` + ` projections.users14.type,` + ` projections.users14.username,` + - ` login_names.loginnames,` + - ` preferred_login_name.login_name,` + + ` login_names.login_names,` + + ` login_names.preferred_login_name,` + ` projections.users14_humans.user_id,` + ` projections.users14_humans.first_name,` + ` projections.users14_humans.last_name,` + @@ -417,12 +336,7 @@ var ( ` FROM projections.users14` + ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + ` LEFT JOIN projections.users14_notifications ON projections.users14.id = projections.users14_notifications.user_id AND projections.users14.instance_id = projections.users14_notifications.instance_id` + - ` LEFT JOIN` + - ` (` + loginNamesQuery + `) AS login_names` + - ` ON login_names.user_id = projections.users14.id AND login_names.instance_id = projections.users14.instance_id` + - ` LEFT JOIN` + - ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` + ` LEFT JOIN LATERAL (SELECT ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names, MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name FROM projections.login_names3 AS ln WHERE ln.user_id = projections.users14.id AND ln.instance_id = projections.users14.instance_id) AS login_names ON TRUE` notifyUserCols = []string{ "id", "creation_date", @@ -432,8 +346,8 @@ var ( "state", "type", "username", - "loginnames", - "login_name", + "login_names", + "preferred_login_name", // human "user_id", "first_name", @@ -460,8 +374,8 @@ var ( ` projections.users14.state,` + ` projections.users14.type,` + ` projections.users14.username,` + - ` login_names.loginnames,` + - ` preferred_login_name.login_name,` + + ` login_names.login_names,` + + ` login_names.preferred_login_name,` + ` projections.users14_humans.user_id,` + ` projections.users14_humans.first_name,` + ` projections.users14_humans.last_name,` + @@ -485,12 +399,7 @@ var ( ` FROM projections.users14` + ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + ` LEFT JOIN projections.users14_machines ON projections.users14.id = projections.users14_machines.user_id AND projections.users14.instance_id = projections.users14_machines.instance_id` + - ` LEFT JOIN` + - ` (` + loginNamesQuery + `) AS login_names` + - ` ON login_names.user_id = projections.users14.id AND login_names.instance_id = projections.users14.instance_id` + - ` LEFT JOIN` + - ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` + ` LEFT JOIN LATERAL (SELECT ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names, MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name FROM projections.login_names3 AS ln WHERE ln.user_id = projections.users14.id AND ln.instance_id = projections.users14.instance_id) AS login_names ON TRUE` usersCols = []string{ "id", "creation_date", @@ -500,8 +409,8 @@ var ( "state", "type", "username", - "loginnames", - "login_name", + "login_names", + "preferred_login_name", // human "user_id", "first_name", @@ -540,240 +449,6 @@ func Test_UserPrepares(t *testing.T) { want want object interface{} }{ - { - name: "prepareUserQuery no result", - prepare: prepareUserQuery, - want: want{ - sqlExpectations: mockQueryScanErr( - regexp.QuoteMeta(userQuery), - nil, - nil, - ), - err: func(err error) (error, bool) { - if !zerrors.IsNotFound(err) { - return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false - } - return nil, true - }, - }, - object: (*User)(nil), - }, - { - name: "prepareUserQuery human found", - prepare: prepareUserQuery, - want: want{ - sqlExpectations: mockQuery( - regexp.QuoteMeta(userQuery), - userCols, - []driver.Value{ - "id", - testNow, - testNow, - "resource_owner", - uint64(20211108), - domain.UserStateActive, - domain.UserTypeHuman, - "username", - database.TextArray[string]{"login_name1", "login_name2"}, - "login_name1", - // human - "id", - "first_name", - "last_name", - "nick_name", - "display_name", - "de", - domain.GenderUnspecified, - "avatar_key", - "email", - true, - "phone", - true, - true, - testNow, - testNow, - // machine - nil, - nil, - nil, - nil, - nil, - 1, - }, - ), - }, - object: &User{ - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - ResourceOwner: "resource_owner", - Sequence: 20211108, - State: domain.UserStateActive, - Type: domain.UserTypeHuman, - Username: "username", - LoginNames: database.TextArray[string]{"login_name1", "login_name2"}, - PreferredLoginName: "login_name1", - Human: &Human{ - FirstName: "first_name", - LastName: "last_name", - NickName: "nick_name", - DisplayName: "display_name", - AvatarKey: "avatar_key", - PreferredLanguage: language.German, - Gender: domain.GenderUnspecified, - Email: "email", - IsEmailVerified: true, - Phone: "phone", - IsPhoneVerified: true, - PasswordChangeRequired: true, - PasswordChanged: testNow, - MFAInitSkipped: testNow, - }, - }, - }, - { - name: "prepareUserQuery machine found", - prepare: prepareUserQuery, - want: want{ - sqlExpectations: mockQuery( - regexp.QuoteMeta(userQuery), - userCols, - []driver.Value{ - "id", - testNow, - testNow, - "resource_owner", - uint64(20211108), - domain.UserStateActive, - domain.UserTypeMachine, - "username", - database.TextArray[string]{"login_name1", "login_name2"}, - "login_name1", - // human - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - // machine - "id", - "name", - "description", - nil, - domain.OIDCTokenTypeBearer, - 1, - }, - ), - }, - object: &User{ - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - ResourceOwner: "resource_owner", - Sequence: 20211108, - State: domain.UserStateActive, - Type: domain.UserTypeMachine, - Username: "username", - LoginNames: database.TextArray[string]{"login_name1", "login_name2"}, - PreferredLoginName: "login_name1", - Machine: &Machine{ - Name: "name", - Description: "description", - EncodedSecret: "", - AccessTokenType: domain.OIDCTokenTypeBearer, - }, - }, - }, - { - name: "prepareUserQuery machine with secret found", - prepare: prepareUserQuery, - want: want{ - sqlExpectations: mockQuery( - regexp.QuoteMeta(userQuery), - userCols, - []driver.Value{ - "id", - testNow, - testNow, - "resource_owner", - uint64(20211108), - domain.UserStateActive, - domain.UserTypeMachine, - "username", - database.TextArray[string]{"login_name1", "login_name2"}, - "login_name1", - // human - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - // machine - "id", - "name", - "description", - "secret", - domain.OIDCTokenTypeBearer, - 1, - }, - ), - }, - object: &User{ - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - ResourceOwner: "resource_owner", - Sequence: 20211108, - State: domain.UserStateActive, - Type: domain.UserTypeMachine, - Username: "username", - LoginNames: database.TextArray[string]{"login_name1", "login_name2"}, - PreferredLoginName: "login_name1", - Machine: &Machine{ - Name: "name", - Description: "description", - EncodedSecret: "secret", - AccessTokenType: domain.OIDCTokenTypeBearer, - }, - }, - }, - { - name: "prepareUserQuery sql err", - prepare: prepareUserQuery, - want: want{ - sqlExpectations: mockQueryErr( - regexp.QuoteMeta(userQuery), - sql.ErrConnDone, - ), - err: func(err error) (error, bool) { - if !errors.Is(err, sql.ErrConnDone) { - return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false - } - return nil, true - }, - }, - object: (*User)(nil), - }, { name: "prepareProfileQuery no result", prepare: prepareProfileQuery, diff --git a/load-test/src/org.ts b/load-test/src/org.ts index f5655432a5..1ed6778d9c 100644 --- a/load-test/src/org.ts +++ b/load-test/src/org.ts @@ -13,7 +13,7 @@ export function createOrg(accessToken: string): Promise { return new Promise((resolve, reject) => { let response = http.asyncRequest( 'POST', - url('/v2beta/organizations'), + url('/v2/organizations'), JSON.stringify({ name: `load-test-${new Date(Date.now()).toISOString()}`, }), diff --git a/load-test/src/use_cases/manipulate_user.ts b/load-test/src/use_cases/manipulate_user.ts index 2ea53bd324..104d81678e 100644 --- a/load-test/src/use_cases/manipulate_user.ts +++ b/load-test/src/use_cases/manipulate_user.ts @@ -15,7 +15,7 @@ export async function setup() { } export default async function (data: any) { - const human = await createHuman(`vu-${__VU}`, data.org, data.tokens.accessToken); + const human = await createHuman(`vu-${__VU}-${new Date(Date.now()).getTime()}`, data.org, data.tokens.accessToken); const updateRes = await updateHuman( { profile: { diff --git a/load-test/src/user.ts b/load-test/src/user.ts index 86ce71fd9b..83a6bba839 100644 --- a/load-test/src/user.ts +++ b/load-test/src/user.ts @@ -30,7 +30,7 @@ export function createHuman(username: string, org: Org, accessToken: string): Pr familyName: 'Zitizen', }, email: { - email: `zitizen-@caos.ch`, + email: `${username}@zitadel.com`, isVerified: true, }, password: { @@ -50,11 +50,11 @@ export function createHuman(username: string, org: Org, accessToken: string): Pr response .then((res) => { check(res, { - 'create user is status ok': (r) => r.status === 201, + 'create user is status ok': (r) => r.status === 200, }) || reject(`unable to create user(username: ${username}) status: ${res.status} body: ${res.body}`); createHumanTrend.add(res.timings.duration); - const user = http.get(url(`/v2beta/users/${res.json('userId')!}`), { + const user = http.get(url(`/v2/users/${res.json('userId')!}`), { headers: { authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', From c1cda9bfac63ea2a34a7fda555781ec8ba5583bb Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 10 Jun 2025 09:48:46 +0200 Subject: [PATCH 30/35] fix: metadata decoding and encoding #9816 (#10024) # Which Problems Are Solved Metadata encoding and decoding on the organization detail page was broken due to use of the old, generated gRPC client. # How the Problems Are Solved The metadata values are now correctly base64 decoded and encoded on the organization detail page. # Additional Changes Refactored parts of the code to remove the dependency on the buffer npm package, replacing it with the browser-native TextEncoder and TextDecoder APIs. # Additional Context - Closes [#9816](https://github.com/zitadel/zitadel/issues/9816) --- .../metadata-dialog/metadata-dialog.component.ts | 4 ++-- .../modules/metadata/metadata/metadata.component.ts | 12 ++++++------ .../pages/orgs/org-detail/org-detail.component.ts | 8 ++++---- .../user-detail/user-detail/user-detail.component.ts | 3 +-- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts b/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts index 8deff09eee..c75e15bf04 100644 --- a/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts +++ b/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts @@ -4,7 +4,6 @@ import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; import { ToastService } from 'src/app/services/toast.service'; import { Metadata as MetadataV2 } from '@zitadel/proto/zitadel/metadata_pb'; import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb'; -import { Buffer } from 'buffer'; export type MetadataDialogData = { metadata: (Metadata.AsObject | MetadataV2)[]; @@ -26,9 +25,10 @@ export class MetadataDialogComponent { public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: MetadataDialogData, ) { + const decoder = new TextDecoder(); this.metadata = data.metadata.map(({ key, value }) => ({ key, - value: typeof value === 'string' ? value : Buffer.from(value as unknown as string, 'base64').toString('utf8'), + value: typeof value === 'string' ? value : decoder.decode(value), })); } diff --git a/console/src/app/modules/metadata/metadata/metadata.component.ts b/console/src/app/modules/metadata/metadata/metadata.component.ts index 7f72297c00..bdb2c7734c 100644 --- a/console/src/app/modules/metadata/metadata/metadata.component.ts +++ b/console/src/app/modules/metadata/metadata/metadata.component.ts @@ -5,7 +5,6 @@ import { Observable, ReplaySubject } from 'rxjs'; import { Metadata as MetadataV2 } from '@zitadel/proto/zitadel/metadata_pb'; import { map, startWith } from 'rxjs/operators'; import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb'; -import { Buffer } from 'buffer'; type StringMetadata = { key: string; @@ -37,12 +36,13 @@ export class MetadataComponent implements OnInit { ngOnInit() { this.dataSource$ = this.metadata$.pipe( - map((metadata) => - metadata.map(({ key, value }) => ({ + map((metadata) => { + const decoder = new TextDecoder(); + return metadata.map(({ key, value }) => ({ key, - value: Buffer.from(value as any as string, 'base64').toString('utf-8'), - })), - ), + value: typeof value === 'string' ? value : decoder.decode(value), + })); + }), startWith([] as StringMetadata[]), map((metadata) => new MatTableDataSource(metadata)), ); diff --git a/console/src/app/pages/orgs/org-detail/org-detail.component.ts b/console/src/app/pages/orgs/org-detail/org-detail.component.ts index 0de6696ac3..39514d33d3 100644 --- a/console/src/app/pages/orgs/org-detail/org-detail.component.ts +++ b/console/src/app/pages/orgs/org-detail/org-detail.component.ts @@ -1,7 +1,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; -import { Buffer } from 'buffer'; import { BehaviorSubject, from, Observable, of, Subject, takeUntil } from 'rxjs'; import { catchError, finalize, map } from 'rxjs/operators'; import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component'; @@ -266,10 +265,11 @@ export class OrgDetailComponent implements OnInit, OnDestroy { .listOrgMetadata() .then((resp) => { this.loadingMetadata = false; - this.metadata = resp.resultList.map((md) => { + const decoder = new TextDecoder(); + this.metadata = resp.resultList.map(({ key, value }) => { return { - key: md.key, - value: Buffer.from(md.value as string, 'base64').toString('utf-8'), + key, + value: atob(typeof value === 'string' ? value : decoder.decode(value)), }; }); }) diff --git a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts index 9370471ea4..90de097f35 100644 --- a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts +++ b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts @@ -4,7 +4,6 @@ import { Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Params, Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { Buffer } from 'buffer'; import { catchError, filter, map, startWith, take } from 'rxjs/operators'; import { ChangeType } from 'src/app/modules/changes/changes.component'; import { phoneValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators'; @@ -582,7 +581,7 @@ export class UserDetailComponent implements OnInit { const setFcn = (key: string, value: string) => this.newMgmtService.setUserMetadata({ key, - value: Buffer.from(value), + value: new TextEncoder().encode(value), id: user.userId, }); const removeFcn = (key: string): Promise => this.newMgmtService.removeUserMetadata({ key, id: user.userId }); From 0ae3f2a6eab7dfe60f686ed08fca7c4f1c9822e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabienne=20B=C3=BChler?= Date: Wed, 11 Jun 2025 11:23:39 +0200 Subject: [PATCH 31/35] docs: remove token exchange from "GA" list as we have some open issues (#10052) # Which Problems Are Solved Token Exchange will not move from Beta to GA feature, as there are still some unsolved issues # How the Problems Are Solved Remove from roadmap --- docs/docs/product/roadmap.mdx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/docs/product/roadmap.mdx b/docs/docs/product/roadmap.mdx index b83efedb10..b61323fa90 100644 --- a/docs/docs/product/roadmap.mdx +++ b/docs/docs/product/roadmap.mdx @@ -394,15 +394,6 @@ Excitingly, v3 introduces the foundational elements for Actions V2, opening up a - [Manage Users Guide](https://zitadel.com/docs/guides/manage/user/scim2) - -

- Token Exchange (Impersonation) - - The Token Exchange grant implements [RFC 8693, OAuth 2.0 Token Exchange](https://www.rfc-editor.org/rfc/rfc8693) and can be used to exchange tokens to a different scope, audience or subject. - Changing the subject of an authenticated token is called impersonation or delegation. - Read more in our [Impersonation and delegation using Token Exchange](https://zitadel.com/docs/guides/integrate/token-exchange) Guide -
-
Caches From 77f0a10c1e303ac881f3c1357a73596c22ffaf16 Mon Sep 17 00:00:00 2001 From: Iraq <66622793+kkrime@users.noreply.github.com> Date: Wed, 11 Jun 2025 13:50:31 +0200 Subject: [PATCH 32/35] fix(import/export): fix for deactivated user/organization being imported as active (#9992) --- internal/api/grpc/admin/export.go | 12 +- internal/api/grpc/admin/import.go | 15 +- internal/api/grpc/management/user.go | 5 +- internal/api/grpc/user/v2/machine.go | 1 + internal/command/org.go | 13 +- internal/command/user_human.go | 65 +++-- internal/command/user_human_test.go | 367 +++++++++++++++++++++++++- internal/command/user_machine.go | 25 +- internal/command/user_machine_test.go | 109 +++++++- proto/zitadel/admin.proto | 1 + proto/zitadel/v1.proto | 2 + 11 files changed, 574 insertions(+), 41 deletions(-) diff --git a/internal/api/grpc/admin/export.go b/internal/api/grpc/admin/export.go index 2558e5b5fc..b5d36272d4 100644 --- a/internal/api/grpc/admin/export.go +++ b/internal/api/grpc/admin/export.go @@ -8,7 +8,9 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" authn_grpc "github.com/zitadel/zitadel/internal/api/grpc/authn" + "github.com/zitadel/zitadel/internal/api/grpc/org" text_grpc "github.com/zitadel/zitadel/internal/api/grpc/text" + user_converter "github.com/zitadel/zitadel/internal/api/grpc/user" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -65,7 +67,7 @@ func (s *Server) ExportData(ctx context.Context, req *admin_pb.ExportDataRequest /****************************************************************************************************************** Organization ******************************************************************************************************************/ - org := &admin_pb.DataOrg{OrgId: queriedOrg.ID, Org: &management_pb.AddOrgRequest{Name: queriedOrg.Name}} + org := &admin_pb.DataOrg{OrgId: queriedOrg.ID, OrgState: org.OrgStateToPb(queriedOrg.State), Org: &management_pb.AddOrgRequest{Name: queriedOrg.Name}} orgs[i] = org } @@ -567,6 +569,7 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w case domain.UserTypeHuman: dataUser := &v1_pb.DataHumanUser{ UserId: user.ID, + State: user_converter.UserStateToPb(user.State), User: &management_pb.ImportHumanUserRequest{ UserName: user.Username, Profile: &management_pb.ImportHumanUserRequest_Profile{ @@ -620,6 +623,7 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w case domain.UserTypeMachine: machineUsers = append(machineUsers, &v1_pb.DataMachineUser{ UserId: user.ID, + State: user_converter.UserStateToPb(user.State), User: &management_pb.AddMachineUserRequest{ UserName: user.Username, Name: user.Machine.Name, @@ -647,7 +651,6 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w ExpirationDate: timestamppb.New(key.Expiration), PublicKey: key.PublicKey, }) - } } @@ -888,7 +891,6 @@ func (s *Server) getNecessaryProjectGrantMembersForOrg(ctx context.Context, org break } } - } } } @@ -940,7 +942,6 @@ func (s *Server) getNecessaryOrgMembersForOrg(ctx context.Context, org string, p } func (s *Server) getNecessaryProjectGrantsForOrg(ctx context.Context, org string, processedOrgs []string, processedProjects []string) ([]*v1_pb.DataProjectGrant, error) { - projectGrantSearchOrg, err := query.NewProjectGrantResourceOwnerSearchQuery(org) if err != nil { return nil, err @@ -991,7 +992,7 @@ func (s *Server) getNecessaryUserGrantsForOrg(ctx context.Context, org string, p for _, userGrant := range queriedUserGrants.UserGrants { for _, projectID := range processedProjects { if projectID == userGrant.ProjectID { - //if usergrant is on a granted project + // if usergrant is on a granted project if userGrant.GrantID != "" { for _, grantID := range processedGrants { if grantID == userGrant.GrantID { @@ -1024,6 +1025,7 @@ func (s *Server) getNecessaryUserGrantsForOrg(ctx context.Context, org string, p } return userGrants, nil } + func (s *Server) getCustomLoginTexts(ctx context.Context, org string, languages []string) ([]*management_pb.SetCustomLoginTextsRequest, error) { customTexts := make([]*management_pb.SetCustomLoginTextsRequest, 0, len(languages)) for _, lang := range languages { diff --git a/internal/api/grpc/admin/import.go b/internal/api/grpc/admin/import.go index 41a1e39081..84b0215f03 100644 --- a/internal/api/grpc/admin/import.go +++ b/internal/api/grpc/admin/import.go @@ -22,6 +22,7 @@ import ( action_grpc "github.com/zitadel/zitadel/internal/api/grpc/action" "github.com/zitadel/zitadel/internal/api/grpc/authn" "github.com/zitadel/zitadel/internal/api/grpc/management" + org_converter "github.com/zitadel/zitadel/internal/api/grpc/org" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -305,7 +306,8 @@ func importOrg1(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataEr ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - _, err = s.command.AddOrgWithID(ctx, org.GetOrg().GetName(), ctxData.UserID, ctxData.ResourceOwner, org.GetOrgId(), []string{}) + setOrgInactive := org_converter.OrgStateToDomain(org.OrgState) == domain.OrgStateInactive + _, err = s.command.AddOrgWithID(ctx, org.GetOrg().GetName(), ctxData.UserID, ctxData.ResourceOwner, org.GetOrgId(), setOrgInactive, []string{}) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "org", Id: org.GetOrgId(), Message: err.Error()}) if _, err := s.query.OrgByID(ctx, true, org.OrgId); err != nil { @@ -474,7 +476,10 @@ func importHumanUsers(ctx context.Context, s *Server, errors *[]*admin_pb.Import logging.Debugf("import user: %s", user.GetUserId()) human, passwordless, links := management.ImportHumanUserRequestToDomain(user.User) human.AggregateID = user.UserId - _, _, err := s.command.ImportHuman(ctx, org.GetOrgId(), human, passwordless, links, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode) + userState := user.State.ToDomain() + + //nolint:staticcheck + _, _, err := s.command.ImportHuman(ctx, org.GetOrgId(), human, passwordless, &userState, links, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "human_user", Id: user.GetUserId(), Message: err.Error()}) if isCtxTimeout(ctx) { @@ -510,7 +515,8 @@ func importMachineUsers(ctx context.Context, s *Server, errors *[]*admin_pb.Impo } for _, user := range org.GetMachineUsers() { logging.Debugf("import user: %s", user.GetUserId()) - _, err := s.command.AddMachine(ctx, management.AddMachineUserRequestToCommand(user.GetUser(), org.GetOrgId()), nil) + userState := user.State.ToDomain() + _, err := s.command.AddMachine(ctx, management.AddMachineUserRequestToCommand(user.GetUser(), org.GetOrgId()), &userState, nil) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "machine_user", Id: user.GetUserId(), Message: err.Error()}) if isCtxTimeout(ctx) { @@ -609,7 +615,6 @@ func importUserLinks(ctx context.Context, s *Server, errors *[]*admin_pb.ImportD successOrg.UserLinks = append(successOrg.UserLinks, &admin_pb.ImportDataSuccessUserLinks{UserId: userLinks.GetUserId(), IdpId: userLinks.GetIdpId(), ExternalUserId: userLinks.GetProvidedUserId(), DisplayName: userLinks.GetProvidedUserName()}) } return nil - } func importProjects(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) (err error) { @@ -750,6 +755,7 @@ func importActions(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDat } return nil } + func importProjectRoles(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -805,6 +811,7 @@ func importResources(ctx context.Context, s *Server, errors *[]*admin_pb.ImportD importDomainClaimedMessageTexts(ctx, s, errors, org) importPasswordlessRegistrationMessageTexts(ctx, s, errors, org) importInviteUserMessageTexts(ctx, s, errors, org) + if err := importHumanUsers(ctx, s, errors, successOrg, org, count, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode); err != nil { return err } diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index ae1040cd1e..09b9faa756 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -273,7 +273,8 @@ func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUs if err != nil { return nil, err } - addedHuman, code, err := s.command.ImportHuman(ctx, authz.GetCtxData(ctx).OrgID, human, passwordless, links, initCodeGenerator, phoneCodeGenerator, emailCodeGenerator, passwordlessInitCode) + //nolint:staticcheck + addedHuman, code, err := s.command.ImportHuman(ctx, authz.GetCtxData(ctx).OrgID, human, passwordless, nil, links, initCodeGenerator, phoneCodeGenerator, emailCodeGenerator, passwordlessInitCode) if err != nil { return nil, err } @@ -297,7 +298,7 @@ func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUs func (s *Server) AddMachineUser(ctx context.Context, req *mgmt_pb.AddMachineUserRequest) (*mgmt_pb.AddMachineUserResponse, error) { machine := AddMachineUserRequestToCommand(req, authz.GetCtxData(ctx).OrgID) - objectDetails, err := s.command.AddMachine(ctx, machine, nil) + objectDetails, err := s.command.AddMachine(ctx, machine, nil, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/user/v2/machine.go b/internal/api/grpc/user/v2/machine.go index 010ba75678..ad02b2289e 100644 --- a/internal/api/grpc/user/v2/machine.go +++ b/internal/api/grpc/user/v2/machine.go @@ -25,6 +25,7 @@ func (s *Server) createUserTypeMachine(ctx context.Context, machinePb *user.Crea details, err := s.command.AddMachine( ctx, cmd, + nil, s.command.NewPermissionCheckUserWrite(ctx), command.AddMachineWithUsernameToIDFallback(), ) diff --git a/internal/command/org.go b/internal/command/org.go index ddf99797e3..faab882d68 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -317,7 +317,7 @@ func (c *Commands) checkOrgExists(ctx context.Context, orgID string) error { return nil } -func (c *Commands) AddOrgWithID(ctx context.Context, name, userID, resourceOwner, orgID string, claimedUserIDs []string) (_ *domain.Org, err error) { +func (c *Commands) AddOrgWithID(ctx context.Context, name, userID, resourceOwner, orgID string, setOrgInactive bool, claimedUserIDs []string) (_ *domain.Org, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -329,7 +329,7 @@ func (c *Commands) AddOrgWithID(ctx context.Context, name, userID, resourceOwner return nil, zerrors.ThrowNotFound(nil, "ORG-lapo2m", "Errors.Org.AlreadyExisting") } - return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, claimedUserIDs) + return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, setOrgInactive, claimedUserIDs) } func (c *Commands) AddOrg(ctx context.Context, name, userID, resourceOwner string, claimedUserIDs []string) (*domain.Org, error) { @@ -342,10 +342,10 @@ func (c *Commands) AddOrg(ctx context.Context, name, userID, resourceOwner strin return nil, zerrors.ThrowInternal(err, "COMMA-OwciI", "Errors.Internal") } - return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, claimedUserIDs) + return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, false, claimedUserIDs) } -func (c *Commands) addOrgWithIDAndMember(ctx context.Context, name, userID, resourceOwner, orgID string, claimedUserIDs []string) (_ *domain.Org, err error) { +func (c *Commands) addOrgWithIDAndMember(ctx context.Context, name, userID, resourceOwner, orgID string, setOrgInactive bool, claimedUserIDs []string) (_ *domain.Org, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -363,10 +363,15 @@ func (c *Commands) addOrgWithIDAndMember(ctx context.Context, name, userID, reso return nil, err } events = append(events, orgMemberEvent) + if setOrgInactive { + deactivateOrgEvent := org.NewOrgDeactivatedEvent(ctx, orgAgg) + events = append(events, deactivateOrgEvent) + } pushedEvents, err := c.eventstore.Push(ctx, events...) if err != nil { return nil, err } + err = AppendAndReduce(addedOrg, pushedEvents...) if err != nil { return nil, err diff --git a/internal/command/user_human.go b/internal/command/user_human.go index 9e6ba43629..07628b9e19 100644 --- a/internal/command/user_human.go +++ b/internal/command/user_human.go @@ -428,7 +428,7 @@ func (h *AddHuman) shouldAddInitCode() bool { } // Deprecated: use commands.AddUserHuman -func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (_ *domain.Human, passwordlessCode *domain.PasswordlessInitCode, err error) { +func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, state *domain.UserState, links []*domain.UserIDPLink, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (_ *domain.Human, passwordlessCode *domain.PasswordlessInitCode, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -455,10 +455,32 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain. } } - events, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, links, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator) + events, userAgg, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, links, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator) if err != nil { return nil, nil, err } + if state != nil { + var event eventstore.Command + switch *state { + case domain.UserStateInactive: + event = user.NewUserDeactivatedEvent(ctx, userAgg) + case domain.UserStateLocked: + event = user.NewUserLockedEvent(ctx, userAgg) + case domain.UserStateDeleted: + // users are never imported if deleted + case domain.UserStateActive: + // added because of the linter + case domain.UserStateSuspend: + // added because of the linter + case domain.UserStateInitial: + // added because of the linter + case domain.UserStateUnspecified: + // added because of the linter + } + if event != nil { + events = append(events, event) + } + } pushedEvents, err := c.eventstore.Push(ctx, events...) if err != nil { return nil, nil, err @@ -479,48 +501,48 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain. return writeModelToHuman(addedHuman), passwordlessCode, nil } -func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) { +func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, userAgg *eventstore.Aggregate, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() if orgID == "" { - return nil, nil, nil, "", zerrors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.Org.Empty") + return nil, nil, nil, nil, "", zerrors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.Org.Empty") } if err = human.Normalize(); err != nil { - return nil, nil, nil, "", err + return nil, nil, nil, nil, "", err } - events, humanWriteModel, err = c.createHuman(ctx, orgID, human, links, passwordless, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) + events, userAgg, humanWriteModel, err = c.createHuman(ctx, orgID, human, links, passwordless, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) if err != nil { - return nil, nil, nil, "", err + return nil, nil, nil, nil, "", err } if passwordless { var codeEvent eventstore.Command codeEvent, passwordlessCodeWriteModel, code, err = c.humanAddPasswordlessInitCode(ctx, human.AggregateID, orgID, true, passwordlessCodeGenerator) if err != nil { - return nil, nil, nil, "", err + return nil, nil, nil, nil, "", err } events = append(events, codeEvent) } - return events, humanWriteModel, passwordlessCodeWriteModel, code, nil + return events, userAgg, humanWriteModel, passwordlessCodeWriteModel, code, nil } -func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, links []*domain.UserIDPLink, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) (events []eventstore.Command, addedHuman *HumanWriteModel, err error) { +func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, links []*domain.UserIDPLink, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) (events []eventstore.Command, userAgg *eventstore.Aggregate, addedHuman *HumanWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() if err = human.CheckDomainPolicy(domainPolicy); err != nil { - return nil, nil, err + return nil, nil, nil, err } human.Username = strings.TrimSpace(human.Username) human.EmailAddress = human.EmailAddress.Normalize() if err = c.userValidateDomain(ctx, orgID, human.Username, domainPolicy.UserLoginMustBeDomain); err != nil { - return nil, nil, err + return nil, nil, nil, err } if human.AggregateID == "" { userID, err := c.idGenerator.Next() if err != nil { - return nil, nil, err + return nil, nil, nil, err } human.AggregateID = userID } @@ -528,20 +550,21 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. human.EnsureDisplayName() if human.Password != nil { if err := human.HashPasswordIfExisting(ctx, pwPolicy, c.userPasswordHasher, human.Password.ChangeRequired); err != nil { - return nil, nil, err + return nil, nil, nil, err } } addedHuman = NewHumanWriteModel(human.AggregateID, orgID) - //TODO: adlerhurst maybe we could simplify the code below - userAgg := UserAggregateFromWriteModel(&addedHuman.WriteModel) + + // TODO: adlerhurst maybe we could simplify the code below + userAgg = UserAggregateFromWriteModelCtx(ctx, &addedHuman.WriteModel) events = append(events, createAddHumanEvent(ctx, userAgg, human, domainPolicy.UserLoginMustBeDomain)) for _, link := range links { event, err := c.addUserIDPLink(ctx, userAgg, link, false) if err != nil { - return nil, nil, err + return nil, nil, nil, err } events = append(events, event) } @@ -549,7 +572,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. if human.IsInitialState(passwordless, len(links) > 0) { initCode, err := domain.NewInitUserCode(initCodeGenerator) if err != nil { - return nil, nil, err + return nil, nil, nil, err } events = append(events, user.NewHumanInitialCodeAddedEvent(ctx, userAgg, initCode.Code, initCode.Expiry, "")) } else { @@ -558,7 +581,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. } else { emailCode, _, err := domain.NewEmailCode(emailCodeGenerator) if err != nil { - return nil, nil, err + return nil, nil, nil, err } events = append(events, user.NewHumanEmailCodeAddedEvent(ctx, userAgg, emailCode.Code, emailCode.Expiry, "")) } @@ -567,14 +590,14 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. if human.Phone != nil && human.PhoneNumber != "" && !human.IsPhoneVerified { phoneCode, generatorID, err := c.newPhoneCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode, c.userEncryption, c.defaultSecretGenerators.PhoneVerificationCode) //nolint:staticcheck if err != nil { - return nil, nil, err + return nil, nil, nil, err } events = append(events, user.NewHumanPhoneCodeAddedEvent(ctx, userAgg, phoneCode.CryptedCode(), phoneCode.CodeExpiry(), generatorID)) } else if human.Phone != nil && human.PhoneNumber != "" && human.IsPhoneVerified { events = append(events, user.NewHumanPhoneVerifiedEvent(ctx, userAgg)) } - return events, addedHuman, nil + return events, userAgg, addedHuman, nil } func (c *Commands) HumanSkipMFAInit(ctx context.Context, userID, resourceowner string) (err error) { diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go index 78d7248516..1ef3e2aab6 100644 --- a/internal/command/user_human_test.go +++ b/internal/command/user_human_test.go @@ -1200,7 +1200,8 @@ func TestCommandSide_AddHuman(t *testing.T) { }, wantID: "user1", }, - }, { + }, + { name: "add human (with return code), ok", fields: fields{ eventstore: expectEventstore( @@ -1432,6 +1433,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { orgID string human *domain.Human passwordless bool + state *domain.UserState links []*domain.UserIDPLink secretGenerator crypto.Generator passwordlessInitCode crypto.Generator @@ -1584,7 +1586,8 @@ func TestCommandSide_ImportHuman(t *testing.T) { res: res{ err: zerrors.IsErrorInvalidArgument, }, - }, { + }, + { name: "add human (with password and initial code), ok", given: func(t *testing.T) (fields, args) { return fields{ @@ -2985,6 +2988,364 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, }, }, + { + name: "add human (with idp, auto creation not allowed) + deactivated state, ok", + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewOIDCIDPChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + []idp.OIDCIDPChanges{ + idp.ChangeOIDCOptions(idp.OptionChanges{IsCreationAllowed: gu.Ptr(false)}), + }, + ) + return e + }(), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewOIDCIDPChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + []idp.OIDCIDPChanges{ + idp.ChangeOIDCOptions(idp.OptionChanges{ + IsCreationAllowed: gu.Ptr(true), + IsAutoCreation: gu.Ptr(false), + }), + }, + ) + return e + }(), + ), + eventFromEventPusher( + org.NewIdentityProviderAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + domain.IdentityProviderTypeOrg, + ), + ), + ), + expectPush( + newAddHumanEvent("", false, true, "", AllowedLanguage), + user.NewUserIDPLinkAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "idpID", + "name", + "externalID", + ), + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), + user.NewUserDeactivatedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + state: func() *domain.UserState { + state := domain.UserStateInactive + return &state + }(), + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + links: []*domain.UserIDPLink{ + { + IDPConfigID: "idpID", + ExternalUserID: "externalID", + DisplayName: "name", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } + }, + res: res{ + wantHuman: &domain.Human{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + DisplayName: "firstname lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + State: domain.UserStateInactive, + }, + }, + }, + { + name: "add human (with idp, auto creation not allowed) + locked state, ok", + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewOIDCIDPChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + []idp.OIDCIDPChanges{ + idp.ChangeOIDCOptions(idp.OptionChanges{IsCreationAllowed: gu.Ptr(false)}), + }, + ) + return e + }(), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewOIDCIDPChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + []idp.OIDCIDPChanges{ + idp.ChangeOIDCOptions(idp.OptionChanges{ + IsCreationAllowed: gu.Ptr(true), + IsAutoCreation: gu.Ptr(false), + }), + }, + ) + return e + }(), + ), + eventFromEventPusher( + org.NewIdentityProviderAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + domain.IdentityProviderTypeOrg, + ), + ), + ), + expectPush( + newAddHumanEvent("", false, true, "", AllowedLanguage), + user.NewUserIDPLinkAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "idpID", + "name", + "externalID", + ), + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), + user.NewUserLockedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + state: func() *domain.UserState { + state := domain.UserStateLocked + return &state + }(), + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + links: []*domain.UserIDPLink{ + { + IDPConfigID: "idpID", + ExternalUserID: "externalID", + DisplayName: "name", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } + }, + res: res{ + wantHuman: &domain.Human{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + DisplayName: "firstname lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + State: domain.UserStateLocked, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -2996,7 +3357,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { newEncryptedCodeWithDefault: f.newEncryptedCodeWithDefault, defaultSecretGenerators: f.defaultSecretGenerators, } - gotHuman, gotCode, err := r.ImportHuman(a.ctx, a.orgID, a.human, a.passwordless, a.links, a.secretGenerator, a.secretGenerator, a.secretGenerator, a.secretGenerator) + gotHuman, gotCode, err := r.ImportHuman(a.ctx, a.orgID, a.human, a.passwordless, a.state, a.links, a.secretGenerator, a.secretGenerator, a.secretGenerator, a.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/user_machine.go b/internal/command/user_machine.go index 7c8fd89eac..75ed43ee69 100644 --- a/internal/command/user_machine.go +++ b/internal/command/user_machine.go @@ -79,7 +79,7 @@ func AddMachineWithUsernameToIDFallback() addMachineOption { } } -func (c *Commands) AddMachine(ctx context.Context, machine *Machine, check PermissionCheck, options ...addMachineOption) (_ *domain.ObjectDetails, err error) { +func (c *Commands) AddMachine(ctx context.Context, machine *Machine, state *domain.UserState, check PermissionCheck, options ...addMachineOption) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -107,6 +107,29 @@ func (c *Commands) AddMachine(ctx context.Context, machine *Machine, check Permi return nil, err } + if state != nil { + var cmd eventstore.Command + switch *state { + case domain.UserStateInactive: + cmd = user.NewUserDeactivatedEvent(ctx, &agg.Aggregate) + case domain.UserStateLocked: + cmd = user.NewUserLockedEvent(ctx, &agg.Aggregate) + case domain.UserStateDeleted: + // users are never imported if deleted + case domain.UserStateActive: + // added because of the linter + case domain.UserStateSuspend: + // added because of the linter + case domain.UserStateInitial: + // added because of the linter + case domain.UserStateUnspecified: + // added because of the linter + } + if cmd != nil { + cmds = append(cmds, cmd) + } + } + events, err := c.eventstore.Push(ctx, cmds...) if err != nil { return nil, err diff --git a/internal/command/user_machine_test.go b/internal/command/user_machine_test.go index 19548ae9c6..6d94154a42 100644 --- a/internal/command/user_machine_test.go +++ b/internal/command/user_machine_test.go @@ -24,6 +24,7 @@ func TestCommandSide_AddMachine(t *testing.T) { type args struct { ctx context.Context machine *Machine + state *domain.UserState check PermissionCheck options func(*Commands) []addMachineOption } @@ -419,6 +420,112 @@ func TestCommandSide_AddMachine(t *testing.T) { err: zerrors.IsPermissionDenied, }, }, + { + name: "add machine, ok + deactive state", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "name", + "description", + true, + domain.OIDCTokenTypeBearer, + ), + user.NewUserDeactivatedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Description: "description", + Name: "name", + Username: "username", + }, + state: func() *domain.UserState { + state := domain.UserStateInactive + return &state + }(), + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "add machine, ok + locked state", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "name", + "description", + true, + domain.OIDCTokenTypeBearer, + ), + user.NewUserLockedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Description: "description", + Name: "name", + Username: "username", + }, + state: func() *domain.UserState { + state := domain.UserStateLocked + return &state + }(), + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -431,7 +538,7 @@ func TestCommandSide_AddMachine(t *testing.T) { if tt.args.options != nil { options = tt.args.options(r) } - got, err := r.AddMachine(tt.args.ctx, tt.args.machine, tt.args.check, options...) + got, err := r.AddMachine(tt.args.ctx, tt.args.machine, tt.args.state, tt.args.check, options...) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index d8c88d540b..da496b7c7d 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -9005,6 +9005,7 @@ message DataOrg { repeated zitadel.management.v1.SetCustomVerifySMSOTPMessageTextRequest verify_sms_otp_messages = 37; repeated zitadel.management.v1.SetCustomVerifyEmailOTPMessageTextRequest verify_email_otp_messages = 38; repeated zitadel.management.v1.SetCustomInviteUserMessageTextRequest invite_user_messages = 39; + zitadel.org.v1.OrgState org_state = 40; } message ImportDataResponse{ diff --git a/proto/zitadel/v1.proto b/proto/zitadel/v1.proto index c186ea7d61..beb91116f1 100644 --- a/proto/zitadel/v1.proto +++ b/proto/zitadel/v1.proto @@ -172,10 +172,12 @@ message DataOIDCApplication { message DataHumanUser { string user_id = 1; zitadel.management.v1.ImportHumanUserRequest user = 2; + zitadel.user.v1.UserState state = 3; } message DataMachineUser { string user_id = 1; zitadel.management.v1.AddMachineUserRequest user = 2; + zitadel.user.v1.UserState state = 3; } message DataAction { string action_id = 1; From 83839fc2eff533611abb2b0a81c09ae65eff94b0 Mon Sep 17 00:00:00 2001 From: Abhinav Sethi Date: Thu, 12 Jun 2025 13:03:25 -0400 Subject: [PATCH 33/35] fix: enable opentelemetry metrics for river queue (#10044) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Which Problems Are Solved Right now we have no visibility into river queue's job processing times and queue sizes. This makes it difficult to reliably know if notifications are actually being published in a reasonable time and current queue size. # How the Problems Are Solved Integrates River's OpenTelemetry middleware with Zitadel's metrics system by adding the otelriver middleware to the queue configuration. # Additional Changes - Updated dependencies to include required `otelriver` package # Additional Context Example output from `/debug/metrics`
output # HELP failed_deliveries_json_total Failed JSON message deliveries # TYPE failed_deliveries_json_total counter failed_deliveries_json_total{otel_scope_name="",otel_scope_version="",triggering_event_type="user.human.phone.code.added"} 2 # HELP go_gc_duration_seconds A summary of the wall-time pause (stop-the-world) duration in garbage collection cycles. # TYPE go_gc_duration_seconds summary go_gc_duration_seconds{quantile="0"} 3.8e-05 go_gc_duration_seconds{quantile="0.25"} 6.3916e-05 go_gc_duration_seconds{quantile="0.5"} 7.5584e-05 go_gc_duration_seconds{quantile="0.75"} 9.2584e-05 go_gc_duration_seconds{quantile="1"} 0.000204292 go_gc_duration_seconds_sum 0.003028502 go_gc_duration_seconds_count 34 # HELP go_gc_gogc_percent Heap size target percentage configured by the user, otherwise 100. This value is set by the GOGC environment variable, and the runtime/debug.SetGCPercent function. Sourced from /gc/gogc:percent # TYPE go_gc_gogc_percent gauge go_gc_gogc_percent 100 # HELP go_gc_gomemlimit_bytes Go runtime memory limit configured by the user, otherwise math.MaxInt64. This value is set by the GOMEMLIMIT environment variable, and the runtime/debug.SetMemoryLimit function. Sourced from /gc/gomemlimit:bytes # TYPE go_gc_gomemlimit_bytes gauge go_gc_gomemlimit_bytes 9.223372036854776e+18 # HELP go_goroutines Number of goroutines that currently exist. # TYPE go_goroutines gauge go_goroutines 231 # HELP go_info Information about the Go environment. # TYPE go_info gauge go_info{version="go1.24.3"} 1 # HELP go_memstats_alloc_bytes Number of bytes allocated in heap and currently in use. Equals to /memory/classes/heap/objects:bytes. # TYPE go_memstats_alloc_bytes gauge go_memstats_alloc_bytes 7.7565832e+07 # HELP go_memstats_alloc_bytes_total Total number of bytes allocated in heap until now, even if released already. Equals to /gc/heap/allocs:bytes. # TYPE go_memstats_alloc_bytes_total counter go_memstats_alloc_bytes_total 7.3319844e+08 # HELP go_memstats_buck_hash_sys_bytes Number of bytes used by the profiling bucket hash table. Equals to /memory/classes/profiling/buckets:bytes. # TYPE go_memstats_buck_hash_sys_bytes gauge go_memstats_buck_hash_sys_bytes 1.63816e+06 # HELP go_memstats_frees_total Total number of heap objects frees. Equals to /gc/heap/frees:objects + /gc/heap/tiny/allocs:objects. # TYPE go_memstats_frees_total counter go_memstats_frees_total 1.1496925e+07 # HELP go_memstats_gc_sys_bytes Number of bytes used for garbage collection system metadata. Equals to /memory/classes/metadata/other:bytes. # TYPE go_memstats_gc_sys_bytes gauge go_memstats_gc_sys_bytes 5.182776e+06 # HELP go_memstats_heap_alloc_bytes Number of heap bytes allocated and currently in use, same as go_memstats_alloc_bytes. Equals to /memory/classes/heap/objects:bytes. # TYPE go_memstats_heap_alloc_bytes gauge go_memstats_heap_alloc_bytes 7.7565832e+07 # HELP go_memstats_heap_idle_bytes Number of heap bytes waiting to be used. Equals to /memory/classes/heap/released:bytes + /memory/classes/heap/free:bytes. # TYPE go_memstats_heap_idle_bytes gauge go_memstats_heap_idle_bytes 5.8179584e+07 # HELP go_memstats_heap_inuse_bytes Number of heap bytes that are in use. Equals to /memory/classes/heap/objects:bytes + /memory/classes/heap/unused:bytes # TYPE go_memstats_heap_inuse_bytes gauge go_memstats_heap_inuse_bytes 8.5868544e+07 # HELP go_memstats_heap_objects Number of currently allocated objects. Equals to /gc/heap/objects:objects. # TYPE go_memstats_heap_objects gauge go_memstats_heap_objects 573723 # HELP go_memstats_heap_released_bytes Number of heap bytes released to OS. Equals to /memory/classes/heap/released:bytes. # TYPE go_memstats_heap_released_bytes gauge go_memstats_heap_released_bytes 7.20896e+06 # HELP go_memstats_heap_sys_bytes Number of heap bytes obtained from system. Equals to /memory/classes/heap/objects:bytes + /memory/classes/heap/unused:bytes + /memory/classes/heap/released:bytes + /memory/classes/heap/free:bytes. # TYPE go_memstats_heap_sys_bytes gauge go_memstats_heap_sys_bytes 1.44048128e+08 # HELP go_memstats_last_gc_time_seconds Number of seconds since 1970 of last garbage collection. # TYPE go_memstats_last_gc_time_seconds gauge go_memstats_last_gc_time_seconds 1.749491558214289e+09 # HELP go_memstats_mallocs_total Total number of heap objects allocated, both live and gc-ed. Semantically a counter version for go_memstats_heap_objects gauge. Equals to /gc/heap/allocs:objects + /gc/heap/tiny/allocs:objects. # TYPE go_memstats_mallocs_total counter go_memstats_mallocs_total 1.2070648e+07 # HELP go_memstats_mcache_inuse_bytes Number of bytes in use by mcache structures. Equals to /memory/classes/metadata/mcache/inuse:bytes. # TYPE go_memstats_mcache_inuse_bytes gauge go_memstats_mcache_inuse_bytes 16912 # HELP go_memstats_mcache_sys_bytes Number of bytes used for mcache structures obtained from system. Equals to /memory/classes/metadata/mcache/inuse:bytes + /memory/classes/metadata/mcache/free:bytes. # TYPE go_memstats_mcache_sys_bytes gauge go_memstats_mcache_sys_bytes 31408 # HELP go_memstats_mspan_inuse_bytes Number of bytes in use by mspan structures. Equals to /memory/classes/metadata/mspan/inuse:bytes. # TYPE go_memstats_mspan_inuse_bytes gauge go_memstats_mspan_inuse_bytes 1.3496e+06 # HELP go_memstats_mspan_sys_bytes Number of bytes used for mspan structures obtained from system. Equals to /memory/classes/metadata/mspan/inuse:bytes + /memory/classes/metadata/mspan/free:bytes. # TYPE go_memstats_mspan_sys_bytes gauge go_memstats_mspan_sys_bytes 2.18688e+06 # HELP go_memstats_next_gc_bytes Number of heap bytes when next garbage collection will take place. Equals to /gc/heap/goal:bytes. # TYPE go_memstats_next_gc_bytes gauge go_memstats_next_gc_bytes 1.34730994e+08 # HELP go_memstats_other_sys_bytes Number of bytes used for other system allocations. Equals to /memory/classes/other:bytes. # TYPE go_memstats_other_sys_bytes gauge go_memstats_other_sys_bytes 3.125168e+06 # HELP go_memstats_stack_inuse_bytes Number of bytes obtained from system for stack allocator in non-CGO environments. Equals to /memory/classes/heap/stacks:bytes. # TYPE go_memstats_stack_inuse_bytes gauge go_memstats_stack_inuse_bytes 2.752512e+06 # HELP go_memstats_stack_sys_bytes Number of bytes obtained from system for stack allocator. Equals to /memory/classes/heap/stacks:bytes + /memory/classes/os-stacks:bytes. # TYPE go_memstats_stack_sys_bytes gauge go_memstats_stack_sys_bytes 2.752512e+06 # HELP go_memstats_sys_bytes Number of bytes obtained from system. Equals to /memory/classes/total:byte. # TYPE go_memstats_sys_bytes gauge go_memstats_sys_bytes 1.58965032e+08 # HELP go_sched_gomaxprocs_threads The current runtime.GOMAXPROCS setting, or the number of operating system threads that can execute user-level Go code simultaneously. Sourced from /sched/gomaxprocs:threads # TYPE go_sched_gomaxprocs_threads gauge go_sched_gomaxprocs_threads 14 # HELP go_threads Number of OS threads created. # TYPE go_threads gauge go_threads 25 # HELP grpc_server_grpc_status_code_total Grpc status code counter # TYPE grpc_server_grpc_status_code_total counter grpc_server_grpc_status_code_total{grpc_method="/zitadel.management.v1.ManagementService/ListUserChanges",otel_scope_name="",otel_scope_version="",return_code="200"} 1 grpc_server_grpc_status_code_total{grpc_method="/zitadel.management.v1.ManagementService/ListUserMetadata",otel_scope_name="",otel_scope_version="",return_code="200"} 2 grpc_server_grpc_status_code_total{grpc_method="/zitadel.management.v1.ManagementService/ResendHumanPhoneVerification",otel_scope_name="",otel_scope_version="",return_code="200"} 1 grpc_server_grpc_status_code_total{grpc_method="/zitadel.user.v2.UserService/GetUserByID",otel_scope_name="",otel_scope_version="",return_code="200"} 1 # HELP grpc_server_request_counter_total Grpc request counter # TYPE grpc_server_request_counter_total counter grpc_server_request_counter_total{grpc_method="/zitadel.management.v1.ManagementService/ListUserChanges",otel_scope_name="",otel_scope_version=""} 1 grpc_server_request_counter_total{grpc_method="/zitadel.management.v1.ManagementService/ListUserMetadata",otel_scope_name="",otel_scope_version=""} 2 grpc_server_request_counter_total{grpc_method="/zitadel.management.v1.ManagementService/ResendHumanPhoneVerification",otel_scope_name="",otel_scope_version=""} 1 grpc_server_request_counter_total{grpc_method="/zitadel.user.v2.UserService/GetUserByID",otel_scope_name="",otel_scope_version=""} 1 # HELP grpc_server_total_request_counter_total Total grpc request counter # TYPE grpc_server_total_request_counter_total counter grpc_server_total_request_counter_total{otel_scope_name="",otel_scope_version=""} 5 # HELP otel_scope_info Instrumentation Scope metadata # TYPE otel_scope_info gauge otel_scope_info{otel_scope_name="",otel_scope_version=""} 1 otel_scope_info{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version=""} 1 # HELP projection_events_processed_total Number of events reduced to process projection updates # TYPE projection_events_processed_total counter projection_events_processed_total{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",success="true"} 1 projection_events_processed_total{otel_scope_name="",otel_scope_version="",projection="projections.instance_features2",success="true"} 0 projection_events_processed_total{otel_scope_name="",otel_scope_version="",projection="projections.login_names3",success="true"} 0 projection_events_processed_total{otel_scope_name="",otel_scope_version="",projection="projections.notifications",success="true"} 1 projection_events_processed_total{otel_scope_name="",otel_scope_version="",projection="projections.orgs1",success="true"} 0 projection_events_processed_total{otel_scope_name="",otel_scope_version="",projection="projections.user_metadata5",success="true"} 0 projection_events_processed_total{otel_scope_name="",otel_scope_version="",projection="projections.users14",success="true"} 0 # HELP projection_handle_timer_seconds Time taken to process a projection update # TYPE projection_handle_timer_seconds histogram projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="0.005"} 0 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="0.01"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="0.05"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="0.1"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="1"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="5"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="10"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="30"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="60"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="120"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="+Inf"} 1 projection_handle_timer_seconds_sum{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler"} 0.007344541 projection_handle_timer_seconds_count{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="0.005"} 0 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="0.01"} 0 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="0.05"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="0.1"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="1"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="5"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="10"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="30"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="60"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="120"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="+Inf"} 1 projection_handle_timer_seconds_sum{otel_scope_name="",otel_scope_version="",projection="projections.notifications"} 0.014258458 projection_handle_timer_seconds_count{otel_scope_name="",otel_scope_version="",projection="projections.notifications"} 1 # HELP projection_state_latency_seconds When finishing processing a batch of events, this track the age of the last events seen from current time # TYPE projection_state_latency_seconds histogram projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="0.1"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="0.5"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="1"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="5"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="10"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="30"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="60"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="300"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="600"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="1800"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="+Inf"} 1 projection_state_latency_seconds_sum{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler"} 0.012979 projection_state_latency_seconds_count{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="0.1"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="0.5"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="1"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="5"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="10"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="30"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="60"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="300"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="600"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="1800"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="+Inf"} 1 projection_state_latency_seconds_sum{otel_scope_name="",otel_scope_version="",projection="projections.notifications"} 0.0199 projection_state_latency_seconds_count{otel_scope_name="",otel_scope_version="",projection="projections.notifications"} 1 # HELP promhttp_metric_handler_requests_in_flight Current number of scrapes being served. # TYPE promhttp_metric_handler_requests_in_flight gauge promhttp_metric_handler_requests_in_flight 1 # HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code. # TYPE promhttp_metric_handler_requests_total counter promhttp_metric_handler_requests_total{code="200"} 1 promhttp_metric_handler_requests_total{code="500"} 0 promhttp_metric_handler_requests_total{code="503"} 0 # HELP river_insert_count_total Number of jobs inserted # TYPE river_insert_count_total counter river_insert_count_total{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok"} 1 # HELP river_insert_many_count_total Number of job batches inserted (all jobs are inserted in a batch, but batches may be one job) # TYPE river_insert_many_count_total counter river_insert_many_count_total{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok"} 1 # HELP river_insert_many_duration_histogram_seconds Duration of job batch insertion (histogram) # TYPE river_insert_many_duration_histogram_seconds histogram river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="0"} 0 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="5"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="10"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="25"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="50"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="75"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="100"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="250"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="500"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="750"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="1000"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="2500"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="5000"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="7500"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="10000"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="+Inf"} 1 river_insert_many_duration_histogram_seconds_sum{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok"} 0.002905666 river_insert_many_duration_histogram_seconds_count{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok"} 1 # HELP river_insert_many_duration_seconds Duration of job batch insertion # TYPE river_insert_many_duration_seconds gauge river_insert_many_duration_seconds{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok"} 0.002905666 # HELP river_work_count_total Number of jobs worked # TYPE river_work_count_total counter river_work_count_total{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]"} 1 river_work_count_total{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]"} 1 # HELP river_work_duration_histogram_seconds Duration of job being worked (histogram) # TYPE river_work_duration_histogram_seconds histogram river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="0"} 0 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="5"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="10"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="25"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="50"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="75"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="100"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="250"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="500"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="750"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="1000"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="2500"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="5000"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="7500"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="10000"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="+Inf"} 1 river_work_duration_histogram_seconds_sum{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]"} 0.029241083 river_work_duration_histogram_seconds_count{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="0"} 0 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="5"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="10"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="25"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="50"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="75"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="100"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="250"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="500"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="750"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="1000"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="2500"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="5000"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="7500"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="10000"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="+Inf"} 1 river_work_duration_histogram_seconds_sum{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]"} 0.0408745 river_work_duration_histogram_seconds_count{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]"} 1 # HELP river_work_duration_seconds Duration of job being worked # TYPE river_work_duration_seconds gauge river_work_duration_seconds{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]"} 0.029241083 river_work_duration_seconds{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]"} 0.0408745 # HELP target_info Target metadata # TYPE target_info gauge target_info{service_name="ZITADEL",service_version="2025-06-09T13:52:29-04:00",telemetry_sdk_language="go",telemetry_sdk_name="opentelemetry",telemetry_sdk_version="1.35.0"} 1
Example grafana dashboard: ![Screenshot 2025-06-11 at 11 30 06 AM](https://github.com/user-attachments/assets/a2c9b377-8ddd-40b9-a506-7df3b31941da) - Closes #10043 --------- Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com> --- go.mod | 1 + go.sum | 2 ++ internal/queue/queue.go | 7 +++++++ 3 files changed, 10 insertions(+) diff --git a/go.mod b/go.mod index 21a7fe9f16..15b3d2b391 100644 --- a/go.mod +++ b/go.mod @@ -146,6 +146,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/riverqueue/river/rivershared v0.22.0 // indirect + github.com/riverqueue/rivercontrib/otelriver v0.5.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect diff --git a/go.sum b/go.sum index cc3bc35841..272ce655a3 100644 --- a/go.sum +++ b/go.sum @@ -682,6 +682,8 @@ github.com/riverqueue/river/rivershared v0.22.0 h1:hLPHr98d6OEfmUJ4KpIXgoy2tbQ14 github.com/riverqueue/river/rivershared v0.22.0/go.mod h1:BK+hvhECfdDLWNDH3xiGI95m2YoPfVtECZLT+my8XM8= github.com/riverqueue/river/rivertype v0.22.0 h1:rSRhbd5uV/BaFTPxReCxuYTAzx+/riBZJlZdREADvO4= github.com/riverqueue/river/rivertype v0.22.0/go.mod h1:lmdl3vLNDfchDWbYdW2uAocIuwIN+ZaXqAukdSCFqWs= +github.com/riverqueue/rivercontrib/otelriver v0.5.0 h1:dZF4Fy7/3RaIRsyCPdpIJtzEip0pCvoJ44YpSDum8e4= +github.com/riverqueue/rivercontrib/otelriver v0.5.0/go.mod h1:rXANcBrlgRvg+auD3/O6Xfs59AWeWNpa/kim62mkxGo= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= diff --git a/internal/queue/queue.go b/internal/queue/queue.go index d680221753..22df8c2b5c 100644 --- a/internal/queue/queue.go +++ b/internal/queue/queue.go @@ -7,9 +7,12 @@ import ( "github.com/riverqueue/river" "github.com/riverqueue/river/riverdriver" "github.com/riverqueue/river/riverdriver/riverpgxv5" + "github.com/riverqueue/river/rivertype" + "github.com/riverqueue/rivercontrib/otelriver" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/telemetry/metrics" ) // Queue abstracts the underlying queuing library @@ -27,12 +30,16 @@ type Config struct { } func NewQueue(config *Config) (_ *Queue, err error) { + middleware := []rivertype.Middleware{otelriver.NewMiddleware(&otelriver.MiddlewareConfig{ + MeterProvider: metrics.GetMetricsProvider(), + })} return &Queue{ driver: riverpgxv5.New(config.Client.Pool), config: &river.Config{ Workers: river.NewWorkers(), Queues: make(map[string]river.QueueConfig), JobTimeout: -1, + Middleware: middleware, }, }, nil } From cddbd3dd47d559dde2e2deb63be25bc93137826b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabienne=20B=C3=BChler?= Date: Tue, 17 Jun 2025 15:20:44 +0200 Subject: [PATCH 34/35] docs: Correct API docs of unlock user (#10064) # Which Problems Are Solved The API docs of unlock user show the description of the lock user. # How the Problems Are Solved Correct API docs for unlock user are added --- proto/zitadel/user/v2/user_service.proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto/zitadel/user/v2/user_service.proto b/proto/zitadel/user/v2/user_service.proto index 3fc81836d6..a416555905 100644 --- a/proto/zitadel/user/v2/user_service.proto +++ b/proto/zitadel/user/v2/user_service.proto @@ -701,7 +701,7 @@ service UserService { // Unlock user // - // The state of the user will be changed to 'locked'. The user will not be able to log in anymore. The endpoint returns an error if the user is already in the state 'locked'. Use this endpoint if the user should not be able to log in temporarily because of an event that happened (wrong password, etc.).. + // The state of the user will be changed to 'active'. The user will be able to log in again. The endpoint returns an error if the user is not in the state 'locked'. rpc UnlockUser(UnlockUserRequest) returns (UnlockUserResponse) { option (google.api.http) = { post: "/v2/users/{user_id}/unlock" From 28f7218ea1068a4bc7587166ce3a17af20ff4156 Mon Sep 17 00:00:00 2001 From: "Marco A." Date: Wed, 18 Jun 2025 13:24:39 +0200 Subject: [PATCH 35/35] feat: Hosted login translation API (#10011) # Which Problems Are Solved This PR implements https://github.com/zitadel/zitadel/issues/9850 # How the Problems Are Solved - New protobuf definition - Implementation of retrieval of system translations - Implementation of retrieval and persistence of organization and instance level translations # Additional Context - Closes #9850 # TODO - [x] Integration tests for Get and Set hosted login translation endpoints - [x] DB migration test - [x] Command function tests - [x] Command util functions tests - [x] Query function test - [x] Query util functions tests --- go.mod | 3 +- go.sum | 2 + internal/api/authz/context_mock.go | 24 +- .../v2/integration_test/query_test.go | 432 +++++ .../v2/integration_test/server_test.go | 9 +- .../v2/integration_test/settings_test.go | 371 +--- internal/api/grpc/settings/v2/query.go | 209 +++ internal/api/grpc/settings/v2/settings.go | 201 +-- internal/command/hosted_login_translation.go | 73 + .../command/hosted_login_translation_model.go | 45 + .../command/hosted_login_translation_test.go | 211 +++ internal/database/mock/sql_mock.go | 12 +- internal/query/hosted_login_translation.go | 256 +++ .../query/hosted_login_translation_test.go | 337 ++++ .../projection/hosted_login_translation.go | 144 ++ internal/query/projection/projection.go | 3 + internal/query/v2-default.json | 1557 +++++++++++++++++ internal/repository/instance/eventstore.go | 1 + .../instance/hosted_login_translation.go | 55 + internal/repository/org/eventstore.go | 1 + .../org/hosted_login_translation.go | 55 + proto/zitadel/settings/v2/settings.proto | 2 +- .../settings/v2/settings_service.proto | 137 ++ 23 files changed, 3613 insertions(+), 527 deletions(-) create mode 100644 internal/api/grpc/settings/v2/integration_test/query_test.go create mode 100644 internal/api/grpc/settings/v2/query.go create mode 100644 internal/command/hosted_login_translation.go create mode 100644 internal/command/hosted_login_translation_model.go create mode 100644 internal/command/hosted_login_translation_test.go create mode 100644 internal/query/hosted_login_translation.go create mode 100644 internal/query/hosted_login_translation_test.go create mode 100644 internal/query/projection/hosted_login_translation.go create mode 100644 internal/query/v2-default.json create mode 100644 internal/repository/instance/hosted_login_translation.go create mode 100644 internal/repository/org/hosted_login_translation.go diff --git a/go.mod b/go.mod index 15b3d2b391..9d02050b48 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ toolchain go1.24.1 require ( cloud.google.com/go/profiler v0.4.2 cloud.google.com/go/storage v1.54.0 + dario.cat/mergo v1.0.2 github.com/BurntSushi/toml v1.5.0 github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.27.0 @@ -65,6 +66,7 @@ require ( github.com/riverqueue/river v0.22.0 github.com/riverqueue/river/riverdriver v0.22.0 github.com/riverqueue/river/rivertype v0.22.0 + github.com/riverqueue/rivercontrib/otelriver v0.5.0 github.com/rs/cors v1.11.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/shopspring/decimal v1.3.1 @@ -146,7 +148,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/riverqueue/river/rivershared v0.22.0 // indirect - github.com/riverqueue/rivercontrib/otelriver v0.5.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect diff --git a/go.sum b/go.sum index 272ce655a3..e2ab9768a6 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,8 @@ cloud.google.com/go/storage v1.54.0 h1:Du3XEyliAiftfyW0bwfdppm2MMLdpVAfiIg4T2nAI cloud.google.com/go/storage v1.54.0/go.mod h1:hIi9Boe8cHxTyaeqh7KMMwKg088VblFK46C2x/BWaZE= cloud.google.com/go/trace v1.11.3 h1:c+I4YFjxRQjvAhRmSsmjpASUKq88chOX854ied0K/pE= cloud.google.com/go/trace v1.11.3/go.mod h1:pt7zCYiDSQjC9Y2oqCsh9jF4GStB/hmjrYLsxRR27q8= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= diff --git a/internal/api/authz/context_mock.go b/internal/api/authz/context_mock.go index 6891030bd3..d26b371bc6 100644 --- a/internal/api/authz/context_mock.go +++ b/internal/api/authz/context_mock.go @@ -1,10 +1,28 @@ package authz -import "context" +import ( + "context" -func NewMockContext(instanceID, orgID, userID string) context.Context { + "golang.org/x/text/language" +) + +type MockContextInstanceOpts func(i *instance) + +func WithMockDefaultLanguage(lang language.Tag) MockContextInstanceOpts { + return func(i *instance) { + i.defaultLanguage = lang + } +} + +func NewMockContext(instanceID, orgID, userID string, opts ...MockContextInstanceOpts) context.Context { ctx := context.WithValue(context.Background(), dataKey, CtxData{UserID: userID, OrgID: orgID}) - return context.WithValue(ctx, instanceKey, &instance{id: instanceID}) + + i := &instance{id: instanceID} + for _, o := range opts { + o(i) + } + + return context.WithValue(ctx, instanceKey, i) } func NewMockContextWithAgent(instanceID, orgID, userID, agentID string) context.Context { diff --git a/internal/api/grpc/settings/v2/integration_test/query_test.go b/internal/api/grpc/settings/v2/integration_test/query_test.go new file mode 100644 index 0000000000..c3bf54e992 --- /dev/null +++ b/internal/api/grpc/settings/v2/integration_test/query_test.go @@ -0,0 +1,432 @@ +//go:build integration + +package settings_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/idp" + idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" +) + +func TestServer_GetSecuritySettings(t *testing.T) { + _, err := Client.SetSecuritySettings(AdminCTX, &settings.SetSecuritySettingsRequest{ + EmbeddedIframe: &settings.EmbeddedIframeSettings{ + Enabled: true, + AllowedOrigins: []string{"foo", "bar"}, + }, + EnableImpersonation: true, + }) + require.NoError(t, err) + + tests := []struct { + name string + ctx context.Context + want *settings.GetSecuritySettingsResponse + wantErr bool + }{ + { + name: "permission error", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + wantErr: true, + }, + { + name: "success", + ctx: AdminCTX, + want: &settings.GetSecuritySettingsResponse{ + Settings: &settings.SecuritySettings{ + EmbeddedIframe: &settings.EmbeddedIframeSettings{ + Enabled: true, + AllowedOrigins: []string{"foo", "bar"}, + }, + EnableImpersonation: true, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute) + assert.EventuallyWithT(t, func(ct *assert.CollectT) { + resp, err := Client.GetSecuritySettings(tt.ctx, &settings.GetSecuritySettingsRequest{}) + if tt.wantErr { + assert.Error(ct, err) + return + } + if !assert.NoError(ct, err) { + return + } + got, want := resp.GetSettings(), tt.want.GetSettings() + assert.Equal(ct, want.GetEmbeddedIframe().GetEnabled(), got.GetEmbeddedIframe().GetEnabled(), "enable iframe embedding") + assert.Equal(ct, want.GetEmbeddedIframe().GetAllowedOrigins(), got.GetEmbeddedIframe().GetAllowedOrigins(), "allowed origins") + assert.Equal(ct, want.GetEnableImpersonation(), got.GetEnableImpersonation(), "enable impersonation") + }, retryDuration, tick) + }) + } +} + +func idpResponse(id, name string, linking, creation, autoCreation, autoUpdate bool, autoLinking idp_pb.AutoLinkingOption) *settings.IdentityProvider { + return &settings.IdentityProvider{ + Id: id, + Name: name, + Type: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OAUTH, + Options: &idp_pb.Options{ + IsLinkingAllowed: linking, + IsCreationAllowed: creation, + IsAutoCreation: autoCreation, + IsAutoUpdate: autoUpdate, + AutoLinking: autoLinking, + }, + } +} + +func TestServer_GetActiveIdentityProviders(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + instance.AddGenericOAuthProvider(isolatedIAMOwnerCTX, gofakeit.AppName()) // inactive + idpActiveName := gofakeit.AppName() + idpActiveResp := instance.AddGenericOAuthProvider(isolatedIAMOwnerCTX, idpActiveName) + instance.AddProviderToDefaultLoginPolicy(isolatedIAMOwnerCTX, idpActiveResp.GetId()) + idpActiveResponse := idpResponse(idpActiveResp.GetId(), idpActiveName, true, true, true, true, idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) + idpLinkingDisallowedName := gofakeit.AppName() + idpLinkingDisallowedResp := instance.AddGenericOAuthProviderWithOptions(isolatedIAMOwnerCTX, idpLinkingDisallowedName, false, true, true, idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) + instance.AddProviderToDefaultLoginPolicy(isolatedIAMOwnerCTX, idpLinkingDisallowedResp.GetId()) + idpLinkingDisallowedResponse := idpResponse(idpLinkingDisallowedResp.GetId(), idpLinkingDisallowedName, false, true, true, true, idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) + idpCreationDisallowedName := gofakeit.AppName() + idpCreationDisallowedResp := instance.AddGenericOAuthProviderWithOptions(isolatedIAMOwnerCTX, idpCreationDisallowedName, true, false, true, idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) + instance.AddProviderToDefaultLoginPolicy(isolatedIAMOwnerCTX, idpCreationDisallowedResp.GetId()) + idpCreationDisallowedResponse := idpResponse(idpCreationDisallowedResp.GetId(), idpCreationDisallowedName, true, false, true, true, idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) + idpNoAutoCreationName := gofakeit.AppName() + idpNoAutoCreationResp := instance.AddGenericOAuthProviderWithOptions(isolatedIAMOwnerCTX, idpNoAutoCreationName, true, true, false, idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) + instance.AddProviderToDefaultLoginPolicy(isolatedIAMOwnerCTX, idpNoAutoCreationResp.GetId()) + idpNoAutoCreationResponse := idpResponse(idpNoAutoCreationResp.GetId(), idpNoAutoCreationName, true, true, false, true, idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) + idpNoAutoLinkingName := gofakeit.AppName() + idpNoAutoLinkingResp := instance.AddGenericOAuthProviderWithOptions(isolatedIAMOwnerCTX, idpNoAutoLinkingName, true, true, true, idp.AutoLinkingOption_AUTO_LINKING_OPTION_UNSPECIFIED) + instance.AddProviderToDefaultLoginPolicy(isolatedIAMOwnerCTX, idpNoAutoLinkingResp.GetId()) + idpNoAutoLinkingResponse := idpResponse(idpNoAutoLinkingResp.GetId(), idpNoAutoLinkingName, true, true, true, true, idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_UNSPECIFIED) + + type args struct { + ctx context.Context + req *settings.GetActiveIdentityProvidersRequest + } + tests := []struct { + name string + args args + want *settings.GetActiveIdentityProvidersResponse + wantErr bool + }{ + { + name: "permission error", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &settings.GetActiveIdentityProvidersRequest{}, + }, + wantErr: true, + }, + { + name: "success, all", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &settings.GetActiveIdentityProvidersRequest{}, + }, + want: &settings.GetActiveIdentityProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 5, + Timestamp: timestamppb.Now(), + }, + IdentityProviders: []*settings.IdentityProvider{ + idpActiveResponse, + idpLinkingDisallowedResponse, + idpCreationDisallowedResponse, + idpNoAutoCreationResponse, + idpNoAutoLinkingResponse, + }, + }, + }, + { + name: "success, exclude linking disallowed", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &settings.GetActiveIdentityProvidersRequest{ + LinkingAllowed: gu.Ptr(true), + }, + }, + want: &settings.GetActiveIdentityProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 4, + Timestamp: timestamppb.Now(), + }, + IdentityProviders: []*settings.IdentityProvider{ + idpActiveResponse, + idpCreationDisallowedResponse, + idpNoAutoCreationResponse, + idpNoAutoLinkingResponse, + }, + }, + }, + { + name: "success, only linking disallowed", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &settings.GetActiveIdentityProvidersRequest{ + LinkingAllowed: gu.Ptr(false), + }, + }, + want: &settings.GetActiveIdentityProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + IdentityProviders: []*settings.IdentityProvider{ + idpLinkingDisallowedResponse, + }, + }, + }, + { + name: "success, exclude creation disallowed", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &settings.GetActiveIdentityProvidersRequest{ + CreationAllowed: gu.Ptr(true), + }, + }, + want: &settings.GetActiveIdentityProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 4, + Timestamp: timestamppb.Now(), + }, + IdentityProviders: []*settings.IdentityProvider{ + idpActiveResponse, + idpLinkingDisallowedResponse, + idpNoAutoCreationResponse, + idpNoAutoLinkingResponse, + }, + }, + }, + { + name: "success, only creation disallowed", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &settings.GetActiveIdentityProvidersRequest{ + CreationAllowed: gu.Ptr(false), + }, + }, + want: &settings.GetActiveIdentityProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + IdentityProviders: []*settings.IdentityProvider{ + idpCreationDisallowedResponse, + }, + }, + }, + { + name: "success, auto creation", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &settings.GetActiveIdentityProvidersRequest{ + AutoCreation: gu.Ptr(true), + }, + }, + want: &settings.GetActiveIdentityProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 4, + Timestamp: timestamppb.Now(), + }, + IdentityProviders: []*settings.IdentityProvider{ + idpActiveResponse, + idpLinkingDisallowedResponse, + idpCreationDisallowedResponse, + idpNoAutoLinkingResponse, + }, + }, + }, + { + name: "success, no auto creation", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &settings.GetActiveIdentityProvidersRequest{ + AutoCreation: gu.Ptr(false), + }, + }, + want: &settings.GetActiveIdentityProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + IdentityProviders: []*settings.IdentityProvider{ + idpNoAutoCreationResponse, + }, + }, + }, + { + name: "success, auto linking", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &settings.GetActiveIdentityProvidersRequest{ + AutoLinking: gu.Ptr(true), + }, + }, + want: &settings.GetActiveIdentityProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 4, + Timestamp: timestamppb.Now(), + }, + IdentityProviders: []*settings.IdentityProvider{ + idpActiveResponse, + idpLinkingDisallowedResponse, + idpCreationDisallowedResponse, + idpNoAutoCreationResponse, + }, + }, + }, + { + name: "success, no auto linking", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &settings.GetActiveIdentityProvidersRequest{ + AutoLinking: gu.Ptr(false), + }, + }, + want: &settings.GetActiveIdentityProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + IdentityProviders: []*settings.IdentityProvider{ + idpNoAutoLinkingResponse, + }, + }, + }, + { + name: "success, exclude all", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &settings.GetActiveIdentityProvidersRequest{ + LinkingAllowed: gu.Ptr(true), + CreationAllowed: gu.Ptr(true), + AutoCreation: gu.Ptr(true), + AutoLinking: gu.Ptr(true), + }, + }, + want: &settings.GetActiveIdentityProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + IdentityProviders: []*settings.IdentityProvider{ + idpActiveResponse, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute) + assert.EventuallyWithT(t, func(ct *assert.CollectT) { + got, err := instance.Client.SettingsV2.GetActiveIdentityProviders(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(ct, err) + return + } + if !assert.NoError(ct, err) { + return + } + for i, result := range tt.want.GetIdentityProviders() { + assert.EqualExportedValues(ct, result, got.GetIdentityProviders()[i]) + } + integration.AssertListDetails(ct, tt.want, got) + }, retryDuration, tick) + }) + } +} + +func TestServer_GetHostedLoginTranslation(t *testing.T) { + // Given + translations := map[string]any{"loginTitle": gofakeit.Slogan()} + + protoTranslations, err := structpb.NewStruct(translations) + require.NoError(t, err) + + setupRequest := &settings.SetHostedLoginTranslationRequest{ + Level: &settings.SetHostedLoginTranslationRequest_OrganizationId{ + OrganizationId: Instance.DefaultOrg.GetId(), + }, + Translations: protoTranslations, + Locale: gofakeit.LanguageBCP(), + } + savedTranslation, err := Client.SetHostedLoginTranslation(AdminCTX, setupRequest) + require.NoError(t, err) + + tt := []struct { + testName string + inputCtx context.Context + inputRequest *settings.GetHostedLoginTranslationRequest + + expectedErrorCode codes.Code + expectedErrorMsg string + expectedResponse *settings.GetHostedLoginTranslationResponse + }{ + { + testName: "when unauthN context should return unauthN error", + inputCtx: CTX, + inputRequest: &settings.GetHostedLoginTranslationRequest{Locale: "en-US"}, + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputCtx: OrgOwnerCtx, + inputRequest: &settings.GetHostedLoginTranslationRequest{Locale: "en-US"}, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when authZ request should save to db and return etag", + inputCtx: AdminCTX, + inputRequest: &settings.GetHostedLoginTranslationRequest{ + Level: &settings.GetHostedLoginTranslationRequest_OrganizationId{ + OrganizationId: Instance.DefaultOrg.GetId(), + }, + Locale: setupRequest.GetLocale(), + }, + expectedResponse: &settings.GetHostedLoginTranslationResponse{ + Etag: savedTranslation.GetEtag(), + Translations: protoTranslations, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // When + res, err := Client.GetHostedLoginTranslation(tc.inputCtx, tc.inputRequest) + + // Then + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + require.NoError(t, err) + assert.NotEmpty(t, res.GetEtag()) + assert.NotEmpty(t, res.GetTranslations().GetFields()) + } + }) + } +} diff --git a/internal/api/grpc/settings/v2/integration_test/server_test.go b/internal/api/grpc/settings/v2/integration_test/server_test.go index d57e2a7694..c5c851c310 100644 --- a/internal/api/grpc/settings/v2/integration_test/server_test.go +++ b/internal/api/grpc/settings/v2/integration_test/server_test.go @@ -13,9 +13,9 @@ import ( ) var ( - CTX, AdminCTX context.Context - Instance *integration.Instance - Client settings.SettingsServiceClient + CTX, AdminCTX, UserTypeLoginCtx, OrgOwnerCtx context.Context + Instance *integration.Instance + Client settings.SettingsServiceClient ) func TestMain(m *testing.M) { @@ -27,6 +27,9 @@ func TestMain(m *testing.M) { CTX = ctx AdminCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner) + UserTypeLoginCtx = Instance.WithAuthorization(ctx, integration.UserTypeLogin) + OrgOwnerCtx = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner) + Client = Instance.Client.SettingsV2 return m.Run() }()) diff --git a/internal/api/grpc/settings/v2/integration_test/settings_test.go b/internal/api/grpc/settings/v2/integration_test/settings_test.go index 3430eae5f8..7d1e4b0239 100644 --- a/internal/api/grpc/settings/v2/integration_test/settings_test.go +++ b/internal/api/grpc/settings/v2/integration_test/settings_test.go @@ -4,78 +4,23 @@ package settings_test import ( "context" + "crypto/md5" + "encoding/hex" + "fmt" "testing" - "time" - "github.com/brianvoe/gofakeit/v6" - "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" - "github.com/zitadel/zitadel/pkg/grpc/idp" - idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2" object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2" "github.com/zitadel/zitadel/pkg/grpc/settings/v2" ) -func TestServer_GetSecuritySettings(t *testing.T) { - _, err := Client.SetSecuritySettings(AdminCTX, &settings.SetSecuritySettingsRequest{ - EmbeddedIframe: &settings.EmbeddedIframeSettings{ - Enabled: true, - AllowedOrigins: []string{"foo", "bar"}, - }, - EnableImpersonation: true, - }) - require.NoError(t, err) - - tests := []struct { - name string - ctx context.Context - want *settings.GetSecuritySettingsResponse - wantErr bool - }{ - { - name: "permission error", - ctx: Instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), - wantErr: true, - }, - { - name: "success", - ctx: AdminCTX, - want: &settings.GetSecuritySettingsResponse{ - Settings: &settings.SecuritySettings{ - EmbeddedIframe: &settings.EmbeddedIframeSettings{ - Enabled: true, - AllowedOrigins: []string{"foo", "bar"}, - }, - EnableImpersonation: true, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute) - assert.EventuallyWithT(t, func(ct *assert.CollectT) { - resp, err := Client.GetSecuritySettings(tt.ctx, &settings.GetSecuritySettingsRequest{}) - if tt.wantErr { - assert.Error(ct, err) - return - } - if !assert.NoError(ct, err) { - return - } - got, want := resp.GetSettings(), tt.want.GetSettings() - assert.Equal(ct, want.GetEmbeddedIframe().GetEnabled(), got.GetEmbeddedIframe().GetEnabled(), "enable iframe embedding") - assert.Equal(ct, want.GetEmbeddedIframe().GetAllowedOrigins(), got.GetEmbeddedIframe().GetAllowedOrigins(), "allowed origins") - assert.Equal(ct, want.GetEnableImpersonation(), got.GetEnableImpersonation(), "enable impersonation") - }, retryDuration, tick) - }) - } -} - func TestServer_SetSecuritySettings(t *testing.T) { type args struct { ctx context.Context @@ -183,280 +128,64 @@ func TestServer_SetSecuritySettings(t *testing.T) { } } -func idpResponse(id, name string, linking, creation, autoCreation, autoUpdate bool, autoLinking idp_pb.AutoLinkingOption) *settings.IdentityProvider { - return &settings.IdentityProvider{ - Id: id, - Name: name, - Type: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OAUTH, - Options: &idp_pb.Options{ - IsLinkingAllowed: linking, - IsCreationAllowed: creation, - IsAutoCreation: autoCreation, - IsAutoUpdate: autoUpdate, - AutoLinking: autoLinking, - }, - } -} +func TestSetHostedLoginTranslation(t *testing.T) { + translations := map[string]any{"loginTitle": "Welcome to our service"} -func TestServer_GetActiveIdentityProviders(t *testing.T) { - instance := integration.NewInstance(CTX) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + protoTranslations, err := structpb.NewStruct(translations) + require.Nil(t, err) - instance.AddGenericOAuthProvider(isolatedIAMOwnerCTX, gofakeit.AppName()) // inactive - idpActiveName := gofakeit.AppName() - idpActiveResp := instance.AddGenericOAuthProvider(isolatedIAMOwnerCTX, idpActiveName) - instance.AddProviderToDefaultLoginPolicy(isolatedIAMOwnerCTX, idpActiveResp.GetId()) - idpActiveResponse := idpResponse(idpActiveResp.GetId(), idpActiveName, true, true, true, true, idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) - idpLinkingDisallowedName := gofakeit.AppName() - idpLinkingDisallowedResp := instance.AddGenericOAuthProviderWithOptions(isolatedIAMOwnerCTX, idpLinkingDisallowedName, false, true, true, idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) - instance.AddProviderToDefaultLoginPolicy(isolatedIAMOwnerCTX, idpLinkingDisallowedResp.GetId()) - idpLinkingDisallowedResponse := idpResponse(idpLinkingDisallowedResp.GetId(), idpLinkingDisallowedName, false, true, true, true, idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) - idpCreationDisallowedName := gofakeit.AppName() - idpCreationDisallowedResp := instance.AddGenericOAuthProviderWithOptions(isolatedIAMOwnerCTX, idpCreationDisallowedName, true, false, true, idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) - instance.AddProviderToDefaultLoginPolicy(isolatedIAMOwnerCTX, idpCreationDisallowedResp.GetId()) - idpCreationDisallowedResponse := idpResponse(idpCreationDisallowedResp.GetId(), idpCreationDisallowedName, true, false, true, true, idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) - idpNoAutoCreationName := gofakeit.AppName() - idpNoAutoCreationResp := instance.AddGenericOAuthProviderWithOptions(isolatedIAMOwnerCTX, idpNoAutoCreationName, true, true, false, idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) - instance.AddProviderToDefaultLoginPolicy(isolatedIAMOwnerCTX, idpNoAutoCreationResp.GetId()) - idpNoAutoCreationResponse := idpResponse(idpNoAutoCreationResp.GetId(), idpNoAutoCreationName, true, true, false, true, idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) - idpNoAutoLinkingName := gofakeit.AppName() - idpNoAutoLinkingResp := instance.AddGenericOAuthProviderWithOptions(isolatedIAMOwnerCTX, idpNoAutoLinkingName, true, true, true, idp.AutoLinkingOption_AUTO_LINKING_OPTION_UNSPECIFIED) - instance.AddProviderToDefaultLoginPolicy(isolatedIAMOwnerCTX, idpNoAutoLinkingResp.GetId()) - idpNoAutoLinkingResponse := idpResponse(idpNoAutoLinkingResp.GetId(), idpNoAutoLinkingName, true, true, true, true, idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_UNSPECIFIED) + hash := md5.Sum(fmt.Append(nil, translations)) - type args struct { - ctx context.Context - req *settings.GetActiveIdentityProvidersRequest - } - tests := []struct { - name string - args args - want *settings.GetActiveIdentityProvidersResponse - wantErr bool + tt := []struct { + testName string + inputCtx context.Context + inputRequest *settings.SetHostedLoginTranslationRequest + + expectedErrorCode codes.Code + expectedErrorMsg string + expectedResponse *settings.SetHostedLoginTranslationResponse }{ { - name: "permission error", - args: args{ - ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), - req: &settings.GetActiveIdentityProvidersRequest{}, - }, - wantErr: true, + testName: "when unauthN context should return unauthN error", + inputCtx: CTX, + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", }, { - name: "success, all", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &settings.GetActiveIdentityProvidersRequest{}, - }, - want: &settings.GetActiveIdentityProvidersResponse{ - Details: &object_pb.ListDetails{ - TotalResult: 5, - Timestamp: timestamppb.Now(), - }, - IdentityProviders: []*settings.IdentityProvider{ - idpActiveResponse, - idpLinkingDisallowedResponse, - idpCreationDisallowedResponse, - idpNoAutoCreationResponse, - idpNoAutoLinkingResponse, - }, - }, + testName: "when unauthZ context should return unauthZ error", + inputCtx: UserTypeLoginCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", }, { - name: "success, exclude linking disallowed", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &settings.GetActiveIdentityProvidersRequest{ - LinkingAllowed: gu.Ptr(true), + testName: "when authZ request should save to db and return etag", + inputCtx: AdminCTX, + inputRequest: &settings.SetHostedLoginTranslationRequest{ + Level: &settings.SetHostedLoginTranslationRequest_OrganizationId{ + OrganizationId: Instance.DefaultOrg.GetId(), }, + Translations: protoTranslations, + Locale: "en-US", }, - want: &settings.GetActiveIdentityProvidersResponse{ - Details: &object_pb.ListDetails{ - TotalResult: 4, - Timestamp: timestamppb.Now(), - }, - IdentityProviders: []*settings.IdentityProvider{ - idpActiveResponse, - idpCreationDisallowedResponse, - idpNoAutoCreationResponse, - idpNoAutoLinkingResponse, - }, - }, - }, - { - name: "success, only linking disallowed", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &settings.GetActiveIdentityProvidersRequest{ - LinkingAllowed: gu.Ptr(false), - }, - }, - want: &settings.GetActiveIdentityProvidersResponse{ - Details: &object_pb.ListDetails{ - TotalResult: 1, - Timestamp: timestamppb.Now(), - }, - IdentityProviders: []*settings.IdentityProvider{ - idpLinkingDisallowedResponse, - }, - }, - }, - { - name: "success, exclude creation disallowed", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &settings.GetActiveIdentityProvidersRequest{ - CreationAllowed: gu.Ptr(true), - }, - }, - want: &settings.GetActiveIdentityProvidersResponse{ - Details: &object_pb.ListDetails{ - TotalResult: 4, - Timestamp: timestamppb.Now(), - }, - IdentityProviders: []*settings.IdentityProvider{ - idpActiveResponse, - idpLinkingDisallowedResponse, - idpNoAutoCreationResponse, - idpNoAutoLinkingResponse, - }, - }, - }, - { - name: "success, only creation disallowed", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &settings.GetActiveIdentityProvidersRequest{ - CreationAllowed: gu.Ptr(false), - }, - }, - want: &settings.GetActiveIdentityProvidersResponse{ - Details: &object_pb.ListDetails{ - TotalResult: 1, - Timestamp: timestamppb.Now(), - }, - IdentityProviders: []*settings.IdentityProvider{ - idpCreationDisallowedResponse, - }, - }, - }, - { - name: "success, auto creation", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &settings.GetActiveIdentityProvidersRequest{ - AutoCreation: gu.Ptr(true), - }, - }, - want: &settings.GetActiveIdentityProvidersResponse{ - Details: &object_pb.ListDetails{ - TotalResult: 4, - Timestamp: timestamppb.Now(), - }, - IdentityProviders: []*settings.IdentityProvider{ - idpActiveResponse, - idpLinkingDisallowedResponse, - idpCreationDisallowedResponse, - idpNoAutoLinkingResponse, - }, - }, - }, - { - name: "success, no auto creation", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &settings.GetActiveIdentityProvidersRequest{ - AutoCreation: gu.Ptr(false), - }, - }, - want: &settings.GetActiveIdentityProvidersResponse{ - Details: &object_pb.ListDetails{ - TotalResult: 1, - Timestamp: timestamppb.Now(), - }, - IdentityProviders: []*settings.IdentityProvider{ - idpNoAutoCreationResponse, - }, - }, - }, - { - name: "success, auto linking", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &settings.GetActiveIdentityProvidersRequest{ - AutoLinking: gu.Ptr(true), - }, - }, - want: &settings.GetActiveIdentityProvidersResponse{ - Details: &object_pb.ListDetails{ - TotalResult: 4, - Timestamp: timestamppb.Now(), - }, - IdentityProviders: []*settings.IdentityProvider{ - idpActiveResponse, - idpLinkingDisallowedResponse, - idpCreationDisallowedResponse, - idpNoAutoCreationResponse, - }, - }, - }, - { - name: "success, no auto linking", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &settings.GetActiveIdentityProvidersRequest{ - AutoLinking: gu.Ptr(false), - }, - }, - want: &settings.GetActiveIdentityProvidersResponse{ - Details: &object_pb.ListDetails{ - TotalResult: 1, - Timestamp: timestamppb.Now(), - }, - IdentityProviders: []*settings.IdentityProvider{ - idpNoAutoLinkingResponse, - }, - }, - }, - { - name: "success, exclude all", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &settings.GetActiveIdentityProvidersRequest{ - LinkingAllowed: gu.Ptr(true), - CreationAllowed: gu.Ptr(true), - AutoCreation: gu.Ptr(true), - AutoLinking: gu.Ptr(true), - }, - }, - want: &settings.GetActiveIdentityProvidersResponse{ - Details: &object_pb.ListDetails{ - TotalResult: 1, - Timestamp: timestamppb.Now(), - }, - IdentityProviders: []*settings.IdentityProvider{ - idpActiveResponse, - }, + expectedResponse: &settings.SetHostedLoginTranslationResponse{ + Etag: hex.EncodeToString(hash[:]), }, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute) - assert.EventuallyWithT(t, func(ct *assert.CollectT) { - got, err := instance.Client.SettingsV2.GetActiveIdentityProviders(tt.args.ctx, tt.args.req) - if tt.wantErr { - assert.Error(ct, err) - return - } - if !assert.NoError(ct, err) { - return - } - for i, result := range tt.want.GetIdentityProviders() { - assert.EqualExportedValues(ct, result, got.GetIdentityProviders()[i]) - } - integration.AssertListDetails(ct, tt.want, got) - }, retryDuration, tick) + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // When + res, err := Client.SetHostedLoginTranslation(tc.inputCtx, tc.inputRequest) + + // Then + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + require.NoError(t, err) + assert.Equal(t, tc.expectedResponse.GetEtag(), res.GetEtag()) + } }) } } diff --git a/internal/api/grpc/settings/v2/query.go b/internal/api/grpc/settings/v2/query.go new file mode 100644 index 0000000000..b8994ccb87 --- /dev/null +++ b/internal/api/grpc/settings/v2/query.go @@ -0,0 +1,209 @@ +package settings + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/i18n" + "github.com/zitadel/zitadel/internal/query" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" +) + +func (s *Server) GetLoginSettings(ctx context.Context, req *settings.GetLoginSettingsRequest) (*settings.GetLoginSettingsResponse, error) { + current, err := s.query.LoginPolicyByID(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) + if err != nil { + return nil, err + } + return &settings.GetLoginSettingsResponse{ + Settings: loginSettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.OrgID, + }, + }, nil +} + +func (s *Server) GetPasswordComplexitySettings(ctx context.Context, req *settings.GetPasswordComplexitySettingsRequest) (*settings.GetPasswordComplexitySettingsResponse, error) { + current, err := s.query.PasswordComplexityPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) + if err != nil { + return nil, err + } + return &settings.GetPasswordComplexitySettingsResponse{ + Settings: passwordComplexitySettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }, nil +} + +func (s *Server) GetPasswordExpirySettings(ctx context.Context, req *settings.GetPasswordExpirySettingsRequest) (*settings.GetPasswordExpirySettingsResponse, error) { + current, err := s.query.PasswordAgePolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) + if err != nil { + return nil, err + } + return &settings.GetPasswordExpirySettingsResponse{ + Settings: passwordExpirySettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }, nil +} + +func (s *Server) GetBrandingSettings(ctx context.Context, req *settings.GetBrandingSettingsRequest) (*settings.GetBrandingSettingsResponse, error) { + current, err := s.query.ActiveLabelPolicyByOrg(ctx, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) + if err != nil { + return nil, err + } + return &settings.GetBrandingSettingsResponse{ + Settings: brandingSettingsToPb(current, s.assetsAPIDomain(ctx)), + Details: &object_pb.Details{ + Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }, nil +} + +func (s *Server) GetDomainSettings(ctx context.Context, req *settings.GetDomainSettingsRequest) (*settings.GetDomainSettingsResponse, error) { + current, err := s.query.DomainPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) + if err != nil { + return nil, err + } + return &settings.GetDomainSettingsResponse{ + Settings: domainSettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }, nil +} + +func (s *Server) GetLegalAndSupportSettings(ctx context.Context, req *settings.GetLegalAndSupportSettingsRequest) (*settings.GetLegalAndSupportSettingsResponse, error) { + current, err := s.query.PrivacyPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) + if err != nil { + return nil, err + } + return &settings.GetLegalAndSupportSettingsResponse{ + Settings: legalAndSupportSettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }, nil +} + +func (s *Server) GetLockoutSettings(ctx context.Context, req *settings.GetLockoutSettingsRequest) (*settings.GetLockoutSettingsResponse, error) { + current, err := s.query.LockoutPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx())) + if err != nil { + return nil, err + } + return &settings.GetLockoutSettingsResponse{ + Settings: lockoutSettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }, nil +} + +func (s *Server) GetActiveIdentityProviders(ctx context.Context, req *settings.GetActiveIdentityProvidersRequest) (*settings.GetActiveIdentityProvidersResponse, error) { + queries, err := activeIdentityProvidersToQuery(req) + if err != nil { + return nil, err + } + + links, err := s.query.IDPLoginPolicyLinks(ctx, object.ResourceOwnerFromReq(ctx, req.GetCtx()), &query.IDPLoginPolicyLinksSearchQuery{Queries: queries}, false) + if err != nil { + return nil, err + } + + return &settings.GetActiveIdentityProvidersResponse{ + Details: object.ToListDetails(links.SearchResponse), + IdentityProviders: identityProvidersToPb(links.Links), + }, nil +} + +func activeIdentityProvidersToQuery(req *settings.GetActiveIdentityProvidersRequest) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, 0, 4) + if req.CreationAllowed != nil { + creationQuery, err := query.NewIDPTemplateIsCreationAllowedSearchQuery(*req.CreationAllowed) + if err != nil { + return nil, err + } + q = append(q, creationQuery) + } + if req.LinkingAllowed != nil { + creationQuery, err := query.NewIDPTemplateIsLinkingAllowedSearchQuery(*req.LinkingAllowed) + if err != nil { + return nil, err + } + q = append(q, creationQuery) + } + if req.AutoCreation != nil { + creationQuery, err := query.NewIDPTemplateIsAutoCreationSearchQuery(*req.AutoCreation) + if err != nil { + return nil, err + } + q = append(q, creationQuery) + } + if req.AutoLinking != nil { + compare := query.NumberEquals + if *req.AutoLinking { + compare = query.NumberNotEquals + } + creationQuery, err := query.NewIDPTemplateAutoLinkingSearchQuery(0, compare) + if err != nil { + return nil, err + } + q = append(q, creationQuery) + } + return q, nil +} + +func (s *Server) GetGeneralSettings(ctx context.Context, _ *settings.GetGeneralSettingsRequest) (*settings.GetGeneralSettingsResponse, error) { + instance := authz.GetInstance(ctx) + return &settings.GetGeneralSettingsResponse{ + SupportedLanguages: domain.LanguagesToStrings(i18n.SupportedLanguages()), + DefaultOrgId: instance.DefaultOrganisationID(), + DefaultLanguage: instance.DefaultLanguage().String(), + }, nil +} + +func (s *Server) GetSecuritySettings(ctx context.Context, req *settings.GetSecuritySettingsRequest) (*settings.GetSecuritySettingsResponse, error) { + policy, err := s.query.SecurityPolicy(ctx) + if err != nil { + return nil, err + } + return &settings.GetSecuritySettingsResponse{ + Settings: securityPolicyToSettingsPb(policy), + }, nil +} + +func (s *Server) GetHostedLoginTranslation(ctx context.Context, req *settings.GetHostedLoginTranslationRequest) (*settings.GetHostedLoginTranslationResponse, error) { + translation, err := s.query.GetHostedLoginTranslation(ctx, req) + if err != nil { + return nil, err + } + + return translation, nil +} diff --git a/internal/api/grpc/settings/v2/settings.go b/internal/api/grpc/settings/v2/settings.go index 77874bf970..09ee6b27c8 100644 --- a/internal/api/grpc/settings/v2/settings.go +++ b/internal/api/grpc/settings/v2/settings.go @@ -3,202 +3,10 @@ package settings import ( "context" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/i18n" - "github.com/zitadel/zitadel/internal/query" - object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2" "github.com/zitadel/zitadel/pkg/grpc/settings/v2" ) -func (s *Server) GetLoginSettings(ctx context.Context, req *settings.GetLoginSettingsRequest) (*settings.GetLoginSettingsResponse, error) { - current, err := s.query.LoginPolicyByID(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) - if err != nil { - return nil, err - } - return &settings.GetLoginSettingsResponse{ - Settings: loginSettingsToPb(current), - Details: &object_pb.Details{ - Sequence: current.Sequence, - CreationDate: timestamppb.New(current.CreationDate), - ChangeDate: timestamppb.New(current.ChangeDate), - ResourceOwner: current.OrgID, - }, - }, nil -} - -func (s *Server) GetPasswordComplexitySettings(ctx context.Context, req *settings.GetPasswordComplexitySettingsRequest) (*settings.GetPasswordComplexitySettingsResponse, error) { - current, err := s.query.PasswordComplexityPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) - if err != nil { - return nil, err - } - return &settings.GetPasswordComplexitySettingsResponse{ - Settings: passwordComplexitySettingsToPb(current), - Details: &object_pb.Details{ - Sequence: current.Sequence, - CreationDate: timestamppb.New(current.CreationDate), - ChangeDate: timestamppb.New(current.ChangeDate), - ResourceOwner: current.ResourceOwner, - }, - }, nil -} - -func (s *Server) GetPasswordExpirySettings(ctx context.Context, req *settings.GetPasswordExpirySettingsRequest) (*settings.GetPasswordExpirySettingsResponse, error) { - current, err := s.query.PasswordAgePolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) - if err != nil { - return nil, err - } - return &settings.GetPasswordExpirySettingsResponse{ - Settings: passwordExpirySettingsToPb(current), - Details: &object_pb.Details{ - Sequence: current.Sequence, - CreationDate: timestamppb.New(current.CreationDate), - ChangeDate: timestamppb.New(current.ChangeDate), - ResourceOwner: current.ResourceOwner, - }, - }, nil -} - -func (s *Server) GetBrandingSettings(ctx context.Context, req *settings.GetBrandingSettingsRequest) (*settings.GetBrandingSettingsResponse, error) { - current, err := s.query.ActiveLabelPolicyByOrg(ctx, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) - if err != nil { - return nil, err - } - return &settings.GetBrandingSettingsResponse{ - Settings: brandingSettingsToPb(current, s.assetsAPIDomain(ctx)), - Details: &object_pb.Details{ - Sequence: current.Sequence, - CreationDate: timestamppb.New(current.CreationDate), - ChangeDate: timestamppb.New(current.ChangeDate), - ResourceOwner: current.ResourceOwner, - }, - }, nil -} - -func (s *Server) GetDomainSettings(ctx context.Context, req *settings.GetDomainSettingsRequest) (*settings.GetDomainSettingsResponse, error) { - current, err := s.query.DomainPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) - if err != nil { - return nil, err - } - return &settings.GetDomainSettingsResponse{ - Settings: domainSettingsToPb(current), - Details: &object_pb.Details{ - Sequence: current.Sequence, - CreationDate: timestamppb.New(current.CreationDate), - ChangeDate: timestamppb.New(current.ChangeDate), - ResourceOwner: current.ResourceOwner, - }, - }, nil -} - -func (s *Server) GetLegalAndSupportSettings(ctx context.Context, req *settings.GetLegalAndSupportSettingsRequest) (*settings.GetLegalAndSupportSettingsResponse, error) { - current, err := s.query.PrivacyPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) - if err != nil { - return nil, err - } - return &settings.GetLegalAndSupportSettingsResponse{ - Settings: legalAndSupportSettingsToPb(current), - Details: &object_pb.Details{ - Sequence: current.Sequence, - CreationDate: timestamppb.New(current.CreationDate), - ChangeDate: timestamppb.New(current.ChangeDate), - ResourceOwner: current.ResourceOwner, - }, - }, nil -} - -func (s *Server) GetLockoutSettings(ctx context.Context, req *settings.GetLockoutSettingsRequest) (*settings.GetLockoutSettingsResponse, error) { - current, err := s.query.LockoutPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx())) - if err != nil { - return nil, err - } - return &settings.GetLockoutSettingsResponse{ - Settings: lockoutSettingsToPb(current), - Details: &object_pb.Details{ - Sequence: current.Sequence, - CreationDate: timestamppb.New(current.CreationDate), - ChangeDate: timestamppb.New(current.ChangeDate), - ResourceOwner: current.ResourceOwner, - }, - }, nil -} - -func (s *Server) GetActiveIdentityProviders(ctx context.Context, req *settings.GetActiveIdentityProvidersRequest) (*settings.GetActiveIdentityProvidersResponse, error) { - queries, err := activeIdentityProvidersToQuery(req) - if err != nil { - return nil, err - } - - links, err := s.query.IDPLoginPolicyLinks(ctx, object.ResourceOwnerFromReq(ctx, req.GetCtx()), &query.IDPLoginPolicyLinksSearchQuery{Queries: queries}, false) - if err != nil { - return nil, err - } - - return &settings.GetActiveIdentityProvidersResponse{ - Details: object.ToListDetails(links.SearchResponse), - IdentityProviders: identityProvidersToPb(links.Links), - }, nil -} - -func activeIdentityProvidersToQuery(req *settings.GetActiveIdentityProvidersRequest) (_ []query.SearchQuery, err error) { - q := make([]query.SearchQuery, 0, 4) - if req.CreationAllowed != nil { - creationQuery, err := query.NewIDPTemplateIsCreationAllowedSearchQuery(*req.CreationAllowed) - if err != nil { - return nil, err - } - q = append(q, creationQuery) - } - if req.LinkingAllowed != nil { - creationQuery, err := query.NewIDPTemplateIsLinkingAllowedSearchQuery(*req.LinkingAllowed) - if err != nil { - return nil, err - } - q = append(q, creationQuery) - } - if req.AutoCreation != nil { - creationQuery, err := query.NewIDPTemplateIsAutoCreationSearchQuery(*req.AutoCreation) - if err != nil { - return nil, err - } - q = append(q, creationQuery) - } - if req.AutoLinking != nil { - compare := query.NumberEquals - if *req.AutoLinking { - compare = query.NumberNotEquals - } - creationQuery, err := query.NewIDPTemplateAutoLinkingSearchQuery(0, compare) - if err != nil { - return nil, err - } - q = append(q, creationQuery) - } - return q, nil -} - -func (s *Server) GetGeneralSettings(ctx context.Context, _ *settings.GetGeneralSettingsRequest) (*settings.GetGeneralSettingsResponse, error) { - instance := authz.GetInstance(ctx) - return &settings.GetGeneralSettingsResponse{ - SupportedLanguages: domain.LanguagesToStrings(i18n.SupportedLanguages()), - DefaultOrgId: instance.DefaultOrganisationID(), - DefaultLanguage: instance.DefaultLanguage().String(), - }, nil -} - -func (s *Server) GetSecuritySettings(ctx context.Context, req *settings.GetSecuritySettingsRequest) (*settings.GetSecuritySettingsResponse, error) { - policy, err := s.query.SecurityPolicy(ctx) - if err != nil { - return nil, err - } - return &settings.GetSecuritySettingsResponse{ - Settings: securityPolicyToSettingsPb(policy), - }, nil -} - func (s *Server) SetSecuritySettings(ctx context.Context, req *settings.SetSecuritySettingsRequest) (*settings.SetSecuritySettingsResponse, error) { details, err := s.command.SetSecurityPolicy(ctx, securitySettingsToCommand(req)) if err != nil { @@ -208,3 +16,12 @@ func (s *Server) SetSecuritySettings(ctx context.Context, req *settings.SetSecur Details: object.DomainToDetailsPb(details), }, nil } + +func (s *Server) SetHostedLoginTranslation(ctx context.Context, req *settings.SetHostedLoginTranslationRequest) (*settings.SetHostedLoginTranslationResponse, error) { + res, err := s.command.SetHostedLoginTranslation(ctx, req) + if err != nil { + return nil, err + } + + return res, nil +} diff --git a/internal/command/hosted_login_translation.go b/internal/command/hosted_login_translation.go new file mode 100644 index 0000000000..024ab6bdad --- /dev/null +++ b/internal/command/hosted_login_translation.go @@ -0,0 +1,73 @@ +package command + +import ( + "context" + "crypto/md5" + "encoding/hex" + "fmt" + + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" +) + +func (c *Commands) SetHostedLoginTranslation(ctx context.Context, req *settings.SetHostedLoginTranslationRequest) (res *settings.SetHostedLoginTranslationResponse, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + var agg eventstore.Aggregate + switch t := req.GetLevel().(type) { + case *settings.SetHostedLoginTranslationRequest_Instance: + agg = instance.NewAggregate(authz.GetInstance(ctx).InstanceID()).Aggregate + case *settings.SetHostedLoginTranslationRequest_OrganizationId: + agg = org.NewAggregate(t.OrganizationId).Aggregate + default: + return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-YB6Sri", "Errors.Arguments.Level.Invalid") + } + + lang, err := language.Parse(req.GetLocale()) + if err != nil || lang.IsRoot() { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-xmjATA", "Errors.Arguments.Locale.Invalid") + } + + commands, wm, err := c.setTranslationEvents(ctx, agg, lang, req.GetTranslations().AsMap()) + if err != nil { + return nil, err + } + + pushedEvents, err := c.eventstore.Push(ctx, commands...) + if err != nil { + return nil, zerrors.ThrowInternal(err, "COMMA-i8nqFl", "Errors.Internal") + } + + err = AppendAndReduce(wm, pushedEvents...) + if err != nil { + return nil, err + } + + etag := md5.Sum(fmt.Append(nil, wm.Translation)) + return &settings.SetHostedLoginTranslationResponse{ + Etag: hex.EncodeToString(etag[:]), + }, nil +} + +func (c *Commands) setTranslationEvents(ctx context.Context, agg eventstore.Aggregate, lang language.Tag, translations map[string]any) ([]eventstore.Command, *HostedLoginTranslationWriteModel, error) { + wm := NewHostedLoginTranslationWriteModel(agg.ID) + events := []eventstore.Command{} + switch agg.Type { + case instance.AggregateType: + events = append(events, instance.NewHostedLoginTranslationSetEvent(ctx, &agg, translations, lang)) + case org.AggregateType: + events = append(events, org.NewHostedLoginTranslationSetEvent(ctx, &agg, translations, lang)) + default: + return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMA-0aw7In", "Errors.Arguments.LevelType.Invalid") + } + + return events, wm, nil +} diff --git a/internal/command/hosted_login_translation_model.go b/internal/command/hosted_login_translation_model.go new file mode 100644 index 0000000000..16bc42c541 --- /dev/null +++ b/internal/command/hosted_login_translation_model.go @@ -0,0 +1,45 @@ +package command + +import ( + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" +) + +type HostedLoginTranslationWriteModel struct { + eventstore.WriteModel + Language language.Tag + Translation map[string]any + Level string + LevelID string +} + +func NewHostedLoginTranslationWriteModel(resourceID string) *HostedLoginTranslationWriteModel { + return &HostedLoginTranslationWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: resourceID, + ResourceOwner: resourceID, + }, + } +} + +func (wm *HostedLoginTranslationWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *org.HostedLoginTranslationSetEvent: + wm.Language = e.Language + wm.Translation = e.Translation + wm.Level = e.Level + wm.LevelID = e.Aggregate().ID + case *instance.HostedLoginTranslationSetEvent: + wm.Language = e.Language + wm.Translation = e.Translation + wm.Level = e.Level + wm.LevelID = e.Aggregate().ID + } + } + + return wm.WriteModel.Reduce() +} diff --git a/internal/command/hosted_login_translation_test.go b/internal/command/hosted_login_translation_test.go new file mode 100644 index 0000000000..a5f0941711 --- /dev/null +++ b/internal/command/hosted_login_translation_test.go @@ -0,0 +1,211 @@ +package command + +import ( + "context" + "crypto/md5" + "encoding/hex" + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/text/language" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/service" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/zerrors" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" +) + +func TestSetTranslationEvents(t *testing.T) { + t.Parallel() + + testCtx := authz.SetCtxData(context.Background(), authz.CtxData{UserID: "test-user"}) + testCtx = service.WithService(testCtx, "test-service") + + tt := []struct { + testName string + + inputAggregate eventstore.Aggregate + inputLanguage language.Tag + inputTranslations map[string]any + + expectedCommands []eventstore.Command + expectedWriteModel *HostedLoginTranslationWriteModel + expectedError error + }{ + { + testName: "when aggregate type is instance should return matching write model and instance.hosted_login_translation_set event", + inputAggregate: eventstore.Aggregate{ID: "123", Type: instance.AggregateType}, + inputLanguage: language.MustParse("en-US"), + inputTranslations: map[string]any{"test": "translation"}, + expectedCommands: []eventstore.Command{ + instance.NewHostedLoginTranslationSetEvent(testCtx, &eventstore.Aggregate{ID: "123", Type: instance.AggregateType}, map[string]any{"test": "translation"}, language.MustParse("en-US")), + }, + expectedWriteModel: &HostedLoginTranslationWriteModel{ + WriteModel: eventstore.WriteModel{AggregateID: "123", ResourceOwner: "123"}, + }, + }, + { + testName: "when aggregate type is org should return matching write model and org.hosted_login_translation_set event", + inputAggregate: eventstore.Aggregate{ID: "123", Type: org.AggregateType}, + inputLanguage: language.MustParse("en-GB"), + inputTranslations: map[string]any{"test": "translation"}, + expectedCommands: []eventstore.Command{ + org.NewHostedLoginTranslationSetEvent(testCtx, &eventstore.Aggregate{ID: "123", Type: org.AggregateType}, map[string]any{"test": "translation"}, language.MustParse("en-GB")), + }, + expectedWriteModel: &HostedLoginTranslationWriteModel{ + WriteModel: eventstore.WriteModel{AggregateID: "123", ResourceOwner: "123"}, + }, + }, + { + testName: "when aggregate type is neither org nor instance should return invalid argument error", + inputAggregate: eventstore.Aggregate{ID: "123"}, + inputLanguage: language.MustParse("en-US"), + inputTranslations: map[string]any{"test": "translation"}, + expectedError: zerrors.ThrowInvalidArgument(nil, "COMMA-0aw7In", "Errors.Arguments.LevelType.Invalid"), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // Given + c := Commands{} + + // When + events, writeModel, err := c.setTranslationEvents(testCtx, tc.inputAggregate, tc.inputLanguage, tc.inputTranslations) + + // Verify + require.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedWriteModel, writeModel) + + require.Len(t, events, len(tc.expectedCommands)) + assert.ElementsMatch(t, tc.expectedCommands, events) + }) + } +} + +func TestSetHostedLoginTranslation(t *testing.T) { + t.Parallel() + + testCtx := authz.SetCtxData(context.Background(), authz.CtxData{UserID: "test-user"}) + testCtx = service.WithService(testCtx, "test-service") + testCtx = authz.WithInstanceID(testCtx, "instance-id") + + testTranslation := map[string]any{"test": "translation", "translation": "2"} + protoTranslation, err := structpb.NewStruct(testTranslation) + require.NoError(t, err) + + hashTestTranslation := md5.Sum(fmt.Append(nil, testTranslation)) + require.NotEmpty(t, hashTestTranslation) + + tt := []struct { + testName string + + mockPush func(*testing.T) *eventstore.Eventstore + + inputReq *settings.SetHostedLoginTranslationRequest + + expectedError error + expectedResult *settings.SetHostedLoginTranslationResponse + }{ + { + testName: "when locale is malformed should return invalid argument error", + mockPush: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} }, + inputReq: &settings.SetHostedLoginTranslationRequest{ + Level: &settings.SetHostedLoginTranslationRequest_Instance{}, + Locale: "123", + }, + + expectedError: zerrors.ThrowInvalidArgument(nil, "COMMA-xmjATA", "Errors.Arguments.Locale.Invalid"), + }, + { + testName: "when locale is unknown should return invalid argument error", + mockPush: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} }, + inputReq: &settings.SetHostedLoginTranslationRequest{ + Level: &settings.SetHostedLoginTranslationRequest_Instance{}, + Locale: "root", + }, + + expectedError: zerrors.ThrowInvalidArgument(nil, "COMMA-xmjATA", "Errors.Arguments.Locale.Invalid"), + }, + { + testName: "when event pushing fails should return internal error", + + mockPush: expectEventstore(expectPushFailed( + errors.New("mock push failed"), + instance.NewHostedLoginTranslationSetEvent( + testCtx, &eventstore.Aggregate{ + ID: "instance-id", + Type: instance.AggregateType, + ResourceOwner: "instance-id", + InstanceID: "instance-id", + Version: instance.AggregateVersion, + }, + testTranslation, + language.MustParse("it-CH"), + ), + )), + + inputReq: &settings.SetHostedLoginTranslationRequest{ + Level: &settings.SetHostedLoginTranslationRequest_Instance{}, + Locale: "it-CH", + Translations: protoTranslation, + }, + + expectedError: zerrors.ThrowInternal(errors.New("mock push failed"), "COMMA-i8nqFl", "Errors.Internal"), + }, + { + testName: "when request is valid should return expected response", + + mockPush: expectEventstore(expectPush( + org.NewHostedLoginTranslationSetEvent( + testCtx, &eventstore.Aggregate{ + ID: "org-id", + Type: org.AggregateType, + ResourceOwner: "org-id", + InstanceID: "", + Version: org.AggregateVersion, + }, + testTranslation, + language.MustParse("it-CH"), + ), + )), + + inputReq: &settings.SetHostedLoginTranslationRequest{ + Level: &settings.SetHostedLoginTranslationRequest_OrganizationId{OrganizationId: "org-id"}, + Locale: "it-CH", + Translations: protoTranslation, + }, + + expectedResult: &settings.SetHostedLoginTranslationResponse{ + Etag: hex.EncodeToString(hashTestTranslation[:]), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // Given + c := Commands{ + eventstore: tc.mockPush(t), + } + + // When + res, err := c.SetHostedLoginTranslation(testCtx, tc.inputReq) + + // Verify + require.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedResult, res) + }) + } +} diff --git a/internal/database/mock/sql_mock.go b/internal/database/mock/sql_mock.go index b8030b269f..cd30cd9cf0 100644 --- a/internal/database/mock/sql_mock.go +++ b/internal/database/mock/sql_mock.go @@ -14,9 +14,9 @@ type SQLMock struct { mock sqlmock.Sqlmock } -type expectation func(m sqlmock.Sqlmock) +type Expectation func(m sqlmock.Sqlmock) -func NewSQLMock(t *testing.T, expectations ...expectation) *SQLMock { +func NewSQLMock(t *testing.T, expectations ...Expectation) *SQLMock { db, mock, err := sqlmock.New( sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual), sqlmock.ValueConverterOption(new(TypeConverter)), @@ -45,7 +45,7 @@ func (m *SQLMock) Assert(t *testing.T) { m.DB.Close() } -func ExpectBegin(err error) expectation { +func ExpectBegin(err error) Expectation { return func(m sqlmock.Sqlmock) { e := m.ExpectBegin() if err != nil { @@ -54,7 +54,7 @@ func ExpectBegin(err error) expectation { } } -func ExpectCommit(err error) expectation { +func ExpectCommit(err error) Expectation { return func(m sqlmock.Sqlmock) { e := m.ExpectCommit() if err != nil { @@ -89,7 +89,7 @@ func WithExecRowsAffected(affected driver.RowsAffected) ExecOpt { } } -func ExcpectExec(stmt string, opts ...ExecOpt) expectation { +func ExcpectExec(stmt string, opts ...ExecOpt) Expectation { return func(m sqlmock.Sqlmock) { e := m.ExpectExec(stmt) for _, opt := range opts { @@ -122,7 +122,7 @@ func WithQueryResult(columns []string, rows [][]driver.Value) QueryOpt { } } -func ExpectQuery(stmt string, opts ...QueryOpt) expectation { +func ExpectQuery(stmt string, opts ...QueryOpt) Expectation { return func(m sqlmock.Sqlmock) { e := m.ExpectQuery(stmt) for _, opt := range opts { diff --git a/internal/query/hosted_login_translation.go b/internal/query/hosted_login_translation.go new file mode 100644 index 0000000000..82193d2069 --- /dev/null +++ b/internal/query/hosted_login_translation.go @@ -0,0 +1,256 @@ +package query + +import ( + "context" + "crypto/md5" + "database/sql" + _ "embed" + "encoding/hex" + "encoding/json" + "fmt" + "time" + + "dario.cat/mergo" + sq "github.com/Masterminds/squirrel" + "github.com/zitadel/logging" + "golang.org/x/text/language" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/query/projection" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/v2/org" + "github.com/zitadel/zitadel/internal/zerrors" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" +) + +var ( + //go:embed v2-default.json + defaultLoginTranslations []byte + + defaultSystemTranslations map[language.Tag]map[string]any + + hostedLoginTranslationTable = table{ + name: projection.HostedLoginTranslationTable, + instanceIDCol: projection.HostedLoginTranslationInstanceIDCol, + } + + hostedLoginTranslationColInstanceID = Column{ + name: projection.HostedLoginTranslationInstanceIDCol, + table: hostedLoginTranslationTable, + } + hostedLoginTranslationColResourceOwner = Column{ + name: projection.HostedLoginTranslationAggregateIDCol, + table: hostedLoginTranslationTable, + } + hostedLoginTranslationColResourceOwnerType = Column{ + name: projection.HostedLoginTranslationAggregateTypeCol, + table: hostedLoginTranslationTable, + } + hostedLoginTranslationColLocale = Column{ + name: projection.HostedLoginTranslationLocaleCol, + table: hostedLoginTranslationTable, + } + hostedLoginTranslationColFile = Column{ + name: projection.HostedLoginTranslationFileCol, + table: hostedLoginTranslationTable, + } + hostedLoginTranslationColEtag = Column{ + name: projection.HostedLoginTranslationEtagCol, + table: hostedLoginTranslationTable, + } +) + +func init() { + err := json.Unmarshal(defaultLoginTranslations, &defaultSystemTranslations) + if err != nil { + panic(err) + } +} + +type HostedLoginTranslations struct { + SearchResponse + HostedLoginTranslations []*HostedLoginTranslation +} + +type HostedLoginTranslation struct { + AggregateID string + Sequence uint64 + CreationDate time.Time + ChangeDate time.Time + + Locale string + File map[string]any + LevelType string + LevelID string + Etag string +} + +func (q *Queries) GetHostedLoginTranslation(ctx context.Context, req *settings.GetHostedLoginTranslationRequest) (res *settings.GetHostedLoginTranslationResponse, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + inst := authz.GetInstance(ctx) + defaultInstLang := inst.DefaultLanguage() + + lang, err := language.BCP47.Parse(req.GetLocale()) + if err != nil || lang.IsRoot() { + return nil, zerrors.ThrowInvalidArgument(nil, "QUERY-rZLAGi", "Errors.Arguments.Locale.Invalid") + } + parentLang := lang.Parent() + if parentLang.IsRoot() { + parentLang = lang + } + + sysTranslation, systemEtag, err := getSystemTranslation(parentLang, defaultInstLang) + if err != nil { + return nil, err + } + + var levelID, resourceOwner string + switch t := req.GetLevel().(type) { + case *settings.GetHostedLoginTranslationRequest_System: + return getTranslationOutputMessage(sysTranslation, systemEtag) + case *settings.GetHostedLoginTranslationRequest_Instance: + levelID = authz.GetInstance(ctx).InstanceID() + resourceOwner = instance.AggregateType + case *settings.GetHostedLoginTranslationRequest_OrganizationId: + levelID = t.OrganizationId + resourceOwner = org.AggregateType + default: + return nil, zerrors.ThrowInvalidArgument(nil, "QUERY-YB6Sri", "Errors.Arguments.Level.Invalid") + } + + stmt, scan := prepareHostedLoginTranslationQuery() + + langORBaseLang := sq.Or{ + sq.Eq{hostedLoginTranslationColLocale.identifier(): lang.String()}, + sq.Eq{hostedLoginTranslationColLocale.identifier(): parentLang.String()}, + } + eq := sq.Eq{ + hostedLoginTranslationColInstanceID.identifier(): inst.InstanceID(), + hostedLoginTranslationColResourceOwner.identifier(): levelID, + hostedLoginTranslationColResourceOwnerType.identifier(): resourceOwner, + } + + query, args, err := stmt.Where(eq).Where(langORBaseLang).ToSql() + if err != nil { + logging.WithError(err).Error("unable to generate sql statement") + return nil, zerrors.ThrowInternal(err, "QUERY-ZgCMux", "Errors.Query.SQLStatement") + } + + var trs []*HostedLoginTranslation + err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { + trs, err = scan(rows) + return err + }, query, args...) + if err != nil { + logging.WithError(err).Error("failed to query translations") + return nil, zerrors.ThrowInternal(err, "QUERY-6k1zjx", "Errors.Internal") + } + + requestedTranslation, parentTranslation := &HostedLoginTranslation{}, &HostedLoginTranslation{} + for _, tr := range trs { + if tr == nil { + continue + } + + if tr.LevelType == resourceOwner { + requestedTranslation = tr + } else { + parentTranslation = tr + } + } + + if !req.GetIgnoreInheritance() { + + // There is no record for the requested level, set the upper level etag + if requestedTranslation.Etag == "" { + requestedTranslation.Etag = parentTranslation.Etag + } + + // Case where Level == ORGANIZATION -> Check if we have an instance level translation + // If so, merge it with the translations we have + if parentTranslation != nil && parentTranslation.LevelType == instance.AggregateType { + if err := mergo.Merge(&requestedTranslation.File, parentTranslation.File); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-pdgEJd", "Errors.Query.MergeTranslations") + } + } + + // The DB query returned no results, we have to set the system translation etag + if requestedTranslation.Etag == "" { + requestedTranslation.Etag = systemEtag + } + + // Merge the system translations + if err := mergo.Merge(&requestedTranslation.File, sysTranslation); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-HdprNF", "Errors.Query.MergeTranslations") + } + } + + return getTranslationOutputMessage(requestedTranslation.File, requestedTranslation.Etag) +} + +func getSystemTranslation(lang, instanceDefaultLang language.Tag) (map[string]any, string, error) { + translation, ok := defaultSystemTranslations[lang] + if !ok { + translation, ok = defaultSystemTranslations[instanceDefaultLang] + if !ok { + return nil, "", zerrors.ThrowNotFoundf(nil, "QUERY-6gb5QR", "Errors.Query.HostedLoginTranslationNotFound-%s", lang) + } + } + + hash := md5.Sum(fmt.Append(nil, translation)) + + return translation, hex.EncodeToString(hash[:]), nil +} + +func prepareHostedLoginTranslationQuery() (sq.SelectBuilder, func(*sql.Rows) ([]*HostedLoginTranslation, error)) { + return sq.Select( + hostedLoginTranslationColFile.identifier(), + hostedLoginTranslationColResourceOwnerType.identifier(), + hostedLoginTranslationColEtag.identifier(), + ).From(hostedLoginTranslationTable.identifier()). + Limit(2). + PlaceholderFormat(sq.Dollar), + func(r *sql.Rows) ([]*HostedLoginTranslation, error) { + translations := make([]*HostedLoginTranslation, 0, 2) + for r.Next() { + var rawTranslation json.RawMessage + translation := &HostedLoginTranslation{} + err := r.Scan( + &rawTranslation, + &translation.LevelType, + &translation.Etag, + ) + if err != nil { + return nil, err + } + + if err := json.Unmarshal(rawTranslation, &translation.File); err != nil { + return nil, err + } + + translations = append(translations, translation) + } + + if err := r.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-oc7r7i", "Errors.Query.CloseRows") + } + + return translations, nil + } +} + +func getTranslationOutputMessage(translation map[string]any, etag string) (*settings.GetHostedLoginTranslationResponse, error) { + protoTranslation, err := structpb.NewStruct(translation) + if err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-70ppPp", "Errors.Protobuf.ConvertToStruct") + } + + return &settings.GetHostedLoginTranslationResponse{ + Translations: protoTranslation, + Etag: etag, + }, nil +} diff --git a/internal/query/hosted_login_translation_test.go b/internal/query/hosted_login_translation_test.go new file mode 100644 index 0000000000..0e9f511002 --- /dev/null +++ b/internal/query/hosted_login_translation_test.go @@ -0,0 +1,337 @@ +package query + +import ( + "crypto/md5" + "database/sql" + "database/sql/driver" + "encoding/hex" + "encoding/json" + "fmt" + "maps" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/text/language" + "google.golang.org/protobuf/runtime/protoimpl" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/database/mock" + "github.com/zitadel/zitadel/internal/zerrors" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" +) + +func TestGetSystemTranslation(t *testing.T) { + okTranslation := defaultLoginTranslations + + parsedOKTranslation := map[string]map[string]any{} + require.Nil(t, json.Unmarshal(okTranslation, &parsedOKTranslation)) + + hashOK := md5.Sum(fmt.Append(nil, parsedOKTranslation["de"])) + + tt := []struct { + testName string + + inputLanguage language.Tag + inputInstanceLanguage language.Tag + systemTranslationToSet []byte + + expectedLanguage map[string]any + expectedEtag string + expectedError error + }{ + { + testName: "when neither input language nor system default language have translation should return not found error", + systemTranslationToSet: okTranslation, + inputLanguage: language.MustParse("ro"), + inputInstanceLanguage: language.MustParse("fr"), + + expectedError: zerrors.ThrowNotFoundf(nil, "QUERY-6gb5QR", "Errors.Query.HostedLoginTranslationNotFound-%s", "ro"), + }, + { + testName: "when input language has no translation should fallback onto instance default", + systemTranslationToSet: okTranslation, + inputLanguage: language.MustParse("ro"), + inputInstanceLanguage: language.MustParse("de"), + + expectedLanguage: parsedOKTranslation["de"], + expectedEtag: hex.EncodeToString(hashOK[:]), + }, + { + testName: "when input language has translation should return it", + systemTranslationToSet: okTranslation, + inputLanguage: language.MustParse("de"), + inputInstanceLanguage: language.MustParse("en"), + + expectedLanguage: parsedOKTranslation["de"], + expectedEtag: hex.EncodeToString(hashOK[:]), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Given + defaultLoginTranslations = tc.systemTranslationToSet + + // When + translation, etag, err := getSystemTranslation(tc.inputLanguage, tc.inputInstanceLanguage) + + // Verify + require.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedLanguage, translation) + assert.Equal(t, tc.expectedEtag, etag) + }) + } +} + +func TestGetTranslationOutput(t *testing.T) { + t.Parallel() + + validMap := map[string]any{"loginHeader": "A login header"} + protoMap, err := structpb.NewStruct(validMap) + require.NoError(t, err) + + hash := md5.Sum(fmt.Append(nil, validMap)) + encodedHash := hex.EncodeToString(hash[:]) + + tt := []struct { + testName string + inputTranslation map[string]any + expectedError error + expectedResponse *settings.GetHostedLoginTranslationResponse + }{ + { + testName: "when unparsable map should return internal error", + inputTranslation: map[string]any{"\xc5z": "something"}, + expectedError: zerrors.ThrowInternal(protoimpl.X.NewError("invalid UTF-8 in string: %q", "\xc5z"), "QUERY-70ppPp", "Errors.Protobuf.ConvertToStruct"), + }, + { + testName: "when input translation is valid should return expected response message", + inputTranslation: validMap, + expectedResponse: &settings.GetHostedLoginTranslationResponse{ + Translations: protoMap, + Etag: hex.EncodeToString(hash[:]), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := getTranslationOutputMessage(tc.inputTranslation, encodedHash) + + // Verify + require.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedResponse, res) + }) + } +} + +func TestGetHostedLoginTranslation(t *testing.T) { + query := `SELECT projections.hosted_login_translations.file, projections.hosted_login_translations.aggregate_type, projections.hosted_login_translations.etag + FROM projections.hosted_login_translations + WHERE projections.hosted_login_translations.aggregate_id = $1 + AND projections.hosted_login_translations.aggregate_type = $2 + AND projections.hosted_login_translations.instance_id = $3 + AND (projections.hosted_login_translations.locale = $4 OR projections.hosted_login_translations.locale = $5) + LIMIT 2` + okTranslation := defaultLoginTranslations + + parsedOKTranslation := map[string]map[string]any{} + require.NoError(t, json.Unmarshal(okTranslation, &parsedOKTranslation)) + + protoDefaultTranslation, err := structpb.NewStruct(parsedOKTranslation["en"]) + require.Nil(t, err) + + defaultWithDBTranslations := maps.Clone(parsedOKTranslation["en"]) + defaultWithDBTranslations["test"] = "translation" + defaultWithDBTranslations["test2"] = "translation2" + protoDefaultWithDBTranslation, err := structpb.NewStruct(defaultWithDBTranslations) + require.NoError(t, err) + + nilProtoDefaultMap, err := structpb.NewStruct(nil) + require.NoError(t, err) + + hashDefaultTranslations := md5.Sum(fmt.Append(nil, parsedOKTranslation["en"])) + + tt := []struct { + testName string + + defaultInstanceLanguage language.Tag + sqlExpectations []mock.Expectation + + inputRequest *settings.GetHostedLoginTranslationRequest + + expectedError error + expectedResult *settings.GetHostedLoginTranslationResponse + }{ + { + testName: "when input language is invalid should return invalid argument error", + + inputRequest: &settings.GetHostedLoginTranslationRequest{}, + + expectedError: zerrors.ThrowInvalidArgument(nil, "QUERY-rZLAGi", "Errors.Arguments.Locale.Invalid"), + }, + { + testName: "when input language is root should return invalid argument error", + + defaultInstanceLanguage: language.English, + inputRequest: &settings.GetHostedLoginTranslationRequest{ + Locale: "root", + }, + + expectedError: zerrors.ThrowInvalidArgument(nil, "QUERY-rZLAGi", "Errors.Arguments.Locale.Invalid"), + }, + { + testName: "when no system translation is available should return not found error", + + defaultInstanceLanguage: language.Romanian, + inputRequest: &settings.GetHostedLoginTranslationRequest{ + Locale: "ro-RO", + }, + + expectedError: zerrors.ThrowNotFoundf(nil, "QUERY-6gb5QR", "Errors.Query.HostedLoginTranslationNotFound-%s", "ro"), + }, + { + testName: "when requesting system translation should return it", + + defaultInstanceLanguage: language.English, + inputRequest: &settings.GetHostedLoginTranslationRequest{ + Locale: "en-US", + Level: &settings.GetHostedLoginTranslationRequest_System{}, + }, + + expectedResult: &settings.GetHostedLoginTranslationResponse{ + Translations: protoDefaultTranslation, + Etag: hex.EncodeToString(hashDefaultTranslations[:]), + }, + }, + { + testName: "when querying DB fails should return internal error", + + defaultInstanceLanguage: language.English, + sqlExpectations: []mock.Expectation{ + mock.ExpectQuery( + query, + mock.WithQueryArgs("123", "org", "instance-id", "en-US", "en"), + mock.WithQueryErr(sql.ErrConnDone), + ), + }, + inputRequest: &settings.GetHostedLoginTranslationRequest{ + Locale: "en-US", + Level: &settings.GetHostedLoginTranslationRequest_OrganizationId{ + OrganizationId: "123", + }, + }, + + expectedError: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-6k1zjx", "Errors.Internal"), + }, + { + testName: "when querying DB returns no result should return system translations", + + defaultInstanceLanguage: language.English, + sqlExpectations: []mock.Expectation{ + mock.ExpectQuery( + query, + mock.WithQueryArgs("123", "org", "instance-id", "en-US", "en"), + mock.WithQueryResult( + []string{"file", "aggregate_type", "etag"}, + [][]driver.Value{}, + ), + ), + }, + inputRequest: &settings.GetHostedLoginTranslationRequest{ + Locale: "en-US", + Level: &settings.GetHostedLoginTranslationRequest_OrganizationId{ + OrganizationId: "123", + }, + }, + + expectedResult: &settings.GetHostedLoginTranslationResponse{ + Translations: protoDefaultTranslation, + Etag: hex.EncodeToString(hashDefaultTranslations[:]), + }, + }, + { + testName: "when querying DB returns no result and inheritance disabled should return empty result", + + defaultInstanceLanguage: language.English, + sqlExpectations: []mock.Expectation{ + mock.ExpectQuery( + query, + mock.WithQueryArgs("123", "org", "instance-id", "en-US", "en"), + mock.WithQueryResult( + []string{"file", "aggregate_type", "etag"}, + [][]driver.Value{}, + ), + ), + }, + inputRequest: &settings.GetHostedLoginTranslationRequest{ + Locale: "en-US", + Level: &settings.GetHostedLoginTranslationRequest_OrganizationId{ + OrganizationId: "123", + }, + IgnoreInheritance: true, + }, + + expectedResult: &settings.GetHostedLoginTranslationResponse{ + Etag: "", + Translations: nilProtoDefaultMap, + }, + }, + { + testName: "when querying DB returns records should return merged result", + + defaultInstanceLanguage: language.English, + sqlExpectations: []mock.Expectation{ + mock.ExpectQuery( + query, + mock.WithQueryArgs("123", "org", "instance-id", "en-US", "en"), + mock.WithQueryResult( + []string{"file", "aggregate_type", "etag"}, + [][]driver.Value{ + {[]byte(`{"test": "translation"}`), "org", "etag-org"}, + {[]byte(`{"test2": "translation2"}`), "instance", "etag-instance"}, + }, + ), + ), + }, + inputRequest: &settings.GetHostedLoginTranslationRequest{ + Locale: "en-US", + Level: &settings.GetHostedLoginTranslationRequest_OrganizationId{ + OrganizationId: "123", + }, + }, + + expectedResult: &settings.GetHostedLoginTranslationResponse{ + Etag: "etag-org", + Translations: protoDefaultWithDBTranslation, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Given + db := &database.DB{DB: mock.NewSQLMock(t, tc.sqlExpectations...).DB} + querier := Queries{client: db} + + ctx := authz.NewMockContext("instance-id", "org-id", "user-id", authz.WithMockDefaultLanguage(tc.defaultInstanceLanguage)) + + // When + res, err := querier.GetHostedLoginTranslation(ctx, tc.inputRequest) + + // Verify + require.Equal(t, tc.expectedError, err) + + if tc.expectedError == nil { + assert.Equal(t, tc.expectedResult.GetEtag(), res.GetEtag()) + assert.Equal(t, tc.expectedResult.GetTranslations().GetFields(), res.GetTranslations().GetFields()) + } + }) + } +} diff --git a/internal/query/projection/hosted_login_translation.go b/internal/query/projection/hosted_login_translation.go new file mode 100644 index 0000000000..865d3738b9 --- /dev/null +++ b/internal/query/projection/hosted_login_translation.go @@ -0,0 +1,144 @@ +package projection + +import ( + "context" + "crypto/md5" + "encoding/hex" + "fmt" + + "github.com/zitadel/zitadel/internal/eventstore" + old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/zerrors" +) + +const ( + HostedLoginTranslationTable = "projections.hosted_login_translations" + + HostedLoginTranslationInstanceIDCol = "instance_id" + HostedLoginTranslationCreationDateCol = "creation_date" + HostedLoginTranslationChangeDateCol = "change_date" + HostedLoginTranslationAggregateIDCol = "aggregate_id" + HostedLoginTranslationAggregateTypeCol = "aggregate_type" + HostedLoginTranslationSequenceCol = "sequence" + HostedLoginTranslationLocaleCol = "locale" + HostedLoginTranslationFileCol = "file" + HostedLoginTranslationEtagCol = "etag" +) + +type hostedLoginTranslationProjection struct{} + +func newHostedLoginTranslationProjection(ctx context.Context, config handler.Config) *handler.Handler { + return handler.NewHandler(ctx, &config, new(hostedLoginTranslationProjection)) +} + +// Init implements [handler.initializer] +func (p *hostedLoginTranslationProjection) Init() *old_handler.Check { + return handler.NewTableCheck( + handler.NewTable([]*handler.InitColumn{ + handler.NewColumn(HostedLoginTranslationInstanceIDCol, handler.ColumnTypeText), + handler.NewColumn(HostedLoginTranslationCreationDateCol, handler.ColumnTypeTimestamp), + handler.NewColumn(HostedLoginTranslationChangeDateCol, handler.ColumnTypeTimestamp), + handler.NewColumn(HostedLoginTranslationAggregateIDCol, handler.ColumnTypeText), + handler.NewColumn(HostedLoginTranslationAggregateTypeCol, handler.ColumnTypeText), + handler.NewColumn(HostedLoginTranslationSequenceCol, handler.ColumnTypeInt64), + handler.NewColumn(HostedLoginTranslationLocaleCol, handler.ColumnTypeText), + handler.NewColumn(HostedLoginTranslationFileCol, handler.ColumnTypeJSONB), + handler.NewColumn(HostedLoginTranslationEtagCol, handler.ColumnTypeText), + }, + handler.NewPrimaryKey( + HostedLoginTranslationInstanceIDCol, + HostedLoginTranslationAggregateIDCol, + HostedLoginTranslationAggregateTypeCol, + HostedLoginTranslationLocaleCol, + ), + ), + ) +} + +func (hltp *hostedLoginTranslationProjection) Name() string { + return HostedLoginTranslationTable +} + +func (hltp *hostedLoginTranslationProjection) Reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: org.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: org.HostedLoginTranslationSet, + Reduce: hltp.reduceSet, + }, + }, + }, + { + Aggregate: instance.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: instance.HostedLoginTranslationSet, + Reduce: hltp.reduceSet, + }, + }, + }, + } +} + +func (hltp *hostedLoginTranslationProjection) reduceSet(e eventstore.Event) (*handler.Statement, error) { + + switch e := e.(type) { + case *org.HostedLoginTranslationSetEvent: + orgEvent := *e + return handler.NewUpsertStatement( + &orgEvent, + []handler.Column{ + handler.NewCol(HostedLoginTranslationInstanceIDCol, nil), + handler.NewCol(HostedLoginTranslationAggregateIDCol, nil), + handler.NewCol(HostedLoginTranslationAggregateTypeCol, nil), + handler.NewCol(HostedLoginTranslationLocaleCol, nil), + }, + []handler.Column{ + handler.NewCol(HostedLoginTranslationInstanceIDCol, orgEvent.Aggregate().InstanceID), + handler.NewCol(HostedLoginTranslationAggregateIDCol, orgEvent.Aggregate().ID), + handler.NewCol(HostedLoginTranslationAggregateTypeCol, orgEvent.Aggregate().Type), + handler.NewCol(HostedLoginTranslationCreationDateCol, handler.OnlySetValueOnInsert(HostedLoginTranslationTable, orgEvent.CreationDate())), + handler.NewCol(HostedLoginTranslationChangeDateCol, orgEvent.CreationDate()), + handler.NewCol(HostedLoginTranslationSequenceCol, orgEvent.Sequence()), + handler.NewCol(HostedLoginTranslationLocaleCol, orgEvent.Language), + handler.NewCol(HostedLoginTranslationFileCol, orgEvent.Translation), + handler.NewCol(HostedLoginTranslationEtagCol, hltp.computeEtag(orgEvent.Translation)), + }, + ), nil + case *instance.HostedLoginTranslationSetEvent: + instanceEvent := *e + return handler.NewUpsertStatement( + &instanceEvent, + []handler.Column{ + handler.NewCol(HostedLoginTranslationInstanceIDCol, nil), + handler.NewCol(HostedLoginTranslationAggregateIDCol, nil), + handler.NewCol(HostedLoginTranslationAggregateTypeCol, nil), + handler.NewCol(HostedLoginTranslationLocaleCol, nil), + }, + []handler.Column{ + handler.NewCol(HostedLoginTranslationInstanceIDCol, instanceEvent.Aggregate().InstanceID), + handler.NewCol(HostedLoginTranslationAggregateIDCol, instanceEvent.Aggregate().ID), + handler.NewCol(HostedLoginTranslationAggregateTypeCol, instanceEvent.Aggregate().Type), + handler.NewCol(HostedLoginTranslationCreationDateCol, handler.OnlySetValueOnInsert(HostedLoginTranslationTable, instanceEvent.CreationDate())), + handler.NewCol(HostedLoginTranslationChangeDateCol, instanceEvent.CreationDate()), + handler.NewCol(HostedLoginTranslationSequenceCol, instanceEvent.Sequence()), + handler.NewCol(HostedLoginTranslationLocaleCol, instanceEvent.Language), + handler.NewCol(HostedLoginTranslationFileCol, instanceEvent.Translation), + handler.NewCol(HostedLoginTranslationEtagCol, hltp.computeEtag(instanceEvent.Translation)), + }, + ), nil + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-AZshaa", "reduce.wrong.event.type %v", []eventstore.EventType{org.HostedLoginTranslationSet}) + } + +} + +func (hltp *hostedLoginTranslationProjection) computeEtag(translation map[string]any) string { + hash := md5.Sum(fmt.Append(nil, translation)) + return hex.EncodeToString(hash[:]) +} diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index 77a28ac79a..5ad62380ea 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -86,6 +86,7 @@ var ( UserSchemaProjection *handler.Handler WebKeyProjection *handler.Handler DebugEventsProjection *handler.Handler + HostedLoginTranslationProjection *handler.Handler ProjectGrantFields *handler.FieldHandler OrgDomainVerifiedFields *handler.FieldHandler @@ -179,6 +180,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore, UserSchemaProjection = newUserSchemaProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["user_schemas"])) WebKeyProjection = newWebKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["web_keys"])) DebugEventsProjection = newDebugEventsProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["debug_events"])) + HostedLoginTranslationProjection = newHostedLoginTranslationProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["hosted_login_translation"])) ProjectGrantFields = newFillProjectGrantFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsProjectGrant])) OrgDomainVerifiedFields = newFillOrgDomainVerifiedFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsOrgDomainVerified])) @@ -357,5 +359,6 @@ func newProjectionsList() { UserSchemaProjection, WebKeyProjection, DebugEventsProjection, + HostedLoginTranslationProjection, } } diff --git a/internal/query/v2-default.json b/internal/query/v2-default.json new file mode 100644 index 0000000000..c86396ef34 --- /dev/null +++ b/internal/query/v2-default.json @@ -0,0 +1,1557 @@ +{ + "de":{ + "common": { + "back": "Zurück" + }, + "accounts": { + "title": "Konten", + "description": "Wählen Sie das Konto aus, das Sie verwenden möchten.", + "addAnother": "Ein weiteres Konto hinzufügen", + "noResults": "Keine Konten gefunden" + }, + "loginname": { + "title": "Willkommen zurück!", + "description": "Geben Sie Ihre Anmeldedaten ein.", + "register": "Neuen Benutzer registrieren" + }, + "password": { + "verify": { + "title": "Passwort", + "description": "Geben Sie Ihr Passwort ein.", + "resetPassword": "Passwort zurücksetzen", + "submit": "Weiter" + }, + "set": { + "title": "Passwort festlegen", + "description": "Legen Sie das Passwort für Ihr Konto fest", + "codeSent": "Ein Code wurde an Ihre E-Mail-Adresse gesendet.", + "noCodeReceived": "Keinen Code erhalten?", + "resend": "Erneut senden", + "submit": "Weiter" + }, + "change": { + "title": "Passwort ändern", + "description": "Legen Sie das Passwort für Ihr Konto fest", + "submit": "Weiter" + } + }, + "idp": { + "title": "Mit SSO anmelden", + "description": "Wählen Sie einen der folgenden Anbieter, um sich anzumelden", + "signInWithApple": "Mit Apple anmelden", + "signInWithGoogle": "Mit Google anmelden", + "signInWithAzureAD": "Mit AzureAD anmelden", + "signInWithGithub": "Mit GitHub anmelden", + "signInWithGitlab": "Mit GitLab anmelden", + "loginSuccess": { + "title": "Anmeldung erfolgreich", + "description": "Sie haben sich erfolgreich angemeldet!" + }, + "linkingSuccess": { + "title": "Konto verknüpft", + "description": "Sie haben Ihr Konto erfolgreich verknüpft!" + }, + "registerSuccess": { + "title": "Registrierung erfolgreich", + "description": "Sie haben sich erfolgreich registriert!" + }, + "loginError": { + "title": "Anmeldung fehlgeschlagen", + "description": "Beim Anmelden ist ein Fehler aufgetreten." + }, + "linkingError": { + "title": "Konto-Verknüpfung fehlgeschlagen", + "description": "Beim Verknüpfen Ihres Kontos ist ein Fehler aufgetreten." + } + }, + "mfa": { + "verify": { + "title": "Bestätigen Sie Ihre Identität", + "description": "Wählen Sie einen der folgenden Faktoren.", + "noResults": "Keine zweiten Faktoren verfügbar, um sie einzurichten." + }, + "set": { + "title": "2-Faktor einrichten", + "description": "Wählen Sie einen der folgenden zweiten Faktoren.", + "skip": "Überspringen" + } + }, + "otp": { + "verify": { + "title": "2-Faktor bestätigen", + "totpDescription": "Geben Sie den Code aus Ihrer Authentifizierungs-App ein.", + "smsDescription": "Geben Sie den Code ein, den Sie per SMS erhalten haben.", + "emailDescription": "Geben Sie den Code ein, den Sie per E-Mail erhalten haben.", + "noCodeReceived": "Keinen Code erhalten?", + "resendCode": "Code erneut senden", + "submit": "Weiter" + }, + "set": { + "title": "2-Faktor einrichten", + "totpDescription": "Scannen Sie den QR-Code mit Ihrer Authentifizierungs-App.", + "smsDescription": "Geben Sie Ihre Telefonnummer ein, um einen Code per SMS zu erhalten.", + "emailDescription": "Geben Sie Ihre E-Mail-Adresse ein, um einen Code per E-Mail zu erhalten.", + "totpRegisterDescription": "Scannen Sie den QR-Code oder navigieren Sie manuell zur URL.", + "submit": "Weiter" + } + }, + "passkey": { + "verify": { + "title": "Mit einem Passkey authentifizieren", + "description": "Ihr Gerät wird nach Ihrem Fingerabdruck, Gesicht oder Bildschirmsperre fragen", + "usePassword": "Passwort verwenden", + "submit": "Weiter" + }, + "set": { + "title": "Passkey einrichten", + "description": "Ihr Gerät wird nach Ihrem Fingerabdruck, Gesicht oder Bildschirmsperre fragen", + "info": { + "description": "Ein Passkey ist eine Authentifizierungsmethode auf einem Gerät wie Ihr Fingerabdruck, Apple FaceID oder ähnliches.", + "link": "Passwortlose Authentifizierung" + }, + "skip": "Überspringen", + "submit": "Weiter" + } + }, + "u2f": { + "verify": { + "title": "2-Faktor bestätigen", + "description": "Bestätigen Sie Ihr Konto mit Ihrem Gerät." + }, + "set": { + "title": "2-Faktor einrichten", + "description": "Richten Sie ein Gerät als zweiten Faktor ein.", + "submit": "Weiter" + } + }, + "register": { + "methods": { + "passkey": "Passkey", + "password": "Password" + }, + "disabled": { + "title": "Registrierung deaktiviert", + "description": "Die Registrierung ist deaktiviert. Bitte wenden Sie sich an den Administrator." + }, + "missingdata": { + "title": "Registrierung fehlgeschlagen", + "description": "Einige Daten fehlen. Bitte überprüfen Sie Ihre Eingaben." + }, + "title": "Registrieren", + "description": "Erstellen Sie Ihr ZITADEL-Konto.", + "selectMethod": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten", + "agreeTo": "Um sich zu registrieren, müssen Sie den Nutzungsbedingungen zustimmen", + "termsOfService": "Nutzungsbedingungen", + "privacyPolicy": "Datenschutzrichtlinie", + "submit": "Weiter", + "password": { + "title": "Passwort festlegen", + "description": "Legen Sie das Passwort für Ihr Konto fest", + "submit": "Weiter" + } + }, + "invite": { + "title": "Benutzer einladen", + "description": "Geben Sie die E-Mail-Adresse des Benutzers ein, den Sie einladen möchten.", + "info": "Der Benutzer erhält eine E-Mail mit einem Link, um sich zu registrieren.", + "notAllowed": "Sie haben keine Berechtigung, Benutzer einzuladen.", + "submit": "Einladen", + "success": { + "title": "Einladung erfolgreich", + "description": "Der Benutzer wurde erfolgreich eingeladen.", + "verified": "Der Benutzer wurde eingeladen und hat seine E-Mail bereits verifiziert.", + "notVerifiedYet": "Der Benutzer wurde eingeladen. Er erhält eine E-Mail mit weiteren Anweisungen.", + "submit": "Weiteren Benutzer einladen" + } + }, + "signedin": { + "title": "Willkommen {user}!", + "description": "Sie sind angemeldet.", + "continue": "Weiter", + "error": { + "title": "Fehler", + "description": "Ein Fehler ist aufgetreten." + } + }, + "verify": { + "userIdMissing": "Keine Benutzer-ID angegeben!", + "success": "Erfolgreich verifiziert", + "setupAuthenticator": "Authentifikator einrichten", + "verify": { + "title": "Benutzer verifizieren", + "description": "Geben Sie den Code ein, der in der Bestätigungs-E-Mail angegeben ist.", + "noCodeReceived": "Keinen Code erhalten?", + "resendCode": "Code erneut senden", + "submit": "Weiter" + } + }, + "authenticator": { + "title": "Authentifizierungsmethode auswählen", + "description": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten.", + "noMethodsAvailable": "Keine Authentifizierungsmethoden verfügbar", + "allSetup": "Sie haben bereits einen Authentifikator eingerichtet!", + "linkWithIDP": "oder verknüpfe mit einem Identitätsanbieter" + }, + "device": { + "usercode": { + "title": "Gerätecode", + "description": "Geben Sie den Code ein.", + "submit": "Weiter" + }, + "request": { + "title": "{appName} möchte eine Verbindung herstellen:", + "disclaimer": "{appName} hat Zugriff auf:", + "description": "Durch Klicken auf Zulassen erlauben Sie {appName} und Zitadel, Ihre Informationen gemäß ihren jeweiligen Nutzungsbedingungen und Datenschutzrichtlinien zu verwenden. Sie können diesen Zugriff jederzeit widerrufen.", + "submit": "Zulassen", + "deny": "Ablehnen" + }, + "scope": { + "openid": "Überprüfen Ihrer Identität.", + "email": "Zugriff auf Ihre E-Mail-Adresse.", + "profile": "Zugriff auf Ihre vollständigen Profilinformationen.", + "offline_access": "Erlauben Sie den Offline-Zugriff auf Ihr Konto." + } + }, + "error": { + "noUserCode": "Kein Benutzercode angegeben!", + "noDeviceRequest": " Es wurde keine Geräteanforderung gefunden. Bitte überprüfen Sie die URL.", + "unknownContext": "Der Kontext des Benutzers konnte nicht ermittelt werden. Stellen Sie sicher, dass Sie zuerst den Benutzernamen eingeben oder einen loginName als Suchparameter angeben.", + "sessionExpired": "Ihre aktuelle Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.", + "failedLoading": "Daten konnten nicht geladen werden. Bitte versuchen Sie es erneut.", + "tryagain": "Erneut versuchen" + } + }, + "en":{ + "common": { + "back": "Back" + }, + "accounts": { + "title": "Accounts", + "description": "Select the account you want to use.", + "addAnother": "Add another account", + "noResults": "No accounts found" + }, + "loginname": { + "title": "Welcome back!", + "description": "Enter your login data.", + "register": "Register new user" + }, + "password": { + "verify": { + "title": "Password", + "description": "Enter your password.", + "resetPassword": "Reset Password", + "submit": "Continue" + }, + "set": { + "title": "Set Password", + "description": "Set the password for your account", + "codeSent": "A code has been sent to your email address.", + "noCodeReceived": "Didn't receive a code?", + "resend": "Resend code", + "submit": "Continue" + }, + "change": { + "title": "Change Password", + "description": "Set the password for your account", + "submit": "Continue" + } + }, + "idp": { + "title": "Sign in with SSO", + "description": "Select one of the following providers to sign in", + "signInWithApple": "Sign in with Apple", + "signInWithGoogle": "Sign in with Google", + "signInWithAzureAD": "Sign in with AzureAD", + "signInWithGithub": "Sign in with GitHub", + "signInWithGitlab": "Sign in with GitLab", + "loginSuccess": { + "title": "Login successful", + "description": "You have successfully been loggedIn!" + }, + "linkingSuccess": { + "title": "Account linked", + "description": "You have successfully linked your account!" + }, + "registerSuccess": { + "title": "Registration successful", + "description": "You have successfully registered!" + }, + "loginError": { + "title": "Login failed", + "description": "An error occurred while trying to login." + }, + "linkingError": { + "title": "Account linking failed", + "description": "An error occurred while trying to link your account." + } + }, + "mfa": { + "verify": { + "title": "Verify your identity", + "description": "Choose one of the following factors.", + "noResults": "No second factors available to setup." + }, + "set": { + "title": "Set up 2-Factor", + "description": "Choose one of the following second factors.", + "skip": "Skip" + } + }, + "otp": { + "verify": { + "title": "Verify 2-Factor", + "totpDescription": "Enter the code from your authenticator app.", + "smsDescription": "Enter the code you received via SMS.", + "emailDescription": "Enter the code you received via email.", + "noCodeReceived": "Didn't receive a code?", + "resendCode": "Resend code", + "submit": "Continue" + }, + "set": { + "title": "Set up 2-Factor", + "totpDescription": "Scan the QR code with your authenticator app.", + "smsDescription": "Enter your phone number to receive a code via SMS.", + "emailDescription": "Enter your email address to receive a code via email.", + "totpRegisterDescription": "Scan the QR Code or navigate to the URL manually.", + "submit": "Continue" + } + }, + "passkey": { + "verify": { + "title": "Authenticate with a passkey", + "description": "Your device will ask for your fingerprint, face, or screen lock", + "usePassword": "Use password", + "submit": "Continue" + }, + "set": { + "title": "Setup a passkey", + "description": "Your device will ask for your fingerprint, face, or screen lock", + "info": { + "description": "A passkey is an authentication method on a device like your fingerprint, Apple FaceID or similar. ", + "link": "Passwordless Authentication" + }, + "skip": "Skip", + "submit": "Continue" + } + }, + "u2f": { + "verify": { + "title": "Verify 2-Factor", + "description": "Verify your account with your device." + }, + "set": { + "title": "Set up 2-Factor", + "description": "Set up a device as a second factor.", + "submit": "Continue" + } + }, + "register": { + "methods": { + "passkey": "Passkey", + "password": "Password" + }, + "disabled": { + "title": "Registration disabled", + "description": "The registration is disabled. Please contact your administrator." + }, + "missingdata": { + "title": "Missing data", + "description": "Provide email, first and last name to register." + }, + "title": "Register", + "description": "Create your ZITADEL account.", + "selectMethod": "Select the method you would like to authenticate", + "agreeTo": "To register you must agree to the terms and conditions", + "termsOfService": "Terms of Service", + "privacyPolicy": "Privacy Policy", + "submit": "Continue", + "password": { + "title": "Set Password", + "description": "Set the password for your account", + "submit": "Continue" + } + }, + "invite": { + "title": "Invite User", + "description": "Provide the email address and the name of the user you want to invite.", + "info": "The user will receive an email with further instructions.", + "notAllowed": "Your settings do not allow you to invite users.", + "submit": "Continue", + "success": { + "title": "User invited", + "description": "The email has successfully been sent.", + "verified": "The user has been invited and has already verified his email.", + "notVerifiedYet": "The user has been invited. They will receive an email with further instructions.", + "submit": "Invite another user" + } + }, + "signedin": { + "title": "Welcome {user}!", + "description": "You are signed in.", + "continue": "Continue", + "error": { + "title": "Error", + "description": "An error occurred while trying to sign in." + } + }, + "verify": { + "userIdMissing": "No userId provided!", + "success": "The user has been verified successfully.", + "setupAuthenticator": "Setup authenticator", + "verify": { + "title": "Verify user", + "description": "Enter the Code provided in the verification email.", + "noCodeReceived": "Didn't receive a code?", + "resendCode": "Resend code", + "submit": "Continue" + } + }, + "authenticator": { + "title": "Choose authentication method", + "description": "Select the method you would like to authenticate", + "noMethodsAvailable": "No authentication methods available", + "allSetup": "You have already setup an authenticator!", + "linkWithIDP": "or link with an Identity Provider" + }, + "device": { + "usercode": { + "title": "Device code", + "description": "Enter the code displayed on your app or device.", + "submit": "Continue" + }, + "request": { + "title": "{appName} would like to connect", + "description": "{appName} will have access to:", + "disclaimer": "By clicking Allow, you allow {appName} and Zitadel to use your information in accordance with their respective terms of service and privacy policies. You can revoke this access at any time.", + "submit": "Allow", + "deny": "Deny" + }, + "scope": { + "openid": "Verify your identity.", + "email": "View your email address.", + "profile": "View your full profile information.", + "offline_access": "Allow offline access to your account." + } + }, + "error": { + "noUserCode": "No user code provided!", + "noDeviceRequest": "No device request found.", + "unknownContext": "Could not get the context of the user. Make sure to enter the username first or provide a loginName as searchParam.", + "sessionExpired": "Your current session has expired. Please login again.", + "failedLoading": "Failed to load data. Please try again.", + "tryagain": "Try Again" + } + }, + "es":{ + "common": { + "back": "Atrás" + }, + "accounts": { + "title": "Cuentas", + "description": "Selecciona la cuenta que deseas usar.", + "addAnother": "Agregar otra cuenta", + "noResults": "No se encontraron cuentas" + }, + "loginname": { + "title": "¡Bienvenido de nuevo!", + "description": "Introduce tus datos de acceso.", + "register": "Registrar nuevo usuario" + }, + "password": { + "verify": { + "title": "Contraseña", + "description": "Introduce tu contraseña.", + "resetPassword": "Restablecer contraseña", + "submit": "Continuar" + }, + "set": { + "title": "Establecer Contraseña", + "description": "Establece la contraseña para tu cuenta", + "codeSent": "Se ha enviado un código a su correo electrónico.", + "noCodeReceived": "¿No recibiste un código?", + "resend": "Reenviar código", + "submit": "Continuar" + }, + "change": { + "title": "Cambiar Contraseña", + "description": "Establece la contraseña para tu cuenta", + "submit": "Continuar" + } + }, + "idp": { + "title": "Iniciar sesión con SSO", + "description": "Selecciona uno de los siguientes proveedores para iniciar sesión", + "signInWithApple": "Iniciar sesión con Apple", + "signInWithGoogle": "Iniciar sesión con Google", + "signInWithAzureAD": "Iniciar sesión con AzureAD", + "signInWithGithub": "Iniciar sesión con GitHub", + "signInWithGitlab": "Iniciar sesión con GitLab", + "loginSuccess": { + "title": "Inicio de sesión exitoso", + "description": "¡Has iniciado sesión con éxito!" + }, + "linkingSuccess": { + "title": "Cuenta vinculada", + "description": "¡Has vinculado tu cuenta con éxito!" + }, + "registerSuccess": { + "title": "Registro exitoso", + "description": "¡Te has registrado con éxito!" + }, + "loginError": { + "title": "Error de inicio de sesión", + "description": "Ocurrió un error al intentar iniciar sesión." + }, + "linkingError": { + "title": "Error al vincular la cuenta", + "description": "Ocurrió un error al intentar vincular tu cuenta." + } + }, + "mfa": { + "verify": { + "title": "Verifica tu identidad", + "description": "Elige uno de los siguientes factores.", + "noResults": "No hay factores secundarios disponibles para configurar." + }, + "set": { + "title": "Configurar autenticación de 2 factores", + "description": "Elige uno de los siguientes factores secundarios.", + "skip": "Omitir" + } + }, + "otp": { + "verify": { + "title": "Verificar autenticación de 2 factores", + "totpDescription": "Introduce el código de tu aplicación de autenticación.", + "smsDescription": "Introduce el código que recibiste por SMS.", + "emailDescription": "Introduce el código que recibiste por correo electrónico.", + "noCodeReceived": "¿No recibiste un código?", + "resendCode": "Reenviar código", + "submit": "Continuar" + }, + "set": { + "title": "Configurar autenticación de 2 factores", + "totpDescription": "Escanea el código QR con tu aplicación de autenticación.", + "smsDescription": "Introduce tu número de teléfono para recibir un código por SMS.", + "emailDescription": "Introduce tu dirección de correo electrónico para recibir un código por correo electrónico.", + "totpRegisterDescription": "Escanea el código QR o navega manualmente a la URL.", + "submit": "Continuar" + } + }, + "passkey": { + "verify": { + "title": "Autenticar con una clave de acceso", + "description": "Tu dispositivo pedirá tu huella digital, rostro o bloqueo de pantalla", + "usePassword": "Usar contraseña", + "submit": "Continuar" + }, + "set": { + "title": "Configurar una clave de acceso", + "description": "Tu dispositivo pedirá tu huella digital, rostro o bloqueo de pantalla", + "info": { + "description": "Una clave de acceso es un método de autenticación en un dispositivo como tu huella digital, Apple FaceID o similar.", + "link": "Autenticación sin contraseña" + }, + "skip": "Omitir", + "submit": "Continuar" + } + }, + "u2f": { + "verify": { + "title": "Verificar autenticación de 2 factores", + "description": "Verifica tu cuenta con tu dispositivo." + }, + "set": { + "title": "Configurar autenticación de 2 factores", + "description": "Configura un dispositivo como segundo factor.", + "submit": "Continuar" + } + }, + "register": { + "methods": { + "passkey": "Clave de acceso", + "password": "Contraseña" + }, + "disabled": { + "title": "Registro deshabilitado", + "description": "Registrarse está deshabilitado en este momento." + }, + "missingdata": { + "title": "Datos faltantes", + "description": "No se proporcionaron datos suficientes para el registro." + }, + "title": "Registrarse", + "description": "Crea tu cuenta ZITADEL.", + "selectMethod": "Selecciona el método con el que deseas autenticarte", + "agreeTo": "Para registrarte debes aceptar los términos y condiciones", + "termsOfService": "Términos de Servicio", + "privacyPolicy": "Política de Privacidad", + "submit": "Continuar", + "password": { + "title": "Establecer Contraseña", + "description": "Establece la contraseña para tu cuenta", + "submit": "Continuar" + } + }, + "invite": { + "title": "Invitar usuario", + "description": "Introduce el correo electrónico del usuario que deseas invitar.", + "info": "El usuario recibirá un correo electrónico con un enlace para completar el registro.", + "notAllowed": "No tienes permiso para invitar usuarios.", + "submit": "Invitar usuario", + "success": { + "title": "¡Usuario invitado!", + "description": "El usuario ha sido invitado.", + "verified": "El usuario ha sido invitado y ya ha verificado su correo electrónico.", + "notVerifiedYet": "El usuario ha sido invitado. Recibirá un correo electrónico con más instrucciones.", + "submit": "Invitar a otro usuario" + } + }, + "signedin": { + "title": "¡Bienvenido {user}!", + "description": "Has iniciado sesión.", + "continue": "Continuar", + "error": { + "title": "Error", + "description": "Ocurrió un error al iniciar sesión." + } + }, + "verify": { + "userIdMissing": "¡No se proporcionó userId!", + "success": "¡Verificación exitosa!", + "setupAuthenticator": "Configurar autenticador", + "verify": { + "title": "Verificar usuario", + "description": "Introduce el código proporcionado en el correo electrónico de verificación.", + "noCodeReceived": "¿No recibiste un código?", + "resendCode": "Reenviar código", + "submit": "Continuar" + } + }, + "authenticator": { + "title": "Seleccionar método de autenticación", + "description": "Selecciona el método con el que deseas autenticarte", + "noMethodsAvailable": "No hay métodos de autenticación disponibles", + "allSetup": "¡Ya has configurado un autenticador!", + "linkWithIDP": "o vincúlalo con un proveedor de identidad" + }, + "device": { + "usercode": { + "title": "Código del dispositivo", + "description": "Introduce el código.", + "submit": "Continuar" + }, + "request": { + "title": "{appName} desea conectarse:", + "description": "{appName} tendrá acceso a:", + "disclaimer": "Al hacer clic en Permitir, autorizas a {appName} y a Zitadel a usar tu información de acuerdo con sus respectivos términos de servicio y políticas de privacidad. Puedes revocar este acceso en cualquier momento.", + "submit": "Permitir", + "deny": "Denegar" + }, + "scope": { + "openid": "Verifica tu identidad.", + "email": "Accede a tu dirección de correo electrónico.", + "profile": "Accede a la información completa de tu perfil.", + "offline_access": "Permitir acceso sin conexión a tu cuenta." + } + }, + "error": { + "noUserCode": "¡No se proporcionó código de usuario!", + "noDeviceRequest": "No se encontró ninguna solicitud de dispositivo.", + "unknownContext": "No se pudo obtener el contexto del usuario. Asegúrate de ingresar primero el nombre de usuario o proporcionar un loginName como parámetro de búsqueda.", + "sessionExpired": "Tu sesión actual ha expirado. Por favor, inicia sesión de nuevo.", + "failedLoading": "No se pudieron cargar los datos. Por favor, inténtalo de nuevo.", + "tryagain": "Intentar de nuevo" + } + }, + "it":{ + "common": { + "back": "Indietro" + }, + "accounts": { + "title": "Account", + "description": "Seleziona l'account che desideri utilizzare.", + "addAnother": "Aggiungi un altro account", + "noResults": "Nessun account trovato" + }, + "loginname": { + "title": "Bentornato!", + "description": "Inserisci i tuoi dati di accesso.", + "register": "Registrati come nuovo utente" + }, + "password": { + "verify": { + "title": "Password", + "description": "Inserisci la tua password.", + "resetPassword": "Reimposta Password", + "submit": "Continua" + }, + "set": { + "title": "Imposta Password", + "description": "Imposta la password per il tuo account", + "codeSent": "Un codice è stato inviato al tuo indirizzo email.", + "noCodeReceived": "Non hai ricevuto un codice?", + "resend": "Invia di nuovo", + "submit": "Continua" + }, + "change": { + "title": "Cambia Password", + "description": "Imposta la password per il tuo account", + "submit": "Continua" + } + }, + "idp": { + "title": "Accedi con SSO", + "description": "Seleziona uno dei seguenti provider per accedere", + "signInWithApple": "Accedi con Apple", + "signInWithGoogle": "Accedi con Google", + "signInWithAzureAD": "Accedi con AzureAD", + "signInWithGithub": "Accedi con GitHub", + "signInWithGitlab": "Accedi con GitLab", + "loginSuccess": { + "title": "Accesso riuscito", + "description": "Accesso effettuato con successo!" + }, + "linkingSuccess": { + "title": "Account collegato", + "description": "Hai collegato con successo il tuo account!" + }, + "registerSuccess": { + "title": "Registrazione riuscita", + "description": "Registrazione effettuata con successo!" + }, + "loginError": { + "title": "Accesso fallito", + "description": "Si è verificato un errore durante il tentativo di accesso." + }, + "linkingError": { + "title": "Collegamento account fallito", + "description": "Si è verificato un errore durante il tentativo di collegare il tuo account." + } + }, + "mfa": { + "verify": { + "title": "Verifica la tua identità", + "description": "Scegli uno dei seguenti fattori.", + "noResults": "Nessun secondo fattore disponibile per la configurazione." + }, + "set": { + "title": "Configura l'autenticazione a 2 fattori", + "description": "Scegli uno dei seguenti secondi fattori.", + "skip": "Salta" + } + }, + "otp": { + "verify": { + "title": "Verifica l'autenticazione a 2 fattori", + "totpDescription": "Inserisci il codice dalla tua app di autenticazione.", + "smsDescription": "Inserisci il codice ricevuto via SMS.", + "emailDescription": "Inserisci il codice ricevuto via email.", + "noCodeReceived": "Non hai ricevuto un codice?", + "resendCode": "Invia di nuovo il codice", + "submit": "Continua" + }, + "set": { + "title": "Configura l'autenticazione a 2 fattori", + "totpDescription": "Scansiona il codice QR con la tua app di autenticazione.", + "smsDescription": "Inserisci il tuo numero di telefono per ricevere un codice via SMS.", + "emailDescription": "Inserisci il tuo indirizzo email per ricevere un codice via email.", + "totpRegisterDescription": "Scansiona il codice QR o naviga manualmente all'URL.", + "submit": "Continua" + } + }, + "passkey": { + "verify": { + "title": "Autenticati con una passkey", + "description": "Il tuo dispositivo chiederà la tua impronta digitale, il volto o il blocco schermo", + "usePassword": "Usa password", + "submit": "Continua" + }, + "set": { + "title": "Configura una passkey", + "description": "Il tuo dispositivo chiederà la tua impronta digitale, il volto o il blocco schermo", + "info": { + "description": "Una passkey è un metodo di autenticazione su un dispositivo come la tua impronta digitale, Apple FaceID o simili.", + "link": "Autenticazione senza password" + }, + "skip": "Salta", + "submit": "Continua" + } + }, + "u2f": { + "verify": { + "title": "Verifica l'autenticazione a 2 fattori", + "description": "Verifica il tuo account con il tuo dispositivo." + }, + "set": { + "title": "Configura l'autenticazione a 2 fattori", + "description": "Configura un dispositivo come secondo fattore.", + "submit": "Continua" + } + }, + "register": { + "methods": { + "passkey": "Passkey", + "password": "Password" + }, + "disabled": { + "title": "Registration disabled", + "description": "Registrazione disabilitata. Contatta l'amministratore di sistema per assistenza." + }, + "missingdata": { + "title": "Registrazione", + "description": "Inserisci i tuoi dati per registrarti." + }, + "title": "Registrati", + "description": "Crea il tuo account ZITADEL.", + "selectMethod": "Seleziona il metodo con cui desideri autenticarti", + "agreeTo": "Per registrarti devi accettare i termini e le condizioni", + "termsOfService": "Termini di Servizio", + "privacyPolicy": "Informativa sulla Privacy", + "submit": "Continua", + "password": { + "title": "Imposta Password", + "description": "Imposta la password per il tuo account", + "submit": "Continua" + } + }, + "invite": { + "title": "Invita Utente", + "description": "Inserisci l'indirizzo email dell'utente che desideri invitare.", + "info": "L'utente riceverà un'email con ulteriori istruzioni.", + "notAllowed": "Non hai i permessi per invitare un utente.", + "submit": "Invita Utente", + "success": { + "title": "Invito inviato", + "description": "L'utente è stato invitato con successo.", + "verified": "L'utente è stato invitato e ha già verificato la sua email.", + "notVerifiedYet": "L'utente è stato invitato. Riceverà un'email con ulteriori istruzioni.", + "submit": "Invita un altro utente" + } + }, + "signedin": { + "title": "Benvenuto {user}!", + "description": "Sei connesso.", + "continue": "Continua", + "error": { + "title": "Errore", + "description": "Si è verificato un errore durante il tentativo di accesso." + } + }, + "verify": { + "userIdMissing": "Nessun userId fornito!", + "success": "Verifica effettuata con successo!", + "setupAuthenticator": "Configura autenticatore", + "verify": { + "title": "Verifica utente", + "description": "Inserisci il codice fornito nell'email di verifica.", + "noCodeReceived": "Non hai ricevuto un codice?", + "resendCode": "Invia di nuovo il codice", + "submit": "Continua" + } + }, + "authenticator": { + "title": "Seleziona metodo di autenticazione", + "description": "Seleziona il metodo con cui desideri autenticarti", + "noMethodsAvailable": "Nessun metodo di autenticazione disponibile", + "allSetup": "Hai già configurato un autenticatore!", + "linkWithIDP": "o collega con un Identity Provider" + }, + "device": { + "usercode": { + "title": "Codice dispositivo", + "description": "Inserisci il codice.", + "submit": "Continua" + }, + "request": { + "title": "{appName} desidera connettersi:", + "description": "{appName} avrà accesso a:", + "disclaimer": "Cliccando su Consenti, autorizzi {appName} e Zitadel a utilizzare le tue informazioni in conformità con i rispettivi termini di servizio e politiche sulla privacy. Puoi revocare questo accesso in qualsiasi momento.", + "submit": "Consenti", + "deny": "Nega" + }, + "scope": { + "openid": "Verifica la tua identità.", + "email": "Accedi al tuo indirizzo email.", + "profile": "Accedi alle informazioni complete del tuo profilo.", + "offline_access": "Consenti l'accesso offline al tuo account." + } + }, + "error": { + "noUserCode": "Nessun codice utente fornito!", + "noDeviceRequest": "Nessuna richiesta di dispositivo trovata.", + "unknownContext": "Impossibile ottenere il contesto dell'utente. Assicurati di inserire prima il nome utente o di fornire un loginName come parametro di ricerca.", + "sessionExpired": "La tua sessione attuale è scaduta. Effettua nuovamente l'accesso.", + "failedLoading": "Impossibile caricare i dati. Riprova.", + "tryagain": "Riprova" + } + + }, + "pl":{ + "common": { + "back": "Powrót" + }, + "accounts": { + "title": "Konta", + "description": "Wybierz konto, którego chcesz użyć.", + "addAnother": "Dodaj kolejne konto", + "noResults": "Nie znaleziono kont" + }, + "loginname": { + "title": "Witamy ponownie!", + "description": "Wprowadź dane logowania.", + "register": "Zarejestruj nowego użytkownika" + }, + "password": { + "verify": { + "title": "Hasło", + "description": "Wprowadź swoje hasło.", + "resetPassword": "Zresetuj hasło", + "submit": "Kontynuuj" + }, + "set": { + "title": "Ustaw hasło", + "description": "Ustaw hasło dla swojego konta", + "codeSent": "Kod został wysłany na twój adres e-mail.", + "noCodeReceived": "Nie otrzymałeś kodu?", + "resend": "Wyślij kod ponownie", + "submit": "Kontynuuj" + }, + "change": { + "title": "Zmień hasło", + "description": "Ustaw nowe hasło dla swojego konta", + "submit": "Kontynuuj" + } + }, + "idp": { + "title": "Zaloguj się za pomocą SSO", + "description": "Wybierz jednego z poniższych dostawców, aby się zalogować", + "signInWithApple": "Zaloguj się przez Apple", + "signInWithGoogle": "Zaloguj się przez Google", + "signInWithAzureAD": "Zaloguj się przez AzureAD", + "signInWithGithub": "Zaloguj się przez GitHub", + "signInWithGitlab": "Zaloguj się przez GitLab", + "loginSuccess": { + "title": "Logowanie udane", + "description": "Zostałeś pomyślnie zalogowany!" + }, + "linkingSuccess": { + "title": "Konto powiązane", + "description": "Pomyślnie powiązałeś swoje konto!" + }, + "registerSuccess": { + "title": "Rejestracja udana", + "description": "Pomyślnie się zarejestrowałeś!" + }, + "loginError": { + "title": "Logowanie nieudane", + "description": "Wystąpił błąd podczas próby logowania." + }, + "linkingError": { + "title": "Powiązanie konta nie powiodło się", + "description": "Wystąpił błąd podczas próby powiązania konta." + } + }, + "mfa": { + "verify": { + "title": "Zweryfikuj swoją tożsamość", + "description": "Wybierz jeden z poniższych sposobów weryfikacji.", + "noResults": "Nie znaleziono dostępnych metod uwierzytelniania dwuskładnikowego." + }, + "set": { + "title": "Skonfiguruj uwierzytelnianie dwuskładnikowe", + "description": "Wybierz jedną z poniższych metod drugiego czynnika.", + "skip": "Pomiń" + } + }, + "otp": { + "verify": { + "title": "Zweryfikuj uwierzytelnianie dwuskładnikowe", + "totpDescription": "Wprowadź kod z aplikacji uwierzytelniającej.", + "smsDescription": "Wprowadź kod otrzymany SMS-em.", + "emailDescription": "Wprowadź kod otrzymany e-mailem.", + "noCodeReceived": "Nie otrzymałeś kodu?", + "resendCode": "Wyślij kod ponownie", + "submit": "Kontynuuj" + }, + "set": { + "title": "Skonfiguruj uwierzytelnianie dwuskładnikowe", + "totpDescription": "Zeskanuj kod QR za pomocą aplikacji uwierzytelniającej.", + "smsDescription": "Wprowadź swój numer telefonu, aby otrzymać kod SMS-em.", + "emailDescription": "Wprowadź swój adres e-mail, aby otrzymać kod e-mailem.", + "totpRegisterDescription": "Zeskanuj kod QR lub otwórz adres URL ręcznie.", + "submit": "Kontynuuj" + } + }, + "passkey": { + "verify": { + "title": "Uwierzytelnij się za pomocą klucza dostępu", + "description": "Twoje urządzenie poprosi o użycie odcisku palca, rozpoznawania twarzy lub blokady ekranu.", + "usePassword": "Użyj hasła", + "submit": "Kontynuuj" + }, + "set": { + "title": "Skonfiguruj klucz dostępu", + "description": "Twoje urządzenie poprosi o użycie odcisku palca, rozpoznawania twarzy lub blokady ekranu.", + "info": { + "description": "Klucz dostępu to metoda uwierzytelniania na urządzeniu, wykorzystująca np. odcisk palca, Apple FaceID lub podobne rozwiązania.", + "link": "Uwierzytelnianie bez hasła" + }, + "skip": "Pomiń", + "submit": "Kontynuuj" + } + }, + "u2f": { + "verify": { + "title": "Zweryfikuj uwierzytelnianie dwuskładnikowe", + "description": "Zweryfikuj swoje konto za pomocą urządzenia." + }, + "set": { + "title": "Skonfiguruj uwierzytelnianie dwuskładnikowe", + "description": "Skonfiguruj urządzenie jako dodatkowy czynnik uwierzytelniania.", + "submit": "Kontynuuj" + } + }, + "register": { + "methods": { + "passkey": "Klucz dostępu", + "password": "Hasło" + }, + "disabled": { + "title": "Rejestracja wyłączona", + "description": "Rejestracja jest wyłączona. Skontaktuj się z administratorem." + }, + "missingdata": { + "title": "Brak danych", + "description": "Podaj e-mail, imię i nazwisko, aby się zarejestrować." + }, + "title": "Rejestracja", + "description": "Utwórz konto ZITADEL.", + "selectMethod": "Wybierz metodę uwierzytelniania, której chcesz użyć", + "agreeTo": "Aby się zarejestrować, musisz zaakceptować warunki korzystania", + "termsOfService": "Regulamin", + "privacyPolicy": "Polityka prywatności", + "submit": "Kontynuuj", + "password": { + "title": "Ustaw hasło", + "description": "Ustaw hasło dla swojego konta", + "submit": "Kontynuuj" + } + }, + "invite": { + "title": "Zaproś użytkownika", + "description": "Podaj adres e-mail oraz imię i nazwisko użytkownika, którego chcesz zaprosić.", + "info": "Użytkownik otrzyma e-mail z dalszymi instrukcjami.", + "notAllowed": "Twoje ustawienia nie pozwalają na zapraszanie użytkowników.", + "submit": "Kontynuuj", + "success": { + "title": "Użytkownik zaproszony", + "description": "E-mail został pomyślnie wysłany.", + "verified": "Użytkownik został zaproszony i już zweryfikował swój e-mail.", + "notVerifiedYet": "Użytkownik został zaproszony. Otrzyma e-mail z dalszymi instrukcjami.", + "submit": "Zaproś kolejnego użytkownika" + } + }, + "signedin": { + "title": "Witaj {user}!", + "description": "Jesteś zalogowany.", + "continue": "Kontynuuj", + "error": { + "title": "Błąd", + "description": "Nie można załadować danych. Sprawdź połączenie z internetem lub spróbuj ponownie później." + } + }, + "verify": { + "userIdMissing": "Nie podano identyfikatora użytkownika!", + "success": "Użytkownik został pomyślnie zweryfikowany.", + "setupAuthenticator": "Skonfiguruj uwierzytelnianie", + "verify": { + "title": "Zweryfikuj użytkownika", + "description": "Wprowadź kod z wiadomości weryfikacyjnej.", + "noCodeReceived": "Nie otrzymałeś kodu?", + "resendCode": "Wyślij kod ponownie", + "submit": "Kontynuuj" + } + }, + "authenticator": { + "title": "Wybierz metodę uwierzytelniania", + "description": "Wybierz metodę, której chcesz użyć do uwierzytelnienia.", + "noMethodsAvailable": "Brak dostępnych metod uwierzytelniania", + "allSetup": "Już skonfigurowałeś metodę uwierzytelniania!", + "linkWithIDP": "lub połącz z dostawcą tożsamości" + }, + "device": { + "usercode": { + "title": "Kod urządzenia", + "description": "Wprowadź kod.", + "submit": "Kontynuuj" + }, + "request": { + "title": "{appName} chce się połączyć:", + "description": "{appName} będzie miało dostęp do:", + "disclaimer": "Klikając Zezwól, pozwalasz tej aplikacji i Zitadel na korzystanie z Twoich informacji zgodnie z ich odpowiednimi warunkami użytkowania i politykami prywatności. Możesz cofnąć ten dostęp w dowolnym momencie.", + "submit": "Zezwól", + "deny": "Odmów" + }, + "scope": { + "openid": "Zweryfikuj swoją tożsamość.", + "email": "Uzyskaj dostęp do swojego adresu e-mail.", + "profile": "Uzyskaj dostęp do pełnych informacji o swoim profilu.", + "offline_access": "Zezwól na dostęp offline do swojego konta." + } + }, + "error": { + "noUserCode": "Nie podano kodu użytkownika!", + "noDeviceRequest": "Nie znaleziono żądania urządzenia.", + "unknownContext": "Nie udało się pobrać kontekstu użytkownika. Upewnij się, że najpierw wprowadziłeś nazwę użytkownika lub podałeś login jako parametr wyszukiwania.", + "sessionExpired": "Twoja sesja wygasła. Zaloguj się ponownie.", + "failedLoading": "Nie udało się załadować danych. Spróbuj ponownie.", + "tryagain": "Spróbuj ponownie" + } + }, + "ru":{ + "common": { + "back": "Назад" + }, + "accounts": { + "title": "Аккаунты", + "description": "Выберите аккаунт, который хотите использовать.", + "addAnother": "Добавить другой аккаунт", + "noResults": "Аккаунты не найдены" + }, + "loginname": { + "title": "С возвращением!", + "description": "Введите свои данные для входа.", + "register": "Зарегистрировать нового пользователя" + }, + "password": { + "verify": { + "title": "Пароль", + "description": "Введите ваш пароль.", + "resetPassword": "Сбросить пароль", + "submit": "Продолжить" + }, + "set": { + "title": "Установить пароль", + "description": "Установите пароль для вашего аккаунта", + "codeSent": "Код отправлен на ваш адрес электронной почты.", + "noCodeReceived": "Не получили код?", + "resend": "Отправить код повторно", + "submit": "Продолжить" + }, + "change": { + "title": "Изменить пароль", + "description": "Установите пароль для вашего аккаунта", + "submit": "Продолжить" + } + }, + "idp": { + "title": "Войти через SSO", + "description": "Выберите одного из провайдеров для входа", + "signInWithApple": "Войти через Apple", + "signInWithGoogle": "Войти через Google", + "signInWithAzureAD": "Войти через AzureAD", + "signInWithGithub": "Войти через GitHub", + "signInWithGitlab": "Войти через GitLab", + "loginSuccess": { + "title": "Вход выполнен успешно", + "description": "Вы успешно вошли в систему!" + }, + "linkingSuccess": { + "title": "Аккаунт привязан", + "description": "Аккаунт успешно привязан!" + }, + "registerSuccess": { + "title": "Регистрация завершена", + "description": "Вы успешно зарегистрировались!" + }, + "loginError": { + "title": "Ошибка входа", + "description": "Произошла ошибка при попытке входа." + }, + "linkingError": { + "title": "Ошибка привязки аккаунта", + "description": "Произошла ошибка при попытке привязать аккаунт." + } + }, + "mfa": { + "verify": { + "title": "Подтвердите вашу личность", + "description": "Выберите один из следующих факторов.", + "noResults": "Нет доступных методов двухфакторной аутентификации" + }, + "set": { + "title": "Настройка двухфакторной аутентификации", + "description": "Выберите один из следующих методов.", + "skip": "Пропустить" + } + }, + "otp": { + "verify": { + "title": "Подтверждение 2FA", + "totpDescription": "Введите код из приложения-аутентификатора.", + "smsDescription": "Введите код, полученный по SMS.", + "emailDescription": "Введите код, полученный по email.", + "noCodeReceived": "Не получили код?", + "resendCode": "Отправить код повторно", + "submit": "Продолжить" + }, + "set": { + "title": "Настройка двухфакторной аутентификации", + "totpDescription": "Отсканируйте QR-код в приложении-аутентификаторе.", + "smsDescription": "Введите номер телефона для получения кода по SMS.", + "emailDescription": "Введите email для получения кода.", + "totpRegisterDescription": "Отсканируйте QR-код или перейдите по ссылке вручную.", + "submit": "Продолжить" + } + }, + "passkey": { + "verify": { + "title": "Аутентификация с помощью пасскей", + "description": "Устройство запросит отпечаток пальца, лицо или экранный замок", + "usePassword": "Использовать пароль", + "submit": "Продолжить" + }, + "set": { + "title": "Настройка пасскей", + "description": "Устройство запросит отпечаток пальца, лицо или экранный замок", + "info": { + "description": "Пасскей — метод аутентификации через устройство (отпечаток пальца, Apple FaceID и аналоги).", + "link": "Аутентификация без пароля" + }, + "skip": "Пропустить", + "submit": "Продолжить" + } + }, + "u2f": { + "verify": { + "title": "Подтверждение 2FA", + "description": "Подтвердите аккаунт с помощью устройства." + }, + "set": { + "title": "Настройка двухфакторной аутентификации", + "description": "Настройте устройство как второй фактор.", + "submit": "Продолжить" + } + }, + "register": { + "methods": { + "passkey": "Пасскей", + "password": "Пароль" + }, + "disabled": { + "title": "Регистрация отключена", + "description": "Регистрация недоступна. Обратитесь к администратору." + }, + "missingdata": { + "title": "Недостаточно данных", + "description": "Укажите email, имя и фамилию для регистрации." + }, + "title": "Регистрация", + "description": "Создайте свой аккаунт ZITADEL.", + "selectMethod": "Выберите метод аутентификации", + "agreeTo": "Для регистрации необходимо принять условия:", + "termsOfService": "Условия использования", + "privacyPolicy": "Политика конфиденциальности", + "submit": "Продолжить", + "password": { + "title": "Установить пароль", + "description": "Установите пароль для вашего аккаунта", + "submit": "Продолжить" + } + }, + "invite": { + "title": "Пригласить пользователя", + "description": "Укажите email и имя пользователя для приглашения.", + "info": "Пользователь получит email с инструкциями.", + "notAllowed": "Ваши настройки не позволяют приглашать пользователей.", + "submit": "Продолжить", + "success": { + "title": "Пользователь приглашён", + "description": "Письмо успешно отправлено.", + "verified": "Пользователь приглашён и уже подтвердил email.", + "notVerifiedYet": "Пользователь приглашён. Он получит email с инструкциями.", + "submit": "Пригласить другого пользователя" + } + }, + "signedin": { + "title": "Добро пожаловать, {user}!", + "description": "Вы вошли в систему.", + "continue": "Продолжить", + "error": { + "title": "Ошибка", + "description": "Не удалось войти в систему. Проверьте свои данные и попробуйте снова." + } + }, + "verify": { + "userIdMissing": "Не указан userId!", + "success": "Пользователь успешно подтверждён.", + "setupAuthenticator": "Настроить аутентификатор", + "verify": { + "title": "Подтверждение пользователя", + "description": "Введите код из письма подтверждения.", + "noCodeReceived": "Не получили код?", + "resendCode": "Отправить код повторно", + "submit": "Продолжить" + } + }, + "authenticator": { + "title": "Выбор метода аутентификации", + "description": "Выберите предпочитаемый метод аутентификации", + "noMethodsAvailable": "Нет доступных методов аутентификации", + "allSetup": "Аутентификатор уже настроен!", + "linkWithIDP": "или привязать через Identity Provider" + }, + "device": { + "usercode": { + "title": "Код устройства", + "description": "Введите код.", + "submit": "Продолжить" + }, + "request": { + "title": "{appName} хочет подключиться:", + "description": "{appName} получит доступ к:", + "disclaimer": "Нажимая «Разрешить», вы разрешаете этому приложению и Zitadel использовать вашу информацию в соответствии с их условиями использования и политиками конфиденциальности. Вы можете отозвать этот доступ в любое время.", + "submit": "Разрешить", + "deny": "Запретить" + }, + "scope": { + "openid": "Проверка вашей личности.", + "email": "Доступ к вашему адресу электронной почты.", + "profile": "Доступ к полной информации вашего профиля.", + "offline_access": "Разрешить офлайн-доступ к вашему аккаунту." + } + }, + "error": { + "noUserCode": "Не указан код пользователя!", + "noDeviceRequest": "Не найдена ни одна заявка на устройство.", + "unknownContext": "Не удалось получить контекст пользователя. Укажите имя пользователя или loginName в параметрах поиска.", + "sessionExpired": "Ваша сессия истекла. Войдите снова.", + "failedLoading": "Ошибка загрузки данных. Попробуйте ещё раз.", + "tryagain": "Попробовать снова" + } + }, + "zh":{ + "common": { + "back": "返回" + }, + "accounts": { + "title": "账户", + "description": "选择您想使用的账户。", + "addAnother": "添加另一个账户", + "noResults": "未找到账户" + }, + "loginname": { + "title": "欢迎回来!", + "description": "请输入您的登录信息。", + "register": "注册新用户" + }, + "password": { + "verify": { + "title": "密码", + "description": "请输入您的密码。", + "resetPassword": "重置密码", + "submit": "继续" + }, + "set": { + "title": "设置密码", + "description": "为您的账户设置密码", + "codeSent": "验证码已发送到您的邮箱。", + "noCodeReceived": "没有收到验证码?", + "resend": "重发验证码", + "submit": "继续" + }, + "change": { + "title": "更改密码", + "description": "为您的账户设置密码", + "submit": "继续" + } + }, + "idp": { + "title": "使用 SSO 登录", + "description": "选择以下提供商中的一个进行登录", + "signInWithApple": "用 Apple 登录", + "signInWithGoogle": "用 Google 登录", + "signInWithAzureAD": "用 AzureAD 登录", + "signInWithGithub": "用 GitHub 登录", + "signInWithGitlab": "用 GitLab 登录", + "loginSuccess": { + "title": "登录成功", + "description": "您已成功登录!" + }, + "linkingSuccess": { + "title": "账户已链接", + "description": "您已成功链接您的账户!" + }, + "registerSuccess": { + "title": "注册成功", + "description": "您已成功注册!" + }, + "loginError": { + "title": "登录失败", + "description": "登录时发生错误。" + }, + "linkingError": { + "title": "账户链接失败", + "description": "链接账户时发生错误。" + } + }, + "mfa": { + "verify": { + "title": "验证您的身份", + "description": "选择以下的一个因素。", + "noResults": "没有可设置的第二因素。" + }, + "set": { + "title": "设置双因素认证", + "description": "选择以下的一个第二因素。", + "skip": "跳过" + } + }, + "otp": { + "verify": { + "title": "验证双因素", + "totpDescription": "请输入认证应用程序中的验证码。", + "smsDescription": "输入通过短信收到的验证码。", + "emailDescription": "输入通过电子邮件收到的验证码。", + "noCodeReceived": "没有收到验证码?", + "resendCode": "重发验证码", + "submit": "继续" + }, + "set": { + "title": "设置双因素认证", + "totpDescription": "使用认证应用程序扫描二维码。", + "smsDescription": "输入您的电话号码以接收短信验证码。", + "emailDescription": "输入您的电子邮箱地址以接收电子邮件验证码。", + "totpRegisterDescription": "扫描二维码或手动导航到URL。", + "submit": "继续" + } + }, + "passkey": { + "verify": { + "title": "使用密钥认证", + "description": "您的设备将请求指纹、面部识别或屏幕锁", + "usePassword": "使用密码", + "submit": "继续" + }, + "set": { + "title": "设置密钥", + "description": "您的设备将请求指纹、面部识别或屏幕锁", + "info": { + "description": "密钥是在设备上如指纹、Apple FaceID 或类似的认证方法。", + "link": "无密码认证" + }, + "skip": "跳过", + "submit": "继续" + } + }, + "u2f": { + "verify": { + "title": "验证双因素", + "description": "使用您的设备验证帐户。" + }, + "set": { + "title": "设置双因素认证", + "description": "设置设备为第二因素。", + "submit": "继续" + } + }, + "register": { + "methods": { + "passkey": "密钥", + "password": "密码" + }, + "disabled": { + "title": "注册已禁用", + "description": "您的设置不允许注册新用户。" + }, + "missingdata": { + "title": "缺少数据", + "description": "请提供所有必需的数据。" + }, + "title": "注册", + "description": "创建您的 ZITADEL 账户。", + "selectMethod": "选择您想使用的认证方法", + "agreeTo": "注册即表示您同意条款和条件", + "termsOfService": "服务条款", + "privacyPolicy": "隐私政策", + "submit": "继续", + "password": { + "title": "设置密码", + "description": "为您的账户设置密码", + "submit": "继续" + } + }, + "invite": { + "title": "邀请用户", + "description": "提供您想邀请的用户的电子邮箱地址和姓名。", + "info": "用户将收到一封包含进一步说明的电子邮件。", + "notAllowed": "您的设置不允许邀请用户。", + "submit": "继续", + "success": { + "title": "用户已邀请", + "description": "邮件已成功发送。", + "verified": "用户已被邀请并已验证其电子邮件。", + "notVerifiedYet": "用户已被邀请。他们将收到一封包含进一步说明的电子邮件。", + "submit": "邀请另一位用户" + } + }, + "signedin": { + "title": "欢迎 {user}!", + "description": "您已登录。", + "continue": "继续", + "error": { + "title": "错误", + "description": "登录时发生错误。" + } + }, + "verify": { + "userIdMissing": "未提供用户 ID!", + "success": "用户验证成功。", + "setupAuthenticator": "设置认证器", + "verify": { + "title": "验证用户", + "description": "输入验证邮件中的验证码。", + "noCodeReceived": "没有收到验证码?", + "resendCode": "重发验证码", + "submit": "继续" + } + }, + "authenticator": { + "title": "选择认证方式", + "description": "选择您想使用的认证方法", + "noMethodsAvailable": "没有可用的认证方法", + "allSetup": "您已经设置好了一个认证器!", + "linkWithIDP": "或将其与身份提供者关联" + }, + "device": { + "usercode": { + "title": "设备代码", + "description": "输入代码。", + "submit": "继续" + }, + "request": { + "title": "{appName} 想要连接:", + "description": "{appName} 将访问:", + "disclaimer": "点击“允许”即表示您允许此应用程序和 Zitadel 根据其各自的服务条款和隐私政策使用您的信息。您可以随时撤销此访问权限。", + "submit": "允许", + "deny": "拒绝" + }, + "scope": { + "openid": "验证您的身份。", + "email": "访问您的电子邮件地址。", + "profile": "访问您的完整个人资料信息。", + "offline_access": "允许离线访问您的账户。" + } + }, + "error": { + "noUserCode": "未提供用户代码!", + "noDeviceRequest": "没有找到设备请求。", + "unknownContext": "无法获取用户的上下文。请先输入用户名或提供 loginName 作为搜索参数。", + "sessionExpired": "当前会话已过期,请重新登录。", + "failedLoading": "加载数据失败,请再试一次。", + "tryagain": "重试" + } + } +} \ No newline at end of file diff --git a/internal/repository/instance/eventstore.go b/internal/repository/instance/eventstore.go index 68621597a8..b8089152bb 100644 --- a/internal/repository/instance/eventstore.go +++ b/internal/repository/instance/eventstore.go @@ -130,4 +130,5 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, NotificationPolicyChangedEventType, NotificationPolicyChangedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, TrustedDomainAddedEventType, eventstore.GenericEventMapper[TrustedDomainAddedEvent]) eventstore.RegisterFilterEventMapper(AggregateType, TrustedDomainRemovedEventType, eventstore.GenericEventMapper[TrustedDomainRemovedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, HostedLoginTranslationSet, HostedLoginTranslationSetEventMapper) } diff --git a/internal/repository/instance/hosted_login_translation.go b/internal/repository/instance/hosted_login_translation.go new file mode 100644 index 0000000000..05380521fc --- /dev/null +++ b/internal/repository/instance/hosted_login_translation.go @@ -0,0 +1,55 @@ +package instance + +import ( + "context" + + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/zerrors" +) + +const ( + HostedLoginTranslationSet = instanceEventTypePrefix + "hosted_login_translation.set" +) + +type HostedLoginTranslationSetEvent struct { + eventstore.BaseEvent `json:"-"` + + Translation map[string]any `json:"translation,omitempty"` + Language language.Tag `json:"language,omitempty"` + Level string `json:"level,omitempty"` +} + +func NewHostedLoginTranslationSetEvent(ctx context.Context, aggregate *eventstore.Aggregate, translation map[string]any, language language.Tag) *HostedLoginTranslationSetEvent { + return &HostedLoginTranslationSetEvent{ + BaseEvent: *eventstore.NewBaseEventForPush(ctx, aggregate, HostedLoginTranslationSet), + Translation: translation, + Language: language, + Level: string(aggregate.Type), + } +} + +func (e *HostedLoginTranslationSetEvent) Payload() any { + return e +} + +func (e *HostedLoginTranslationSetEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func (e *HostedLoginTranslationSetEvent) Fields() []*eventstore.FieldOperation { + return nil +} + +func HostedLoginTranslationSetEventMapper(event eventstore.Event) (eventstore.Event, error) { + translationSet := &HostedLoginTranslationSetEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := event.Unmarshal(translationSet) + if err != nil { + return nil, zerrors.ThrowInternal(err, "INST-lOxtJJ", "unable to unmarshal hosted login translation set event") + } + + return translationSet, nil +} diff --git a/internal/repository/org/eventstore.go b/internal/repository/org/eventstore.go index d1efa75dfc..289bbbc608 100644 --- a/internal/repository/org/eventstore.go +++ b/internal/repository/org/eventstore.go @@ -114,4 +114,5 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, NotificationPolicyAddedEventType, NotificationPolicyAddedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, NotificationPolicyChangedEventType, NotificationPolicyChangedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, NotificationPolicyRemovedEventType, NotificationPolicyRemovedEventMapper) + eventstore.RegisterFilterEventMapper(AggregateType, HostedLoginTranslationSet, HostedLoginTranslationSetEventMapper) } diff --git a/internal/repository/org/hosted_login_translation.go b/internal/repository/org/hosted_login_translation.go new file mode 100644 index 0000000000..e07bdc1e3b --- /dev/null +++ b/internal/repository/org/hosted_login_translation.go @@ -0,0 +1,55 @@ +package org + +import ( + "context" + + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/zerrors" +) + +const ( + HostedLoginTranslationSet = orgEventTypePrefix + "hosted_login_translation.set" +) + +type HostedLoginTranslationSetEvent struct { + eventstore.BaseEvent `json:"-"` + + Translation map[string]any `json:"translation,omitempty"` + Language language.Tag `json:"language,omitempty"` + Level string `json:"level,omitempty"` +} + +func NewHostedLoginTranslationSetEvent(ctx context.Context, aggregate *eventstore.Aggregate, translation map[string]any, language language.Tag) *HostedLoginTranslationSetEvent { + return &HostedLoginTranslationSetEvent{ + BaseEvent: *eventstore.NewBaseEventForPush(ctx, aggregate, HostedLoginTranslationSet), + Translation: translation, + Language: language, + Level: string(aggregate.Type), + } +} + +func (e *HostedLoginTranslationSetEvent) Payload() any { + return e +} + +func (e *HostedLoginTranslationSetEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func (e *HostedLoginTranslationSetEvent) Fields() []*eventstore.FieldOperation { + return nil +} + +func HostedLoginTranslationSetEventMapper(event eventstore.Event) (eventstore.Event, error) { + translationSet := &HostedLoginTranslationSetEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := event.Unmarshal(translationSet) + if err != nil { + return nil, zerrors.ThrowInternal(err, "ORG-BH82Eb", "unable to unmarshal hosted login translation set event") + } + + return translationSet, nil +} diff --git a/proto/zitadel/settings/v2/settings.proto b/proto/zitadel/settings/v2/settings.proto index b3ca5b5ca5..c797d27965 100644 --- a/proto/zitadel/settings/v2/settings.proto +++ b/proto/zitadel/settings/v2/settings.proto @@ -10,4 +10,4 @@ enum ResourceOwnerType { RESOURCE_OWNER_TYPE_UNSPECIFIED = 0; RESOURCE_OWNER_TYPE_INSTANCE = 1; RESOURCE_OWNER_TYPE_ORG = 2; -} +} \ No newline at end of file diff --git a/proto/zitadel/settings/v2/settings_service.proto b/proto/zitadel/settings/v2/settings_service.proto index 7f71e08da4..0a1f13e7e7 100644 --- a/proto/zitadel/settings/v2/settings_service.proto +++ b/proto/zitadel/settings/v2/settings_service.proto @@ -15,6 +15,8 @@ import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; +import "google/protobuf/struct.proto"; +import "zitadel/settings/v2/settings.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2;settings"; @@ -362,6 +364,69 @@ service SettingsService { description: "Set the security settings of the ZITADEL instance." }; } + + // Get Hosted Login Translation + // + // Returns the translations in the requested locale for the hosted login. + // The translations returned are based on the input level specified (system, instance or organization). + // + // If the requested level doesn't contain all translations, and ignore_inheritance is set to false, + // a merging process fallbacks onto the higher levels ensuring all keys in the file have a translation, + // which could be in the default language if the one of the locale is missing on all levels. + // + // The etag returned in the response represents the hash of the translations as they are stored on DB + // and its reliable only if ignore_inheritance = true. + // + // Required permissions: + // - `iam.policy.read` + rpc GetHostedLoginTranslation(GetHostedLoginTranslationRequest) returns (GetHostedLoginTranslationResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The localized translations."; + } + }; + }; + + option (google.api.http) = { + get: "/v2/settings/hosted_login_translation" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.policy.read" + } + }; + } + + // Set Hosted Login Translation + // + // Sets the input translations at the specified level (instance or organization) for the input language. + // + // Required permissions: + // - `iam.policy.write` + rpc SetHostedLoginTranslation(SetHostedLoginTranslationRequest) returns (SetHostedLoginTranslationResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The translations was successfully set."; + } + }; + }; + + option (google.api.http) = { + put: "/v2/settings/hosted_login_translation"; + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.policy.write" + } + }; + } } message GetLoginSettingsRequest { @@ -480,4 +545,76 @@ message SetSecuritySettingsRequest{ message SetSecuritySettingsResponse{ zitadel.object.v2.Details details = 1; +} + +message GetHostedLoginTranslationRequest { + oneof level { + bool system = 1 [(validate.rules).bool = {const: true}]; + bool instance = 2 [(validate.rules).bool = {const: true}]; + string organization_id = 3; + } + + string locale = 4 [ + (validate.rules).string = {min_len: 2}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 2; + example: "\"fr-FR\""; + } + ]; + + // if set to true, higher levels are ignored, if false higher levels are merged into the file + bool ignore_inheritance = 5; +} + +message GetHostedLoginTranslationResponse { + // hash of the payload + string etag = 1 [ + (validate.rules).string = {min_len: 32, max_len: 32}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 32; + max_length: 32; + example: "\"42a1ba123e6ea6f0c93e286ed97c7018\""; + } + ]; + + google.protobuf.Struct translations = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "{\"common\":{\"back\":\"Indietro\"},\"accounts\":{\"title\":\"Account\",\"description\":\"Seleziona l'account che desideri utilizzare.\",\"addAnother\":\"Aggiungi un altro account\",\"noResults\":\"Nessun account trovato\"}}"; + description: "Translations contains the translations in the request language."; + } + ]; +} + +message SetHostedLoginTranslationRequest { + oneof level { + bool instance = 1 [(validate.rules).bool = {const: true}]; + string organization_id = 2; + } + + string locale = 3 [ + (validate.rules).string = {min_len: 2}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 2; + example: "\"fr-FR\""; + } + ]; + + google.protobuf.Struct translations = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "{\"common\":{\"back\":\"Indietro\"},\"accounts\":{\"title\":\"Account\",\"description\":\"Seleziona l'account che desideri utilizzare.\",\"addAnother\":\"Aggiungi un altro account\",\"noResults\":\"Nessun account trovato\"}}"; + description: "Translations should contain the translations in the specified locale."; + } + ]; +} + +message SetHostedLoginTranslationResponse { + // hash of the saved translation. Valid only when ignore_inheritance = true + string etag = 1 [ + (validate.rules).string = {min_len: 32, max_len: 32}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 32; + max_length: 32; + example: "\"42a1ba123e6ea6f0c93e286ed97c7018\""; + } + ]; } \ No newline at end of file