From 711d688294316e36b1a8a10ea9619b56561d82c3 Mon Sep 17 00:00:00 2001 From: Livio Spring <9405495+livio-a@users.noreply.github.com> Date: Fri, 28 Aug 2026 12:37:52 +0200 Subject: [PATCH] fix(login): resolve the registration org consistently with the login policy (#12621) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Which Problems Are Solved When resolving the organization for a self-registration, the resource owner was determined only from an explicitly requested org, falling back to the default org. The login policy applied to the same request, however, is resolved from the requested org, the user org, and only then the default org. As a result the two could diverge — e.g. after the login context had already resolved a user's organization — and a user could be registered in a different organization than the one whose settings were evaluated for the request. # How the Problems Are Solved `determineResourceOwner` now uses the same resolution order as the policy lookup (requested org, then user org, then default org), so the organization a user is registered in matches the one the request was evaluated against. # Additional Changes Added a unit test covering the resolution order. # Additional Context Aligns login v1 registration org resolution with the behavior in login v2. Co-authored-by: Claude Opus 4.8 --- .github/workflows/lint_test_build.yml | 11 ++++ internal/api/ui/login/register_handler.go | 8 ++- .../api/ui/login/register_handler_test.go | 52 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 internal/api/ui/login/register_handler_test.go diff --git a/.github/workflows/lint_test_build.yml b/.github/workflows/lint_test_build.yml index d7208c07a..b6c50535c 100644 --- a/.github/workflows/lint_test_build.yml +++ b/.github/workflows/lint_test_build.yml @@ -55,6 +55,17 @@ jobs: run: pnpm install --frozen-lockfile - name: Set SHAs for nx affected commands uses: nrwl/nx-set-shas@v4 + # The Depot runner image points /etc/resolv.conf directly at public DNS (8.8.8.8), + # bypassing the systemd-resolved stub, so *.integration.localhost (synthesized by + # systemd-resolved) no longer resolves and all API integration tests hang. + # Route DNS back through the stub until the image is fixed or the tests stop + # depending on wildcard localhost DNS. + - name: Restore systemd-resolved stub for *.localhost resolution + run: | + set -ex + test -e /run/systemd/resolve/stub-resolv.conf + sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf + getent hosts foo.integration.localhost - name: Lint, Test and Build env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN || secrets.NX_CLOUD_ACCESS_TOKEN_READONLY }} diff --git a/internal/api/ui/login/register_handler.go b/internal/api/ui/login/register_handler.go index 1c6af8551..1a0dce4fd 100644 --- a/internal/api/ui/login/register_handler.go +++ b/internal/api/ui/login/register_handler.go @@ -42,9 +42,15 @@ type registerData struct { } func determineResourceOwner(ctx context.Context, authRequest *domain.AuthRequest) string { - if authRequest != nil && authRequest.RequestedOrgID != "" { + if authRequest == nil { + return authz.GetInstance(ctx).DefaultOrganisationID() + } + if authRequest.RequestedOrgID != "" { return authRequest.RequestedOrgID } + if authRequest.UserOrgID != "" { + return authRequest.UserOrgID + } return authz.GetInstance(ctx).DefaultOrganisationID() } diff --git a/internal/api/ui/login/register_handler_test.go b/internal/api/ui/login/register_handler_test.go new file mode 100644 index 000000000..eb337a6fa --- /dev/null +++ b/internal/api/ui/login/register_handler_test.go @@ -0,0 +1,52 @@ +package login + +import ( + "context" + "testing" + + "github.com/zitadel/zitadel/internal/domain" +) + +func Test_determineResourceOwner(t *testing.T) { + // Note: the nil-authRequest and "both empty" cases fall back to + // authz.GetInstance(ctx).DefaultOrganisationID() and are covered by the + // integration/handler tests where an instance is present in the context. + // Here we lock the resolution order for the cases that do not touch the + // instance: RequestedOrgID takes precedence over UserOrgID, and UserOrgID + // is used as the fallback before the default org. + tests := []struct { + name string + authRequest *domain.AuthRequest + want string + }{ + { + name: "requested org wins over user org", + authRequest: &domain.AuthRequest{RequestedOrgID: "requested", UserOrgID: "user"}, + want: "requested", + }, + { + name: "requested org only", + authRequest: &domain.AuthRequest{RequestedOrgID: "requested"}, + want: "requested", + }, + { + name: "user org used when no requested org (matches fillPolicies resolution)", + authRequest: &domain.AuthRequest{UserOrgID: "user"}, + want: "user", + }, + { + // domain discovery sets BOTH RequestedOrgID and UserOrgID to the + // discovered org, so it still resolves to the discovered org. + name: "domain discovery result", + authRequest: &domain.AuthRequest{RequestedOrgID: "discovered", UserOrgID: "discovered"}, + want: "discovered", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := determineResourceOwner(context.Background(), tt.authRequest); got != tt.want { + t.Errorf("determineResourceOwner() = %q, want %q", got, tt.want) + } + }) + } +}