mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-14 18:05:12 +00:00
Merge commit from fork
* fix: block Login V2 auth for users in deactivated organizations Enforce organization state on session creation, OIDC token issuance/refresh, and SAML session creation so deactivated org users cannot authenticate. Co-authored-by: Cursor <[email protected]> * fix: reject claim serving for users in deactivated organizations Require an active resource-owner org in OIDC userinfo and SAML attribute paths, and use distinct error IDs for inactive user vs inactive org at token issuance. Co-authored-by: Cursor <[email protected]> * fix: drop sessions and tokens when an organization is deactivated Mirror OrgRemoved cleanup for OrgDeactivated in the V2 session projection and V1 auth user_session, token, and refresh_token handlers. Also delete V2 sessions on OrgRemoved, which does not emit per-user removal events. Co-authored-by: Cursor <[email protected]> * fix: reject API tokens from deactivated organizations Check the caller's resource-owner org state in authz middleware via a cached OrgByID lookup, so already-issued tokens lose ZITADEL API access when their organization is deactivated. Return unauthenticated (401). Target org remains unrestricted so instance admins can still manage deactivated orgs. Co-authored-by: Cursor <[email protected]> * fix: invalidate OIDC refresh tokens after org deactivation Reject refresh exchange if the user's organization was deactivated after the refresh token was issued, so grants stay dead after reactivate. Co-authored-by: Cursor <[email protected]> * fix: invalidate V2 access tokens after org deactivation Treat OrgDeactivated after the token position as session termination in ActiveAccessTokenByToken, so issued ATs stay dead after reactivation. Co-authored-by: Cursor <[email protected]> --------- Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
package authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type tokenVerifierMock struct {
|
||||
AccessTokenVerifier
|
||||
SystemTokenVerifier
|
||||
existsOrg func(ctx context.Context, id, domain string) (string, error)
|
||||
checkOrgActive func(ctx context.Context, orgID string) error
|
||||
projectIDOrigin func(ctx context.Context, clientID string) (string, []string, error)
|
||||
}
|
||||
|
||||
func (m *tokenVerifierMock) RegisterServer(string, string, MethodMapping) {}
|
||||
|
||||
func (m *tokenVerifierMock) CheckAuthMethod(string) (Option, bool) {
|
||||
return Option{}, false
|
||||
}
|
||||
|
||||
func (m *tokenVerifierMock) SearchMyMemberships(context.Context, string, bool) ([]*Membership, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *tokenVerifierMock) ExistsOrg(ctx context.Context, id, domain string) (string, error) {
|
||||
if m.existsOrg != nil {
|
||||
return m.existsOrg(ctx, id, domain)
|
||||
}
|
||||
if id != "" {
|
||||
return id, nil
|
||||
}
|
||||
return domain, nil
|
||||
}
|
||||
|
||||
func (m *tokenVerifierMock) CheckOrgActive(ctx context.Context, orgID string) error {
|
||||
if m.checkOrgActive != nil {
|
||||
return m.checkOrgActive(ctx, orgID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *tokenVerifierMock) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) {
|
||||
if m.projectIDOrigin != nil {
|
||||
return m.projectIDOrigin(ctx, clientID)
|
||||
}
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
func TestVerifyTokenAndCreateCtxData_orgActive(t *testing.T) {
|
||||
activeCaller := AccessTokenVerifierFunc(func(context.Context, string) (string, string, string, string, string, error) {
|
||||
return "user1", "", "", "", "caller-org", nil
|
||||
})
|
||||
systemTokenNOK := SystemTokenVerifierFunc(func(context.Context, string, string) (Memberships, string, error) {
|
||||
return nil, "", zerrors.ThrowUnauthenticated(nil, "TEST-sys", "unauthenticated")
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
orgID string
|
||||
orgDomain string
|
||||
accessToken AccessTokenVerifier
|
||||
checkOrgActive func(context.Context, string) error
|
||||
wantErr func(error) bool
|
||||
wantResource string
|
||||
wantOrgID string
|
||||
}{
|
||||
{
|
||||
name: "active caller org",
|
||||
accessToken: activeCaller,
|
||||
wantResource: "caller-org",
|
||||
wantOrgID: "caller-org",
|
||||
},
|
||||
{
|
||||
name: "deactivated caller org",
|
||||
accessToken: activeCaller,
|
||||
checkOrgActive: func(_ context.Context, orgID string) error {
|
||||
assert.Equal(t, "caller-org", orgID)
|
||||
return zerrors.ThrowPreconditionFailed(nil, "QUERY-oR9nA", "Errors.Org.NotActive")
|
||||
},
|
||||
wantErr: zerrors.IsUnauthenticated,
|
||||
},
|
||||
{
|
||||
name: "deactivated target org with active caller",
|
||||
orgID: "target-org",
|
||||
accessToken: activeCaller,
|
||||
checkOrgActive: func(_ context.Context, orgID string) error {
|
||||
// Must check the caller's org, not the target.
|
||||
assert.Equal(t, "caller-org", orgID)
|
||||
return nil
|
||||
},
|
||||
wantResource: "caller-org",
|
||||
wantOrgID: "target-org",
|
||||
},
|
||||
{
|
||||
name: "empty resource owner for system token",
|
||||
accessToken: AccessTokenVerifierFunc(func(context.Context, string) (string, string, string, string, string, error) {
|
||||
return "", "", "", "", "", zerrors.ThrowUnauthenticated(nil, "TEST-tok", "unauthenticated")
|
||||
}),
|
||||
orgID: "system-org",
|
||||
checkOrgActive: func(context.Context, string) error {
|
||||
t.Fatal("CheckOrgActive must not be called for system tokens without resource owner")
|
||||
return nil
|
||||
},
|
||||
wantOrgID: "system-org",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
verifier := &tokenVerifierMock{
|
||||
AccessTokenVerifier: tt.accessToken,
|
||||
SystemTokenVerifier: SystemTokenVerifierFunc(func(ctx context.Context, token, orgID string) (Memberships, string, error) {
|
||||
if tt.name == "empty resource owner for system token" {
|
||||
return Memberships{{MemberType: MemberTypeSystem, Roles: []string{"SYSTEM_OWNER"}}}, "system-user", nil
|
||||
}
|
||||
return systemTokenNOK.VerifySystemToken(ctx, token, orgID)
|
||||
}),
|
||||
checkOrgActive: tt.checkOrgActive,
|
||||
}
|
||||
got, err := VerifyTokenAndCreateCtxData(context.Background(), BearerPrefix+"token", tt.orgID, tt.orgDomain, verifier, nil)
|
||||
if tt.wantErr != nil {
|
||||
require.Error(t, err)
|
||||
assert.True(t, tt.wantErr(err))
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantResource, got.ResourceOwner)
|
||||
assert.Equal(t, tt.wantOrgID, got.OrgID)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user