From f9cad0f3e51618b61990279f0de4c9d731217b45 Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Thu, 10 Jul 2025 13:10:44 +0200 Subject: [PATCH 1/7] chore(typescript): improve close PR action (#10229) # Which Problems Are Solved The close PR action currently fails because of unescaped backticks. # How the Problems Are Solved Backticks are escaped. # Additional Changes - Adding a login remote immediately fetches for better UX. - Adding a subtree is not necessary, as it is already added in the repo. - Fix and clarify PR migration steps. - Add workflow dispatch event --- Makefile | 8 +------- login/.github/workflows/close_pr.yml | 18 +++++++++++------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index 3bad5aa1c6..b2d6e508dd 100644 --- a/Makefile +++ b/Makefile @@ -191,16 +191,10 @@ login_push: login_ensure_remote login_ensure_remote: @if ! git remote get-url $(LOGIN_REMOTE_NAME) > /dev/null 2>&1; then \ echo "Adding remote $(LOGIN_REMOTE_NAME)"; \ - git remote add $(LOGIN_REMOTE_NAME) $(LOGIN_REMOTE_URL); \ + git remote add --fetch $(LOGIN_REMOTE_NAME) $(LOGIN_REMOTE_URL); \ else \ echo "Remote $(LOGIN_REMOTE_NAME) already exists."; \ fi - @if [ ! -d login ]; then \ - echo "Adding subtree for 'login' from branch $(LOGIN_REMOTE_BRANCH)"; \ - git subtree add --prefix=login $(LOGIN_REMOTE_NAME) $(LOGIN_REMOTE_BRANCH); \ - else \ - echo "Subtree 'login' already exists."; \ - fi export LOGIN_DIR := ./login/ export LOGIN_BAKE_CLI_ADDITIONAL_ARGS := --set login-*.context=./login/ --file ./docker-bake.hcl diff --git a/login/.github/workflows/close_pr.yml b/login/.github/workflows/close_pr.yml index b44eb5bfe8..febf2e9143 100644 --- a/login/.github/workflows/close_pr.yml +++ b/login/.github/workflows/close_pr.yml @@ -1,6 +1,7 @@ name: Auto-close PRs and guide to correct repo on: + workflow_dispatch: pull_request_target: types: [opened] @@ -14,14 +15,17 @@ jobs: with: script: | const message = ` - 👋 **Thanks for your contribution!** + 👋 **Thanks for your contribution @${{ github.event.pull_request.user.login }}!** - This repository \`${{ github.repository }}\` is a read-only mirror of our internal development in [\`zitadel/zitadel\`](https://github.com/zitadel/zitadel). - Therefore, we close this pull request automatically, but submitting your changes to the main repository is easy: - 1. Fork and clone zitadel/zitadel - 2. Create a new branch for your changes - 3. Pull your changes into the new fork by running `make login_pull LOGIN_REMOTE_URL=/typescript LOGIN_REMOTE_BRANCH=`. - 4. Push your changes and open a pull request to zitadel/zitadel + This repository \`${{ github.repository }}\` is a read-only mirror of the git subtree at [\`zitadel/zitadel/login`](https://github.com/zitadel/zitadel). + Therefore, we close this pull request automatically. + + Your changes are not lost. Submitting them to the main repository is easy: + 1. [Fork zitadel/zitadel](https://github.com/zitadel/zitadel/fork) + 2. Clone your Zitadel fork \`git clone https://github.com//zitadel.git\` + 3. Change directory to your Zitadel forks root. + 4. Pull your changes into the Zitadel fork by running \`make login_pull LOGIN_REMOTE_URL=https://github.com//typescript.git LOGIN_REMOTE_BRANCH=\`. + 5. Push your changes and [open a pull request to zitadel/zitadel](https://github.com/zitadel/zitadel/compare) `.trim(); await github.rest.issues.createComment({ ...context.repo, From 0598abe7e619276988948ca9387cdc12d1188882 Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Thu, 10 Jul 2025 15:39:45 +0200 Subject: [PATCH 2/7] chore(login): fix close pr action (#10234) # Which Problems Are Solved The close PR action fails https://github.com/zitadel/typescript/actions/runs/16196332400/job/45723668837?pr=511 # How the Problems Are Solved A backtick is escaped. # Additional Context - Completes #10229 --- login/.github/workflows/close_pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/login/.github/workflows/close_pr.yml b/login/.github/workflows/close_pr.yml index febf2e9143..6029e36d4c 100644 --- a/login/.github/workflows/close_pr.yml +++ b/login/.github/workflows/close_pr.yml @@ -17,7 +17,7 @@ jobs: const message = ` 👋 **Thanks for your contribution @${{ github.event.pull_request.user.login }}!** - This repository \`${{ github.repository }}\` is a read-only mirror of the git subtree at [\`zitadel/zitadel/login`](https://github.com/zitadel/zitadel). + This repository \`${{ github.repository }}\` is a read-only mirror of the git subtree at [\`zitadel/zitadel/login\`](https://github.com/zitadel/zitadel). Therefore, we close this pull request automatically. Your changes are not lost. Submitting them to the main repository is easy: From fefeaea56ad21dea5b808f8c7c89bfab6d9840e1 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Thu, 10 Jul 2025 11:17:49 -0400 Subject: [PATCH 3/7] perf: improve org and org domain creation (#10232) # Which Problems Are Solved When an organization domain is verified, e.g. also when creating a new organization (incl. generated domain), existing usernames are checked if the domain has been claimed. The query was not optimized for instances with many users and organizations. # How the Problems Are Solved - Replace the query, which was searching over the users projection with (computed loginnames) with a dedicated query checking the loginnames projection directly. - All occurrences have been updated to use the new query. # Additional Changes None # Additional Context - reported through support - requires backport to v3.x --- internal/api/grpc/admin/org.go | 15 +---- internal/api/grpc/management/org.go | 23 +------ .../org/v2beta/integration_test/org_test.go | 64 +++++++++++++++++++ internal/api/grpc/org/v2beta/org.go | 23 +------ internal/api/ui/login/login.go | 14 +--- internal/query/user.go | 29 +++++++++ internal/query/user_claimed_user_ids.sql | 13 ++++ 7 files changed, 110 insertions(+), 71 deletions(-) create mode 100644 internal/query/user_claimed_user_ids.sql diff --git a/internal/api/grpc/admin/org.go b/internal/api/grpc/admin/org.go index ef97e47bb0..a2b55cf21c 100644 --- a/internal/api/grpc/admin/org.go +++ b/internal/api/grpc/admin/org.go @@ -9,7 +9,6 @@ import ( http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/query" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" ) @@ -104,17 +103,5 @@ func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (* } func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain string) ([]string, error) { - loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+orgDomain, query.TextEndsWithIgnoreCase) - if err != nil { - return nil, err - } - users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}}, 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 + return s.query.SearchClaimedUserIDsOfOrgDomain(ctx, orgDomain, "") } diff --git a/internal/api/grpc/management/org.go b/internal/api/grpc/management/org.go index ee4f5eb633..a006db063d 100644 --- a/internal/api/grpc/management/org.go +++ b/internal/api/grpc/management/org.go @@ -316,28 +316,7 @@ func (s *Server) RemoveOrgMember(ctx context.Context, req *mgmt_pb.RemoveOrgMemb } 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 - } - 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 + return s.query.SearchClaimedUserIDsOfOrgDomain(ctx, orgDomain, orgID) } func (s *Server) ListOrgMetadata(ctx context.Context, req *mgmt_pb.ListOrgMetadataRequest) (*mgmt_pb.ListOrgMetadataResponse, error) { 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 0d3b920afe..d36c570b92 100644 --- a/internal/api/grpc/org/v2beta/integration_test/org_test.go +++ b/internal/api/grpc/org/v2beta/integration_test/org_test.go @@ -1044,6 +1044,70 @@ func TestServer_AddOrganizationDomain(t *testing.T) { } } +func TestServer_AddOrganizationDomain_ClaimDomain(t *testing.T) { + domain := gofakeit.DomainName() + + // create an organization, ensure it has globally unique usernames + // and create a user with a loginname that matches the domain later on + organization, err := Client.CreateOrganization(CTX, &v2beta_org.CreateOrganizationRequest{ + Name: gofakeit.AppName(), + }) + require.NoError(t, err) + _, err = Instance.Client.Admin.AddCustomDomainPolicy(CTX, &admin.AddCustomDomainPolicyRequest{ + OrgId: organization.GetId(), + UserLoginMustBeDomain: false, + }) + require.NoError(t, err) + username := gofakeit.Username() + "@" + domain + ownUser := Instance.CreateHumanUserVerified(CTX, organization.GetId(), username, "") + + // create another organization, ensure it has globally unique usernames + // and create a user with a loginname that matches the domain later on + otherOrg, err := Client.CreateOrganization(CTX, &v2beta_org.CreateOrganizationRequest{ + Name: gofakeit.AppName(), + }) + require.NoError(t, err) + _, err = Instance.Client.Admin.AddCustomDomainPolicy(CTX, &admin.AddCustomDomainPolicyRequest{ + OrgId: otherOrg.GetId(), + UserLoginMustBeDomain: false, + }) + require.NoError(t, err) + + otherUsername := gofakeit.Username() + "@" + domain + otherUser := Instance.CreateHumanUserVerified(CTX, otherOrg.GetId(), otherUsername, "") + + // if we add the domain now to the first organization, it should be claimed on the second organization, resp. its user(s) + _, err = Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: organization.GetId(), + Domain: domain, + }) + require.NoError(t, err) + + // check both users: the first one must be untouched, the second one must be updated + users, err := Instance.Client.UserV2.ListUsers(CTX, &user.ListUsersRequest{ + Queries: []*user.SearchQuery{ + { + Query: &user.SearchQuery_InUserIdsQuery{ + InUserIdsQuery: &user.InUserIDQuery{UserIds: []string{ownUser.GetUserId(), otherUser.GetUserId()}}, + }, + }, + }, + }) + require.NoError(t, err) + require.Len(t, users.GetResult(), 2) + + for _, u := range users.GetResult() { + if u.GetUserId() == ownUser.GetUserId() { + assert.Equal(t, username, u.GetPreferredLoginName()) + continue + } + if u.GetUserId() == otherUser.GetUserId() { + assert.NotEqual(t, otherUsername, u.GetPreferredLoginName()) + assert.Contains(t, u.GetPreferredLoginName(), "@temporary.") + } + } +} + func TestServer_ListOrganizationDomains(t *testing.T) { domain := gofakeit.URL() tests := []struct { diff --git a/internal/api/grpc/org/v2beta/org.go b/internal/api/grpc/org/v2beta/org.go index 35e1d72d3c..edc667409a 100644 --- a/internal/api/grpc/org/v2beta/org.go +++ b/internal/api/grpc/org/v2beta/org.go @@ -250,26 +250,5 @@ func createOrganizationRequestAdminToCommand(admin *v2beta_org.CreateOrganizatio } 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 - } - 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 + return s.query.SearchClaimedUserIDsOfOrgDomain(ctx, orgDomain, orgID) } diff --git a/internal/api/ui/login/login.go b/internal/api/ui/login/login.go index f1ce9bfa2a..5ff27c14fc 100644 --- a/internal/api/ui/login/login.go +++ b/internal/api/ui/login/login.go @@ -178,19 +178,7 @@ func (l *Login) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgName string if err != nil { return nil, err } - loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+orgDomain, query.TextEndsWithIgnoreCase) - if err != nil { - return nil, err - } - users, err := l.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}}, 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 + return l.query.SearchClaimedUserIDsOfOrgDomain(ctx, orgDomain, "") } func setContext(ctx context.Context, resourceOwner string) context.Context { diff --git a/internal/query/user.go b/internal/query/user.go index ac3eb79fc9..66c7abb228 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -697,6 +697,35 @@ func (q *Queries) IsUserUnique(ctx context.Context, username, email, resourceOwn return isUnique, err } +//go:embed user_claimed_user_ids.sql +var userClaimedUserIDOfOrgDomain string + +func (q *Queries) SearchClaimedUserIDsOfOrgDomain(ctx context.Context, domain, orgID string) (userIDs []string, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + err = q.client.QueryContext(ctx, + func(rows *sql.Rows) error { + userIDs = make([]string, 0) + for rows.Next() { + var userID string + err := rows.Scan(&userID) + if err != nil { + return err + } + userIDs = append(userIDs, userID) + } + return nil + }, + userClaimedUserIDOfOrgDomain, + authz.GetInstance(ctx).InstanceID(), + "%@"+domain, + orgID, + ) + + return userIDs, err +} + func (q *UserSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { query = q.SearchRequest.toQuery(query) for _, q := range q.Queries { diff --git a/internal/query/user_claimed_user_ids.sql b/internal/query/user_claimed_user_ids.sql new file mode 100644 index 0000000000..5d4639be46 --- /dev/null +++ b/internal/query/user_claimed_user_ids.sql @@ -0,0 +1,13 @@ +SELECT u.id +FROM projections.login_names3_users u + LEFT JOIN projections.login_names3_policies p_custom + ON u.instance_id = p_custom.instance_id + AND p_custom.instance_id = $1 + AND p_custom.resource_owner = u.resource_owner + JOIN projections.login_names3_policies p_default + ON u.instance_id = p_default.instance_id + AND p_default.instance_id = $1 AND p_default.is_default IS TRUE +WHERE u.instance_id = $1 + AND COALESCE(p_custom.must_be_domain, p_default.must_be_domain) = false + AND u.user_name_lower like $2 + AND u.resource_owner <> $3; \ No newline at end of file From 8f61b24532aa9238367266761e2106eb5f41c711 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Fri, 11 Jul 2025 05:29:27 -0400 Subject: [PATCH 4/7] fix(login v1): correctly auto-link users on organizations with suffixed usernames (#10205) --- internal/api/ui/login/external_provider_handler.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index 967d79c1a9..6f851df562 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -523,7 +523,7 @@ func (l *Login) handleExternalUserAuthenticated( // The decision, which information will be checked is based on the IdP template option. // The function returns a boolean whether a user was found or not. // If single a user was found, it will be automatically linked. -func (l *Login) checkAutoLinking(r *http.Request, authReq *domain.AuthRequest, provider *query.IDPTemplate, externalUser *domain.ExternalUser) (bool, error) { +func (l *Login) checkAutoLinking(r *http.Request, authReq *domain.AuthRequest, provider *query.IDPTemplate, externalUser *domain.ExternalUser, human *domain.Human) (bool, error) { queries := make([]query.SearchQuery, 0, 2) switch provider.AutoLinking { case domain.AutoLinkingOptionUnspecified: @@ -532,7 +532,7 @@ func (l *Login) checkAutoLinking(r *http.Request, authReq *domain.AuthRequest, p case domain.AutoLinkingOptionUsername: // if we're checking for usernames there are to options: // - // If no specific org has been requested (by id or domain scope), we'll check the provided username against + // If no specific org has been requested (by id or domain scope), we'll check the provided username (loginname) against // all existing loginnames and directly use that result to either prompt or continue with other idp options. if authReq.RequestedOrgID == "" { user, err := l.query.GetNotifyUserByLoginName(r.Context(), false, externalUser.PreferredUsername) @@ -544,8 +544,9 @@ func (l *Login) checkAutoLinking(r *http.Request, authReq *domain.AuthRequest, p } return true, nil } - // If a specific org has been requested, we'll check the provided username against usernames (of that org). - usernameQuery, err := query.NewUserUsernameSearchQuery(externalUser.PreferredUsername, query.TextEqualsIgnoreCase) + // If a specific org has been requested, we'll check the username (org policy (suffixed or not) is already applied) + // against usernames (of that org). + usernameQuery, err := query.NewUserUsernameSearchQuery(human.Username, query.TextEqualsIgnoreCase) if err != nil { return false, nil } @@ -605,7 +606,7 @@ func (l *Login) createOrLinkUser(w http.ResponseWriter, r *http.Request, authReq human, idpLink, _ := mapExternalUserToLoginUser(externalUser, orgIAMPolicy.UserLoginMustBeDomain) // let's check if auto-linking is enabled and if the user would be found by the corresponding option if provider.AutoLinking != domain.AutoLinkingOptionUnspecified { - userLinked, err = l.checkAutoLinking(r, authReq, provider, externalUser) + userLinked, err = l.checkAutoLinking(r, authReq, provider, externalUser, human) if err != nil { l.renderError(w, r, authReq, err) return false From 1b01fc6c4083e94a6b17877d23cdb077ac128ff3 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Fri, 11 Jul 2025 05:55:01 -0400 Subject: [PATCH 5/7] fix(api): CORS for connectRPC and grpc-web (#10227) # Which Problems Are Solved The CORS handler for the new connectRPC handlers was missing, leading to unhandled preflight requests and a unusable api for browser based calls, e.g. cross domain gRPC-web requests. # How the Problems Are Solved - Added the http CORS middleware to the connectRPC handlers. - Added `Grpc-Timeout`, `Connect-Protocol-Version`,`Connect-Timeout-Ms` to the default allowed headers (this improves also the old grpc-web handling) - Added `Grpc-Status`, `Grpc-Message`, `Grpc-Status-Details-Bin` to the default exposed headers (this improves also the old grpc-web handling) # Additional Changes None # Additional Context noticed internally while testing other issues --- internal/api/api.go | 2 +- internal/api/http/header.go | 54 ++++++++++--------- .../api/http/middleware/cors_interceptor.go | 6 +++ 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index 349e9186bc..18c554ca37 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -201,7 +201,7 @@ func (a *API) registerConnectServer(service server.ConnectServer) { methodNames[i] = string(methods.Get(i).Name()) } a.connectServices[prefix] = methodNames - a.RegisterHandlerPrefixes(handler, prefix) + a.RegisterHandlerPrefixes(http_mw.CORSInterceptor(handler), prefix) } // HandleFunc allows registering a [http.HandlerFunc] on an exact diff --git a/internal/api/http/header.go b/internal/api/http/header.go index a6c2818728..fce5330df8 100644 --- a/internal/api/http/header.go +++ b/internal/api/http/header.go @@ -10,30 +10,36 @@ import ( ) const ( - Authorization = "authorization" - Accept = "accept" - AcceptLanguage = "accept-language" - CacheControl = "cache-control" - ContentType = "content-type" - ContentLength = "content-length" - ContentLocation = "content-location" - Expires = "expires" - Location = "location" - Origin = "origin" - Pragma = "pragma" - UserAgentHeader = "user-agent" - ForwardedFor = "x-forwarded-for" - ForwardedHost = "x-forwarded-host" - ForwardedProto = "x-forwarded-proto" - Forwarded = "forwarded" - ZitadelForwarded = "x-zitadel-forwarded" - XUserAgent = "x-user-agent" - XGrpcWeb = "x-grpc-web" - XRequestedWith = "x-requested-with" - XRobotsTag = "x-robots-tag" - IfNoneMatch = "If-None-Match" - LastModified = "Last-Modified" - Etag = "Etag" + Authorization = "authorization" + Accept = "accept" + AcceptLanguage = "accept-language" + CacheControl = "cache-control" + ContentType = "content-type" + ContentLength = "content-length" + ContentLocation = "content-location" + Expires = "expires" + Location = "location" + Origin = "origin" + Pragma = "pragma" + UserAgentHeader = "user-agent" + ForwardedFor = "x-forwarded-for" + ForwardedHost = "x-forwarded-host" + ForwardedProto = "x-forwarded-proto" + Forwarded = "forwarded" + ZitadelForwarded = "x-zitadel-forwarded" + XUserAgent = "x-user-agent" + XGrpcWeb = "x-grpc-web" + XRequestedWith = "x-requested-with" + XRobotsTag = "x-robots-tag" + IfNoneMatch = "if-none-match" + LastModified = "last-modified" + Etag = "etag" + GRPCTimeout = "grpc-timeout" + ConnectProtocolVersion = "connect-protocol-version" + ConnectTimeoutMS = "connect-timeout-ms" + GrpcStatus = "grpc-status" + GrpcMessage = "grpc-message" + GrpcStatusDetailsBin = "grpc-status-details-bin" ContentSecurityPolicy = "content-security-policy" XXSSProtection = "x-xss-protection" diff --git a/internal/api/http/middleware/cors_interceptor.go b/internal/api/http/middleware/cors_interceptor.go index 02f1924956..fb5503909d 100644 --- a/internal/api/http/middleware/cors_interceptor.go +++ b/internal/api/http/middleware/cors_interceptor.go @@ -21,6 +21,9 @@ var ( http_utils.XUserAgent, http_utils.XGrpcWeb, http_utils.XRequestedWith, + http_utils.ConnectProtocolVersion, + http_utils.ConnectTimeoutMS, + http_utils.GRPCTimeout, }, AllowedMethods: []string{ http.MethodOptions, @@ -34,6 +37,9 @@ var ( ExposedHeaders: []string{ http_utils.Location, http_utils.ContentLength, + http_utils.GrpcStatus, + http_utils.GrpcMessage, + http_utils.GrpcStatusDetailsBin, }, AllowOriginFunc: func(_ string) bool { return true From 23d6d24bc8f10515d6852bf34159d537002c6092 Mon Sep 17 00:00:00 2001 From: Iraq <66622793+kkrime@users.noreply.github.com> Date: Mon, 14 Jul 2025 09:19:50 +0200 Subject: [PATCH 6/7] fix(login): changed permission check for sending invite code on log in (#10197) # Which Problems Are Solved Fixes issue when users would get an error message when attempting to resend invitation code when logging in # How the Problems Are Solved Changing the permission check for looking for `org.write` to `ommand.checkPermissionUpdateUser()` # Additional Context - Closes https://github.com/zitadel/zitadel/issues/10100 - backport to 3.x --- internal/command/user_v2_invite.go | 7 +- internal/command/user_v2_invite_test.go | 130 ++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 2 deletions(-) diff --git a/internal/command/user_v2_invite.go b/internal/command/user_v2_invite.go index 430ba8c7d1..10948164e9 100644 --- a/internal/command/user_v2_invite.go +++ b/internal/command/user_v2_invite.go @@ -6,6 +6,7 @@ import ( "github.com/zitadel/logging" + "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" @@ -50,8 +51,10 @@ func (c *Commands) sendInviteCode(ctx context.Context, invite *CreateUserInvite, if err != nil { return nil, nil, err } - if err := c.checkPermission(ctx, domain.PermissionUserWrite, wm.ResourceOwner, wm.AggregateID); err != nil { - return nil, nil, err + if wm.AggregateID != authz.GetCtxData(ctx).UserID { + if err := c.checkPermission(ctx, domain.PermissionUserWrite, wm.ResourceOwner, wm.AggregateID); err != nil { + return nil, nil, err + } } if !wm.UserState.Exists() { return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wgvn4", "Errors.User.NotFound") diff --git a/internal/command/user_v2_invite_test.go b/internal/command/user_v2_invite_test.go index 75bd3157db..53ad1bd944 100644 --- a/internal/command/user_v2_invite_test.go +++ b/internal/command/user_v2_invite_test.go @@ -11,6 +11,7 @@ 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" @@ -205,6 +206,64 @@ func TestCommands_CreateInviteCode(t *testing.T) { returnCode: gu.Ptr("code"), }, }, + { + "return ok, with same user requests code", + 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, + ), + ), + ), + expectPush( + eventFromEventPusher( + user.NewHumanInviteCodeAddedEvent(authz.SetCtxData(context.Background(), authz.CtxData{UserID: "userID"}), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "", + true, + "", + "", + ), + ), + ), + ), + // we do not run checkPermission() because the same user is requesting the code as the user to which the code is intended for + checkPermission: nil, + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code", time.Hour), + defaultSecretGenerators: &SecretGenerators{}, + }, + args{ + ctx: authz.SetCtxData(context.Background(), authz.CtxData{UserID: "userID"}), + invite: &CreateUserInvite{ + UserID: "userID", + ReturnCode: true, + }, + }, + want{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "userID", + }, + returnCode: gu.Ptr("code"), + }, + }, { "with template and application name ok", fields{ @@ -510,6 +569,77 @@ func TestCommands_ResendInviteCode(t *testing.T) { }, }, }, + { + "return ok, with same user requests code", + 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.SetCtxData(context.Background(), authz.CtxData{UserID: "userID"}), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "", + false, + "", + "authRequestID", + ), + ), + ), + ), + // we do not run checkPermission() because the same user is requesting the code as the user to which the code is intended for + checkPermission: nil, + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code", time.Hour), + defaultSecretGenerators: &SecretGenerators{}, + }, + args{ + // ctx: context.Background(), + ctx: authz.SetCtxData(context.Background(), authz.CtxData{UserID: "userID"}), + userID: "userID", + }, + want{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "userID", + }, + }, + }, { "resend with new auth requestID ok", fields{ From 79fcc2f2b6aa81b9614facd1f7e921919634a20e Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Mon, 14 Jul 2025 04:01:36 -0400 Subject: [PATCH 7/7] chore(tests): name integration test packages correctly to let them run (#10242) # Which Problems Are Solved After changing some internal logic, which should have failed the integration test, but didn't, I noticed that some integration tests were never executed. The make command lists all `integration_test` packages, but some are named `integration` # How the Problems Are Solved Correct wrong integration test package names. # Additional Changes None # Additional Context - noticed internally - backport to 3.x and 2.x --- .../{integration => integration_test}/administrator_test.go | 0 .../v2beta/{integration => integration_test}/query_test.go | 0 .../v2beta/{integration => integration_test}/server_test.go | 0 .../{integration => integration_test}/project_grant_test.go | 0 .../v2beta/{integration => integration_test}/project_role_test.go | 0 .../v2beta/{integration => integration_test}/project_test.go | 0 .../v2beta/{integration => integration_test}/query_test.go | 0 .../v2beta/{integration => integration_test}/server_test.go | 0 .../grpc/saml/v2/{integration => integration_test}/saml_test.go | 0 .../grpc/saml/v2/{integration => integration_test}/server_test.go | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename internal/api/grpc/internal_permission/v2beta/{integration => integration_test}/administrator_test.go (100%) rename internal/api/grpc/internal_permission/v2beta/{integration => integration_test}/query_test.go (100%) rename internal/api/grpc/internal_permission/v2beta/{integration => integration_test}/server_test.go (100%) rename internal/api/grpc/project/v2beta/{integration => integration_test}/project_grant_test.go (100%) rename internal/api/grpc/project/v2beta/{integration => integration_test}/project_role_test.go (100%) rename internal/api/grpc/project/v2beta/{integration => integration_test}/project_test.go (100%) rename internal/api/grpc/project/v2beta/{integration => integration_test}/query_test.go (100%) rename internal/api/grpc/project/v2beta/{integration => integration_test}/server_test.go (100%) rename internal/api/grpc/saml/v2/{integration => integration_test}/saml_test.go (100%) rename internal/api/grpc/saml/v2/{integration => integration_test}/server_test.go (100%) diff --git a/internal/api/grpc/internal_permission/v2beta/integration/administrator_test.go b/internal/api/grpc/internal_permission/v2beta/integration_test/administrator_test.go similarity index 100% rename from internal/api/grpc/internal_permission/v2beta/integration/administrator_test.go rename to internal/api/grpc/internal_permission/v2beta/integration_test/administrator_test.go diff --git a/internal/api/grpc/internal_permission/v2beta/integration/query_test.go b/internal/api/grpc/internal_permission/v2beta/integration_test/query_test.go similarity index 100% rename from internal/api/grpc/internal_permission/v2beta/integration/query_test.go rename to internal/api/grpc/internal_permission/v2beta/integration_test/query_test.go diff --git a/internal/api/grpc/internal_permission/v2beta/integration/server_test.go b/internal/api/grpc/internal_permission/v2beta/integration_test/server_test.go similarity index 100% rename from internal/api/grpc/internal_permission/v2beta/integration/server_test.go rename to internal/api/grpc/internal_permission/v2beta/integration_test/server_test.go diff --git a/internal/api/grpc/project/v2beta/integration/project_grant_test.go b/internal/api/grpc/project/v2beta/integration_test/project_grant_test.go similarity index 100% rename from internal/api/grpc/project/v2beta/integration/project_grant_test.go rename to internal/api/grpc/project/v2beta/integration_test/project_grant_test.go diff --git a/internal/api/grpc/project/v2beta/integration/project_role_test.go b/internal/api/grpc/project/v2beta/integration_test/project_role_test.go similarity index 100% rename from internal/api/grpc/project/v2beta/integration/project_role_test.go rename to internal/api/grpc/project/v2beta/integration_test/project_role_test.go diff --git a/internal/api/grpc/project/v2beta/integration/project_test.go b/internal/api/grpc/project/v2beta/integration_test/project_test.go similarity index 100% rename from internal/api/grpc/project/v2beta/integration/project_test.go rename to internal/api/grpc/project/v2beta/integration_test/project_test.go diff --git a/internal/api/grpc/project/v2beta/integration/query_test.go b/internal/api/grpc/project/v2beta/integration_test/query_test.go similarity index 100% rename from internal/api/grpc/project/v2beta/integration/query_test.go rename to internal/api/grpc/project/v2beta/integration_test/query_test.go diff --git a/internal/api/grpc/project/v2beta/integration/server_test.go b/internal/api/grpc/project/v2beta/integration_test/server_test.go similarity index 100% rename from internal/api/grpc/project/v2beta/integration/server_test.go rename to internal/api/grpc/project/v2beta/integration_test/server_test.go diff --git a/internal/api/grpc/saml/v2/integration/saml_test.go b/internal/api/grpc/saml/v2/integration_test/saml_test.go similarity index 100% rename from internal/api/grpc/saml/v2/integration/saml_test.go rename to internal/api/grpc/saml/v2/integration_test/saml_test.go diff --git a/internal/api/grpc/saml/v2/integration/server_test.go b/internal/api/grpc/saml/v2/integration_test/server_test.go similarity index 100% rename from internal/api/grpc/saml/v2/integration/server_test.go rename to internal/api/grpc/saml/v2/integration_test/server_test.go