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:
Livio Spring
2026-08-14 06:12:45 +02:00
committed by GitHub
co-authored by Cursor
parent a4266fa7fc
commit 260446f91f
52 changed files with 854 additions and 20 deletions
+1
View File
@@ -20,6 +20,7 @@ type authZRepo interface {
VerifierClientID(ctx context.Context, name string) (clientID, projectID string, err error)
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
ExistsOrg(ctx context.Context, id, domain string) (string, error)
CheckOrgActive(ctx context.Context, orgID string) error
}
var _ AccessTokenVerifier = (*AccessTokenVerifierFromRepo)(nil)
+7
View File
@@ -15,6 +15,7 @@ type APITokenVerifier interface {
CheckAuthMethod(method string) (Option, bool)
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (_ string, _ []string, err error)
ExistsOrg(ctx context.Context, id, domain string) (orgID string, err error)
CheckOrgActive(ctx context.Context, orgID string) error
SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) (_ []*Membership, err error)
}
@@ -63,6 +64,12 @@ func (v *ApiTokenVerifier) ExistsOrg(ctx context.Context, id, domain string) (or
return v.authZRepo.ExistsOrg(ctx, id, domain)
}
func (v *ApiTokenVerifier) CheckOrgActive(ctx context.Context, orgID string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return v.authZRepo.CheckOrgActive(ctx, orgID)
}
func (v *ApiTokenVerifier) CheckAuthMethod(method string) (Option, bool) {
authOpt, ok := v.authMethods[method]
return authOpt, ok
+8
View File
@@ -71,6 +71,7 @@ const (
type TokenVerifier interface {
ExistsOrg(ctx context.Context, id, domain string) (string, error)
CheckOrgActive(ctx context.Context, orgID string) error
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
AccessTokenVerifier
SystemTokenVerifier
@@ -132,6 +133,13 @@ func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain st
return CtxData{}, zerrors.ThrowPermissionDenied(nil, "AUTH-Bs7Ds", "Organisation doesn't exist")
}
}
// Reject tokens whose caller's organization is inactive. Gate resourceOwner, not the
// resolved target orgID, so instance admins can still manage a deactivated org.
if resourceOwner != "" {
if err = t.CheckOrgActive(ctx, resourceOwner); err != nil {
return CtxData{}, zerrors.ThrowUnauthenticated(err, "AUTH-oR9nA", "Errors.Org.NotActive")
}
}
return CtxData{
UserID: userID,
OrgID: orgID,
+137
View File
@@ -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)
})
}
}
@@ -0,0 +1,50 @@
//go:build integration
package org_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/auth"
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
)
func TestServer_DeactivateOrganization_RejectsCallerToken(t *testing.T) {
orgResp := Instance.CreateOrganization(CTX, integration.OrganizationName(), integration.Email())
userID := orgResp.GetCreatedAdmins()[0].GetUserId()
pat := Instance.CreatePersonalAccessToken(CTX, userID)
userCtx := integration.WithAuthorizationToken(CTX, pat.GetToken())
_, err := Instance.Client.Auth.GetMyUser(userCtx, &auth.GetMyUserRequest{})
require.NoError(t, err)
_, err = Client.DeactivateOrganization(CTX, &org.DeactivateOrganizationRequest{
OrganizationId: orgResp.GetOrganizationId(),
})
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
_, getErr := Instance.Client.Auth.GetMyUser(userCtx, &auth.GetMyUserRequest{})
assert.Error(collect, getErr)
}, retryDuration, tick, "timeout waiting for deactivated org to reject caller token")
// Instance admin can still target the deactivated org.
_, err = Client.ListOrganizations(CTX, &org.ListOrganizationsRequest{
Queries: []*org.SearchQuery{
{
Query: &org.SearchQuery_IdQuery{
IdQuery: &org.OrganizationIDQuery{
Id: orgResp.GetOrganizationId(),
},
},
},
},
})
require.NoError(t, err)
}
@@ -37,6 +37,10 @@ func (v *authzRepoMock) ExistsOrg(ctx context.Context, orgID, domain string) (st
return orgID, nil
}
func (v *authzRepoMock) CheckOrgActive(ctx context.Context, orgID string) error {
return nil
}
func (v *authzRepoMock) VerifierClientID(ctx context.Context, appName string) (string, string, error) {
return "", "", nil
}
@@ -37,6 +37,10 @@ func (v *authzRepoMock) ExistsOrg(ctx context.Context, orgID, domain string) (st
return orgID, nil
}
func (v *authzRepoMock) CheckOrgActive(ctx context.Context, orgID string) error {
return nil
}
func (v *authzRepoMock) VerifierClientID(ctx context.Context, appName string) (string, string, error) {
return "", "", nil
}
+7
View File
@@ -126,6 +126,13 @@ func (s *Server) checksToCommand(ctx context.Context, checks *session.Checks) ([
if !user.State.IsEnabled() {
return nil, zerrors.ThrowPreconditionFailed(nil, "SESSION-Gj4ko", "Errors.User.NotActive")
}
org, err := s.query.OrgByID(ctx, user.ResourceOwner)
if err != nil {
return nil, err
}
if !org.State.IsActive() {
return nil, zerrors.ThrowPreconditionFailed(nil, "SESSION-oL9nT", "Errors.User.NotActive")
}
var preferredLanguage *language.Tag
if user.Human != nil && !user.Human.PreferredLanguage.IsRoot() {
@@ -347,6 +347,13 @@ func (s *Server) checksToCommand(ctx context.Context, checks *session.Checks) ([
if !user.State.IsEnabled() {
return nil, zerrors.ThrowPreconditionFailed(nil, "SESSION-Gj4ko", "Errors.User.NotActive")
}
org, err := s.query.OrgByID(ctx, user.ResourceOwner)
if err != nil {
return nil, err
}
if !org.State.IsActive() {
return nil, zerrors.ThrowPreconditionFailed(nil, "SESSION-oL9nT", "Errors.User.NotActive")
}
var preferredLanguage *language.Tag
if user.Human != nil && !user.Human.PreferredLanguage.IsRoot() {
+14
View File
@@ -210,6 +210,13 @@ func (p *Storage) SetUserinfoWithUserID(ctx context.Context, applicationID strin
if user.State != domain.UserStateActive {
return zerrors.ThrowPreconditionFailed(nil, "SAML-S3gFd", "Errors.User.NotActive")
}
org, err := p.query.OrgByID(ctx, user.ResourceOwner)
if err != nil {
return err
}
if !org.State.IsActive() {
return zerrors.ThrowPreconditionFailed(nil, "SAML-oR9nA", "Errors.User.NotActive")
}
userGrants, err := p.getGrants(ctx, userID, applicationID)
if err != nil {
@@ -239,6 +246,13 @@ func (p *Storage) SetUserinfoWithLoginName(ctx context.Context, userinfo models.
if user.State != domain.UserStateActive {
return zerrors.ThrowPreconditionFailed(nil, "SAML-FJ262", "Errors.User.NotActive")
}
org, err := p.query.OrgByID(ctx, user.ResourceOwner)
if err != nil {
return err
}
if !org.State.IsActive() {
return zerrors.ThrowPreconditionFailed(nil, "SAML-pQ2mB", "Errors.User.NotActive")
}
setUserinfo(user, userinfo, attributes, map[string]*customAttribute{})
return nil
@@ -90,6 +90,10 @@ func (t *RefreshToken) Reducers() []handler.AggregateReducer {
Event: org.OrgRemovedEventType,
Reduce: t.Reduce,
},
{
Event: org.OrgDeactivatedEventType,
Reduce: t.Reduce,
},
},
},
}
@@ -166,7 +170,8 @@ func (t *RefreshToken) Reduce(event eventstore.Event) (_ *handler.Statement, err
handler.NewCond(view_model.RefreshTokenKeyInstanceID, event.Aggregate().InstanceID),
},
), nil
case org.OrgRemovedEventType:
case org.OrgRemovedEventType,
org.OrgDeactivatedEventType:
return handler.NewDeleteStatement(event,
[]handler.Condition{
handler.NewCond(view_model.RefreshTokenKeyInstanceID, event.Aggregate().InstanceID),
@@ -136,6 +136,10 @@ func (t *Token) Reducers() []handler.AggregateReducer {
Event: org.OrgRemovedEventType,
Reduce: t.Reduce,
},
{
Event: org.OrgDeactivatedEventType,
Reduce: t.Reduce,
},
},
},
{
@@ -311,7 +315,8 @@ func (t *Token) Reduce(event eventstore.Event) (_ *handler.Statement, err error)
handler.NewCond(view_model.TokenKeyInstanceID, event.Aggregate().InstanceID),
},
), nil
case org.OrgRemovedEventType:
case org.OrgRemovedEventType,
org.OrgDeactivatedEventType:
return handler.NewDeleteStatement(event,
[]handler.Condition{
handler.NewCond(view_model.TokenKeyInstanceID, event.Aggregate().InstanceID),
@@ -192,6 +192,10 @@ func (s *UserSession) Reducers() []handler.AggregateReducer {
Event: org.OrgRemovedEventType,
Reduce: s.Reduce,
},
{
Event: org.OrgDeactivatedEventType,
Reduce: s.Reduce,
},
},
},
{
@@ -502,7 +506,8 @@ func (u *UserSession) Reduce(event eventstore.Event) (_ *handler.Statement, err
handler.NewCond(view_model.UserSessionKeyInstanceID, event.Aggregate().InstanceID),
},
), nil
case org.OrgRemovedEventType:
case org.OrgRemovedEventType,
org.OrgDeactivatedEventType:
return handler.NewDeleteStatement(event,
[]handler.Condition{
handler.NewCond(view_model.UserSessionKeyInstanceID, event.Aggregate().InstanceID),
+3
View File
@@ -1247,6 +1247,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@@ -1345,6 +1346,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@@ -1450,6 +1452,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+10
View File
@@ -19,6 +19,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore/repository/mock"
"github.com/zitadel/zitadel/internal/execution/target"
"github.com/zitadel/zitadel/internal/feature"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -104,6 +105,15 @@ func expectFilter(events ...eventstore.Event) expect {
m.ExpectFilterEvents(events...)
}
}
func expectFilterActiveOrg(orgID string) expect {
return expectFilter(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate(orgID).Aggregate,
"org"),
)
}
func expectFilterError(err error) expect {
return func(m *mock.MockRepository) {
m.ExpectFilterEventsError(err)
+7 -8
View File
@@ -241,6 +241,9 @@ func (c *Commands) OIDCSessionByRefreshToken(ctx context.Context, refreshToken s
if err = writeModel.CheckRefreshToken(refreshTokenID); err != nil {
return nil, err
}
if err = c.checkOrgNotDeactivatedAfter(ctx, writeModel.UserResourceOwner, writeModel.RefreshTokenIssuedAt); err != nil {
return nil, err
}
return writeModel, nil
}
@@ -294,13 +297,10 @@ func (c *Commands) RevokeOIDCSessionToken(ctx context.Context, token, clientID s
}
func (c *Commands) newOIDCSessionAddEvents(ctx context.Context, userID, resourceOwner string, pending ...eventstore.Command) (*OIDCSessionEvents, error) {
userStateModel, err := c.userStateWriteModel(ctx, userID)
userStateModel, err := c.userStateForAuthentication(ctx, userID, resourceOwner, "OIDCS-kj3g2", "OIDCS-oR9nA")
if err != nil {
return nil, err
}
if !userStateModel.UserState.IsEnabled() {
return nil, zerrors.ThrowPreconditionFailed(nil, "OIDCS-kj3g2", "Errors.User.NotActive")
}
accessTokenLifetime, refreshTokenLifeTime, refreshTokenIdleLifetime, err := c.tokenTokenLifetimes(ctx)
if err != nil {
return nil, err
@@ -353,12 +353,11 @@ func (c *Commands) newOIDCSessionUpdateEvents(ctx context.Context, refreshToken
if err = sessionWriteModel.CheckRefreshToken(refreshTokenID); err != nil {
return nil, err
}
userStateWriteModel, err := c.userStateWriteModel(ctx, sessionWriteModel.UserID)
if err != nil {
if err = c.checkOrgNotDeactivatedAfter(ctx, sessionWriteModel.UserResourceOwner, sessionWriteModel.RefreshTokenIssuedAt); err != nil {
return nil, err
}
if !userStateWriteModel.UserState.IsEnabled() {
return nil, zerrors.ThrowPreconditionFailed(nil, "OIDCS-J39h2", "Errors.User.NotActive")
if _, err = c.userStateForAuthentication(ctx, sessionWriteModel.UserID, sessionWriteModel.UserResourceOwner, "OIDCS-J39h2", "OIDCS-pQ2mB"); err != nil {
return nil, err
}
accessTokenLifetime, refreshTokenLifeTime, refreshTokenIdleLifetime, err := c.tokenTokenLifetimes(ctx)
if err != nil {
+3
View File
@@ -35,6 +35,7 @@ type OIDCSessionWriteModel struct {
RefreshToken string
RefreshTokenExpiration time.Time
RefreshTokenIdleExpiration time.Time
RefreshTokenIssuedAt time.Time
aggregate *eventstore.Aggregate
}
@@ -125,6 +126,7 @@ func (wm *OIDCSessionWriteModel) reduceRefreshTokenAdded(e *oidcsession.RefreshT
wm.RefreshTokenID = e.ID
wm.RefreshTokenExpiration = e.CreationDate().Add(e.Lifetime)
wm.RefreshTokenIdleExpiration = e.CreationDate().Add(e.IdleLifetime)
wm.RefreshTokenIssuedAt = e.CreationDate()
}
func (wm *OIDCSessionWriteModel) reduceRefreshTokenRenewed(e *oidcsession.RefreshTokenRenewedEvent) {
@@ -136,6 +138,7 @@ func (wm *OIDCSessionWriteModel) reduceRefreshTokenRevoked(e *oidcsession.Refres
wm.RefreshTokenID = ""
wm.RefreshTokenExpiration = e.CreationDate()
wm.RefreshTokenIdleExpiration = e.CreationDate()
wm.RefreshTokenIssuedAt = time.Time{}
wm.AccessTokenID = ""
wm.AccessTokenExpiration = e.CreationDate()
}
+60
View File
@@ -22,6 +22,7 @@ import (
"github.com/zitadel/zitadel/internal/id/mock"
"github.com/zitadel/zitadel/internal/repository/authrequest"
"github.com/zitadel/zitadel/internal/repository/oidcsession"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/session"
"github.com/zitadel/zitadel/internal/repository/sessionlogout"
"github.com/zitadel/zitadel/internal/repository/user"
@@ -388,6 +389,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
authrequest.NewCodeExchangedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
@@ -523,6 +525,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
authrequest.NewCodeExchangedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
@@ -664,6 +667,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@@ -885,6 +889,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@@ -981,6 +986,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@@ -1062,6 +1068,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@@ -1160,6 +1167,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@@ -1258,6 +1266,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@@ -1354,6 +1363,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@@ -1459,6 +1469,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
),
idGenerator: mock.NewIDGeneratorExpectIDs(t, "oidcSessionID"),
@@ -1516,6 +1527,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
user.NewUserImpersonatedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, "clientID", &domain.TokenActor{
@@ -1799,6 +1811,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
"rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour),
),
),
expectFilter(), // no OrgDeactivated after refresh token issuance
expectFilter(
user.NewHumanAddedEvent(
context.Background(),
@@ -1835,6 +1848,48 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
err: zerrors.ThrowPreconditionFailed(nil, "OIDCS-J39h2", "Errors.User.NotActive"),
},
},
{
"org deactivated after refresh token issuance",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusherWithCreationDateNow(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"userID", "org1", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"},
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow, "nonce", &language.Afrikaans,
&domain.UserAgent{FingerprintID: gu.Ptr("browserFP")},
),
),
eventFromEventPusherWithCreationDateNow(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour, domain.TokenReasonAuthRequest, nil),
),
eventFromEventPusherWithCreationDate(
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour),
testNow,
),
),
expectFilter(
eventFromEventPusherWithCreationDate(
org.NewOrgDeactivatedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate),
testNow.Add(time.Minute),
),
),
),
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
refreshToken: "V2_oidcSessionID-rt_refreshTokenID:userID",
scope: []string{"openid", "offline_access"},
complianceCheck: mockRefreshTokenComplianceChecker(nil),
},
res{
err: zerrors.ThrowPreconditionFailed(nil, "OIDCS-oR9nR", "Errors.OIDCSession.RefreshTokenInvalid"),
},
},
{
"refresh with an invalid client id fails",
fields{
@@ -1856,6 +1911,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
"rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour),
),
),
expectFilter(), // no OrgDeactivated after refresh token issuance
expectFilter(
user.NewHumanAddedEvent(
context.Background(),
@@ -1871,6 +1927,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(),
),
idGenerator: mock.NewIDGeneratorExpectIDs(t),
@@ -1908,6 +1965,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
"rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour),
),
),
expectFilter(), // no OrgDeactivated after refresh token issuance
expectFilter(
user.NewHumanAddedEvent(
context.Background(),
@@ -1923,6 +1981,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@@ -2121,6 +2180,7 @@ func TestCommands_OIDCSessionByRefreshToken(t *testing.T) {
"rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour),
),
),
expectFilter(), // no OrgDeactivated after refresh token issuance
),
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
+60
View File
@@ -0,0 +1,60 @@
package command
import (
"context"
"time"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
// checkOrgNotDeactivatedAfter returns an invalid refresh token error if the organization
// was deactivated after the given time. This keeps refresh grants dead after reactivation.
func (c *Commands) checkOrgNotDeactivatedAfter(ctx context.Context, orgID string, after time.Time) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if orgID == "" || after.IsZero() {
return nil
}
model := &orgDeactivatedAfterModel{
orgID: orgID,
after: after,
}
if err = c.eventstore.FilterToQueryReducer(ctx, model); err != nil {
return zerrors.ThrowPreconditionFailed(err, "OIDCS-oR9nD", "Errors.Internal")
}
if model.deactivated {
return zerrors.ThrowPreconditionFailed(nil, "OIDCS-oR9nR", "Errors.OIDCSession.RefreshTokenInvalid")
}
return nil
}
type orgDeactivatedAfterModel struct {
orgID string
after time.Time
events int
deactivated bool
}
func (m *orgDeactivatedAfterModel) Reduce() error {
m.deactivated = m.events > 0
return nil
}
func (m *orgDeactivatedAfterModel) AppendEvents(events ...eventstore.Event) {
m.events += len(events)
}
func (m *orgDeactivatedAfterModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
CreationDateAfter(m.after).
AddQuery().
AggregateTypes(org.AggregateType).
AggregateIDs(m.orgID).
EventTypes(org.OrgDeactivatedEventType).
Builder()
}
+1 -4
View File
@@ -93,13 +93,10 @@ func (c *Commands) CreateSAMLSessionFromSAMLRequest(ctx context.Context, samlReq
}
func (c *Commands) newSAMLSessionAddEvents(ctx context.Context, userID, resourceOwner string, pending ...eventstore.Command) (*SAMLSessionEvents, error) {
userStateModel, err := c.userStateWriteModel(ctx, userID)
userStateModel, err := c.userStateForAuthentication(ctx, userID, resourceOwner, "SAML-1768ZQpmcP", "SAML-oR9nA")
if err != nil {
return nil, err
}
if !userStateModel.UserState.IsEnabled() {
return nil, zerrors.ThrowPreconditionFailed(nil, "SAML-1768ZQpmcP", "Errors.User.NotActive")
}
sessionID, err := c.idGenerator.Next()
if err != nil {
return nil, err
+1
View File
@@ -299,6 +299,7 @@ func TestCommands_CreateSAMLSessionFromSAMLRequest(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectPush(
samlsession.NewAddedEvent(context.Background(), &samlsession.NewAggregate("V2_samlSessionID", "org1").Aggregate,
"userID", "org1", "sessionID", "issuer", []string{"issuer"},
+39
View File
@@ -0,0 +1,39 @@
package command
import (
"context"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
func (c *Commands) userStateForAuthentication(ctx context.Context, userID, resourceOwner, userErrorID, orgErrorID string) (_ *UserV2WriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
userStateModel, err := c.userStateWriteModel(ctx, userID)
if err != nil {
return nil, err
}
if !userStateModel.UserState.IsEnabled() {
return nil, zerrors.ThrowPreconditionFailed(nil, userErrorID, "Errors.User.NotActive")
}
if err := c.checkOrgActiveForAuthentication(ctx, resourceOwner, orgErrorID); err != nil {
return nil, err
}
return userStateModel, nil
}
func (c *Commands) checkOrgActiveForAuthentication(ctx context.Context, orgID, errorID string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
orgWriteModel, err := c.getOrgWriteModelByID(ctx, orgID)
if err != nil {
return err
}
if !orgWriteModel.State.IsActive() {
return zerrors.ThrowPreconditionFailed(nil, errorID, "Errors.User.NotActive")
}
return nil
}
+148
View File
@@ -0,0 +1,148 @@
package command
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
"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 TestCommands_userStateForAuthentication(t *testing.T) {
type args struct {
ctx context.Context
userID string
resourceOwner string
userErrorID string
orgErrorID string
}
tests := []struct {
name string
eventstore func(*testing.T) *eventstore.Eventstore
args args
wantErr func(error) bool
}{
{
name: "user not active",
eventstore: expectEventstore(
expectFilter(
user.NewHumanAddedEvent(
context.Background(),
&user.NewAggregate("userID", "org1").Aggregate,
"username",
"firstname",
"lastname",
"nickname",
"displayname",
language.English,
domain.GenderUnspecified,
"email",
false,
),
user.NewUserDeactivatedEvent(
context.Background(),
&user.NewAggregate("userID", "org1").Aggregate,
),
),
),
args: args{
ctx: context.Background(),
userID: "userID",
resourceOwner: "org1",
userErrorID: "TEST-1",
orgErrorID: "TEST-1-ORG",
},
wantErr: zerrors.IsPreconditionFailed,
},
{
name: "org not active",
eventstore: expectEventstore(
expectFilter(
user.NewHumanAddedEvent(
context.Background(),
&user.NewAggregate("userID", "org1").Aggregate,
"username",
"firstname",
"lastname",
"nickname",
"displayname",
language.English,
domain.GenderUnspecified,
"email",
false,
),
),
expectFilter(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"org"),
org.NewOrgDeactivatedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate),
),
),
args: args{
ctx: context.Background(),
userID: "userID",
resourceOwner: "org1",
userErrorID: "TEST-2",
orgErrorID: "TEST-2-ORG",
},
wantErr: zerrors.IsPreconditionFailed,
},
{
name: "success",
eventstore: expectEventstore(
expectFilter(
user.NewHumanAddedEvent(
context.Background(),
&user.NewAggregate("userID", "org1").Aggregate,
"username",
"firstname",
"lastname",
"nickname",
"displayname",
language.English,
domain.GenderUnspecified,
"email",
false,
),
),
expectFilter(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"org"),
),
),
args: args{
ctx: context.Background(),
userID: "userID",
resourceOwner: "org1",
userErrorID: "TEST-3",
orgErrorID: "TEST-3-ORG",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.eventstore(t),
}
got, err := c.userStateForAuthentication(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.userErrorID, tt.args.orgErrorID)
if tt.wantErr != nil {
require.Error(t, err)
require.True(t, tt.wantErr(err))
require.Nil(t, got)
return
}
require.NoError(t, err)
require.NotNil(t, got)
require.Equal(t, "userID", got.AggregateID)
})
}
}
+4
View File
@@ -47,3 +47,7 @@ func (s OrgState) Valid() bool {
func (s OrgState) Exists() bool {
return s != OrgStateRemoved && s != OrgStateUnspecified
}
func (s OrgState) IsActive() bool {
return s == OrgStateActive
}
+19 -4
View File
@@ -11,6 +11,7 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/oidcsession"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/session"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
@@ -21,6 +22,7 @@ type OIDCSessionAccessTokenReadModel struct {
eventstore.ReadModel
UserID string
UserResourceOwner string
SessionID string
ClientID string
Audience []string
@@ -77,6 +79,7 @@ func (wm *OIDCSessionAccessTokenReadModel) Query() *eventstore.SearchQueryBuilde
func (wm *OIDCSessionAccessTokenReadModel) reduceAdded(e *oidcsession.AddedEvent) {
wm.UserID = e.UserID
wm.UserResourceOwner = e.UserResourceOwner
wm.SessionID = e.SessionID
wm.ClientID = e.ClientID
wm.Audience = e.Audience
@@ -119,7 +122,7 @@ func (q *Queries) ActiveAccessTokenByToken(ctx context.Context, token string) (m
if !model.AccessTokenExpiration.After(time.Now()) {
return nil, zerrors.ThrowUnauthenticated(nil, "QUERY-SAF3rf", "Errors.OIDCSession.Token.Expired")
}
if err = q.checkSessionNotTerminatedAfter(ctx, model.SessionID, model.UserID, model.Position, model.UserAgent.GetFingerprintID()); err != nil {
if err = q.checkSessionNotTerminatedAfter(ctx, model.SessionID, model.UserID, model.UserResourceOwner, model.Position, model.UserAgent.GetFingerprintID()); err != nil {
return nil, err
}
return model, nil
@@ -139,19 +142,20 @@ func (q *Queries) accessTokenByOIDCSessionAndTokenID(ctx context.Context, oidcSe
return model, nil
}
// checkSessionNotTerminatedAfter checks if a [session.TerminateType] event (or user events leading to a session termination)
// checkSessionNotTerminatedAfter checks if a [session.TerminateType] event (or user / org 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 decimal.Decimal, fingerprintID string) (err error) {
func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID, userID, resourceOwner string, position decimal.Decimal, fingerprintID string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if sessionID == "" && userID == "" && fingerprintID == "" {
if sessionID == "" && userID == "" && fingerprintID == "" && resourceOwner == "" {
return nil
}
model := &sessionTerminatedModel{
sessionID: sessionID,
position: position,
userID: userID,
resourceOwner: resourceOwner,
fingerPrintID: fingerprintID,
}
err = q.eventstore.FilterToQueryReducer(ctx, model)
@@ -169,6 +173,7 @@ type sessionTerminatedModel struct {
position decimal.Decimal
sessionID string
userID string
resourceOwner string
fingerPrintID string
events int
@@ -220,5 +225,15 @@ func (s *sessionTerminatedModel) Query() *eventstore.SearchQueryBuilder {
Builder()
}
}
if s.resourceOwner != "" {
builder = builder.AddQuery().
AggregateTypes(org.AggregateType).
AggregateIDs(s.resourceOwner).
EventTypes(
org.OrgDeactivatedEventType,
).
PositionAfter(s.position).
Builder()
}
return builder
}
+120
View File
@@ -0,0 +1,120 @@
package query
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/repository/oidcsession"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/zerrors"
)
func eventFromEventPusherWithCreationDateNow(event eventstore.Command) *repository.Event {
e := eventFromEventPusher(event)
e.CreationDate = time.Now()
return e
}
func TestQueries_ActiveAccessTokenByToken(t *testing.T) {
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
token string
}
type res struct {
wantUserID string
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "invalid token format",
fields: fields{
eventstore: expectEventstore(),
},
args: args{token: "invalid"},
res: res{
err: zerrors.ThrowUnauthenticated(nil, "QUERY-LJK2W", "Errors.OIDCSession.Token.Invalid"),
},
},
{
name: "active token",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusherWithCreationDateNow(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"userID", "org1", "sessionID", "clientID", []string{"audience"}, []string{"openid"},
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, time.Now(), "nonce", &language.English, nil,
),
),
eventFromEventPusherWithCreationDateNow(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"at_accessTokenID", []string{"openid"}, time.Hour, domain.TokenReasonAuthRequest, nil),
),
),
expectFilter(), // no session/user/org termination after token
),
},
args: args{token: "V2_oidcSessionID-at_accessTokenID"},
res: res{
wantUserID: "userID",
},
},
{
name: "org deactivated after token issuance",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusherWithCreationDateNow(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"userID", "org1", "sessionID", "clientID", []string{"audience"}, []string{"openid"},
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, time.Now(), "nonce", &language.English, nil,
),
),
eventFromEventPusherWithCreationDateNow(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"at_accessTokenID", []string{"openid"}, time.Hour, domain.TokenReasonAuthRequest, nil),
),
),
expectFilter(
eventFromEventPusherWithCreationDateNow(
org.NewOrgDeactivatedEvent(context.Background(), &org.NewAggregate("org1").Aggregate),
),
),
),
},
args: args{token: "V2_oidcSessionID-at_accessTokenID"},
res: res{
err: zerrors.ThrowUnauthenticated(nil, "QUERY-IJL3H", "Errors.OIDCSession.Token.Invalid"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q := &Queries{
eventstore: tt.fields.eventstore(t),
}
got, err := q.ActiveAccessTokenByToken(context.Background(), tt.args.token)
require.ErrorIs(t, err, tt.res.err)
if tt.res.err == nil {
require.NotNil(t, got)
assert.Equal(t, tt.res.wantUserID, got.UserID)
assert.Equal(t, "org1", got.UserResourceOwner)
}
})
}
}
+14
View File
@@ -264,6 +264,20 @@ func (q *Queries) ExistsOrg(ctx context.Context, id, domain string) (verifiedID
return org.ID, nil
}
func (q *Queries) CheckOrgActive(ctx context.Context, orgID string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
org, err := q.OrgByID(ctx, orgID)
if err != nil {
return err
}
if !org.State.IsActive() {
return zerrors.ThrowPreconditionFailed(nil, "QUERY-oR9nA", "Errors.Org.NotActive")
}
return nil
}
func (q *Queries) SearchOrgs(ctx context.Context, queries *OrgSearchQueries, permissionCheck domain_pkg.PermissionCheck) (*Orgs, error) {
permissionCheckV2 := PermissionV2(ctx, permissionCheck)
orgs, err := q.searchOrgs(ctx, queries, permissionCheckV2)
+31
View File
@@ -8,6 +8,7 @@ import (
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/repository/session"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
@@ -183,6 +184,19 @@ func (p *sessionProjection) Reducers() []handler.AggregateReducer {
},
},
},
{
Aggregate: org.AggregateType,
EventReducers: []handler.EventReducer{
{
Event: org.OrgDeactivatedEventType,
Reduce: p.reduceOwnerRemoved,
},
{
Event: org.OrgRemovedEventType,
Reduce: p.reduceOwnerRemoved,
},
},
},
}
}
@@ -494,3 +508,20 @@ func (p *sessionProjection) reduceUserStateNotActive(event eventstore.Event) (_
},
), nil
}
func (p *sessionProjection) reduceOwnerRemoved(event eventstore.Event) (*handler.Statement, error) {
switch t := event.(type) {
case *org.OrgDeactivatedEvent, *org.OrgRemovedEvent:
// ok
default:
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-oR9nA", "reduce.wrong.event.type %v", t)
}
return handler.NewDeleteStatement(
event,
[]handler.Condition{
handler.NewCond(SessionColumnUserResourceOwner, event.Aggregate().ID),
handler.NewCond(SessionColumnInstanceID, event.Aggregate().InstanceID),
},
), nil
}
+53
View File
@@ -10,6 +10,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore"
"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/repository/session"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
@@ -492,6 +493,58 @@ func TestSessionProjection_reduces(t *testing.T) {
},
},
},
{
name: "reduce OrgDeactivated",
args: args{
event: getEvent(testEvent(
org.OrgDeactivatedEventType,
org.AggregateType,
nil,
), org.OrgDeactivatedEventMapper),
},
reduce: (&sessionProjection{}).reduceOwnerRemoved,
want: wantReduce{
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.sessions8 WHERE (user_resource_owner = $1) AND (instance_id = $2)",
expectedArgs: []any{
"agg-id",
"instance-id",
},
},
},
},
},
},
{
name: "reduce OrgRemoved",
args: args{
event: getEvent(testEvent(
org.OrgRemovedEventType,
org.AggregateType,
nil,
), org.OrgRemovedEventMapper),
},
reduce: (&sessionProjection{}).reduceOwnerRemoved,
want: wantReduce{
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.sessions8 WHERE (user_resource_owner = $1) AND (instance_id = $2)",
expectedArgs: []any{
"agg-id",
"instance-id",
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+2 -1
View File
@@ -2,7 +2,8 @@ with usr as (
select u.id, u.creation_date, u.change_date, u.sequence, u.state, u.resource_owner, u.username, n.login_name as preferred_login_name
from projections.users14 u
left join projections.login_names3 n on u.id = n.user_id and u.instance_id = n.instance_id
where u.id = $1 and u.state = 1 -- only allow active users
join projections.orgs1 o on o.id = u.resource_owner and o.instance_id = u.instance_id and o.org_state = 1
where u.id = $1 and u.state = 1 -- only allow active users in active organizations
and u.instance_id = $2
and n.is_primary = true
),
+1
View File
@@ -205,6 +205,7 @@ Errors:
AlreadyActive: "المنظمة نشطة بالفعل"
Empty: "المنظمة فارغة"
NotFound: "المنظمة غير موجودة"
NotActive: "المنظمة غير نشطة"
NotChanged: "المنظمة لم تتغير"
DefaultOrgNotDeletable: "لا يجب حذف المنظمة الافتراضية"
ZitadelOrgNotDeletable: "المنظمة التي تحتوي على مشروع ZITADEL لا يجب حذفها"
+1
View File
@@ -204,6 +204,7 @@ Errors:
AlreadyActive: "Организацията вече е активна"
Empty: "Организацията е празна"
NotFound: "Организацията не е намерена"
NotActive: "Организацията не е активна"
NotChanged: "Организацията не е променена"
DefaultOrgNotDeletable: "Организацията по подразбиране не трябва да се изтрива"
ZitadelOrgNotDeletable: "Организация с проект Zitadel не трябва да се изтрива"
+1
View File
@@ -204,6 +204,7 @@ Errors:
AlreadyActive: "Organizace je již aktivní"
Empty: "Organizace je prázdná"
NotFound: "Organizace nenalezena"
NotActive: "Organizace není aktivní"
NotChanged: "Organizace nezměněna"
DefaultOrgNotDeletable: "Výchozí organizace nesmí být smazána"
ZitadelOrgNotDeletable: "Organizaci s projektem Zitadel nelze smazat"
+1
View File
@@ -204,6 +204,7 @@ Errors:
AlreadyActive: "Organisation ist bereits aktiv"
Empty: "Organisation ist leer"
NotFound: "Organisation konnte nicht gefunden werden"
NotActive: "Organisation ist nicht aktiv"
NotChanged: "Organisation wurde nicht verändert"
DefaultOrgNotDeletable: "Default Organisation kann nicht gelöscht werden"
ZitadelOrgNotDeletable: "Organisation mit Zitadel Projekt kann nicht gelöscht werden"
+1
View File
@@ -207,6 +207,7 @@ Errors:
AlreadyActive: "Organisation is already active"
Empty: "Organisation is empty"
NotFound: "Organisation not found"
NotActive: "Organisation is not active"
NotChanged: "Organisation not changed"
DefaultOrgNotDeletable: "Default Organisation must not be deleted"
ZitadelOrgNotDeletable: "Organisation with Zitadel project must not be deleted"
+1
View File
@@ -204,6 +204,7 @@ Errors:
AlreadyActive: "La organización ya está activada"
Empty: "La organización está vacía"
NotFound: "Organización no encontrada"
NotActive: "La organización no está activa"
NotChanged: "La organización no ha cambiado"
DefaultOrgNotDeletable: "La organización por defecto no debe borrarse"
ZitadelOrgNotDeletable: "La organización que contiene el proyecto Zitadel no debe borrarse"
+1
View File
@@ -204,6 +204,7 @@ Errors:
AlreadyActive: "L'organisation est déjà active"
Empty: "L'organisation est vide"
NotFound: "Organisation non trouvée"
NotActive: "L'organisation n'est pas active"
NotChanged: "L'organisation n'a pas changé"
DefaultOrgNotDeletable: "L'organisation par défault ne doit pas être supprimée"
ZitadelOrgNotDeletable: "L'organisation avec Zitadel project ne doit pas être supprimée"
+1
View File
@@ -204,6 +204,7 @@ Errors:
AlreadyActive: "A szervezet már aktív"
Empty: "A szervezet üres"
NotFound: "Szervezet nem található"
NotActive: "A szervezet nem aktív"
NotChanged: "A szervezet nem változott"
DefaultOrgNotDeletable: "Az alapértelmezett szervezetet nem szabad törölni"
ZitadelOrgNotDeletable: "A Zitadel projekttel rendelkező szervezetet nem szabad törölni"
+1
View File
@@ -204,6 +204,7 @@ Errors:
AlreadyActive: "Organisasi sudah aktif"
Empty: "Organisasi kosong"
NotFound: "Organisasi tidak ditemukan"
NotActive: "Organisasi tidak aktif"
NotChanged: "Organisasi tidak berubah"
DefaultOrgNotDeletable: "Organisasi Default tidak boleh dihapus"
ZitadelOrgNotDeletable: "Organisasi dengan proyek Zitadel tidak boleh dihapus"
+1
View File
@@ -204,6 +204,7 @@ Errors:
AlreadyActive: "L'organizzazione è già attiva"
Empty: "L'organizzazione è vuota"
NotFound: "Organizzazione non trovata"
NotActive: "L'organizzazione non è attiva"
NotChanged: "Organizzazione non cambiata"
DefaultOrgNotDeletable: "L'organizzazione predefinita non deve essere cancellata"
ZitadelOrgNotDeletable: "L'organizzazione con il progetto Zitadel non deve essere cancellata"
+1
View File
@@ -205,6 +205,7 @@ Errors:
AlreadyActive: "組織はすでにアクティブです"
Empty: "組織は空です"
NotFound: "組織が見つかりません"
NotActive: "組織はアクティブではありません"
NotChanged: "組織は変更されていません"
DefaultOrgNotDeletable: "デフォルトの組織は削除できません"
ZitadelOrgNotDeletable: "Zitadelプロジェクトの組織は削除できません"
+1
View File
@@ -205,6 +205,7 @@ Errors:
AlreadyActive: "조직이 이미 활성화되었습니다"
Empty: "조직이 비어 있습니다"
NotFound: "조직을 찾을 수 없습니다"
NotActive: "조직이 활성 상태가 아닙니다"
NotChanged: "조직이 변경되지 않았습니다"
DefaultOrgNotDeletable: "기본 조직은 삭제할 수 없습니다"
ZitadelOrgNotDeletable: "Zitadel 프로젝트가 포함된 조직은 삭제할 수 없습니다"
+1
View File
@@ -203,6 +203,7 @@ Errors:
AlreadyActive: "Организацијата е веќе активна"
Empty: "Организацијата е празна"
NotFound: "Организацијата не е пронајдена"
NotActive: "Организацијата не е активна"
NotChanged: "Организацијата не е променета"
DefaultOrgNotDeletable: "Стандардната организација не смее да биде избришана"
ZitadelOrgNotDeletable: "Организацијата со Zitadel проект не смее да биде избришана"
+1
View File
@@ -204,6 +204,7 @@ Errors:
AlreadyActive: "Organisatie is al actief"
Empty: "Organisatie is leeg"
NotFound: "Organisatie niet gevonden"
NotActive: "Organisatie is niet actief"
NotChanged: "Organisatie is niet veranderd"
DefaultOrgNotDeletable: "Standaard organisatie kan niet worden verwijderd"
ZitadelOrgNotDeletable: "Organisatie met Zitadel-project kan niet worden verwijderd"
+1
View File
@@ -204,6 +204,7 @@ Errors:
AlreadyActive: "Organizacja jest już aktywna"
Empty: "Organizacja jest pusta"
NotFound: "Organizacja nie znaleziona"
NotActive: "Organizacja nie jest aktywna"
NotChanged: "Organizacja nie zmieniona"
DefaultOrgNotDeletable: "Domyślna organizacja nie może być usunięta"
ZitadelOrgNotDeletable: "Organizacja z projektem Zitadel nie może być usunięta"
+1
View File
@@ -203,6 +203,7 @@ Errors:
AlreadyActive: "Organização já está ativa"
Empty: "Organização está vazia"
NotFound: "Organização não encontrada"
NotActive: "A organização não está ativa"
NotChanged: "Organização não alterada"
DefaultOrgNotDeletable: "A organização padrão não pode ser excluída"
ZitadelOrgNotDeletable: "A organização com o projeto Zitadel não pode ser excluída"
+1
View File
@@ -205,6 +205,7 @@ Errors:
AlreadyActive: "Organizația este deja activă"
Empty: "Organizația este goală"
NotFound: "Organizația nu a fost găsită"
NotActive: "Organizația nu este activă"
NotChanged: "Organizația nu a fost schimbată"
DefaultOrgNotDeletable: "Organizația implicită nu trebuie să fie ștearsă"
ZitadelOrgNotDeletable: "Organizația cu proiectul Zitadel nu trebuie să fie ștearsă"
+1
View File
@@ -204,6 +204,7 @@ Errors:
AlreadyActive: "Организация уже активна"
Empty: "Организация не заполнена"
NotFound: "Организация не найдена"
NotActive: "Организация неактивна"
NotChanged: "Организация не изменена"
DefaultOrgNotDeletable: "Организация по умолчанию не может быть удалена"
ZitadelOrgNotDeletable: "Невозможно удалить организацию с проектом Zitadel"
+1
View File
@@ -204,6 +204,7 @@ Errors:
AlreadyActive: "Organisationen är redan aktiv"
Empty: "Organisationen är tom"
NotFound: "Organisationen hittades inte"
NotActive: "Organisationen är inte aktiv"
NotChanged: "Organisationen ändrades inte"
DefaultOrgNotDeletable: "Standardorganisationen får inte raderas"
ZitadelOrgNotDeletable: "Organisationen med Zitadel-projekt får inte raderas"
+1
View File
@@ -205,6 +205,7 @@ Errors:
AlreadyActive: "Organizasyon zaten aktif"
Empty: "Organizasyon boş"
NotFound: "Organizasyon bulunamadı"
NotActive: "Organizasyon aktif değil"
NotChanged: "Organizasyon değişmedi"
DefaultOrgNotDeletable: "Varsayılan Organizasyon silinmemeli"
ZitadelOrgNotDeletable: "Zitadel projesi olan organizasyon silinmemeli"
+1
View File
@@ -199,6 +199,7 @@ Errors:
AlreadyActive: "Організація вже активна"
Empty: "Організація порожня"
NotFound: "Організація не знайдена"
NotActive: "Організація неактивна"
NotChanged: "Організація не змінена"
DefaultOrgNotDeletable: "Організація за замовчуванням не може бути видалена"
ZitadelOrgNotDeletable: "Організація з проектом Zitadel не може бути видалена"
+1
View File
@@ -205,6 +205,7 @@ Errors:
AlreadyActive: "组织已处于启用状态"
Empty: "组织为空"
NotFound: "未找到组织"
NotActive: "组织未激活"
NotChanged: "组织信息未改变"
DefaultOrgNotDeletable: "默认组织不应删除"
ZitadelOrgNotDeletable: "不得删除与Zitadel项目有关的组织"