mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,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
|
||||
),
|
||||
|
||||
@@ -205,6 +205,7 @@ Errors:
|
||||
AlreadyActive: "المنظمة نشطة بالفعل"
|
||||
Empty: "المنظمة فارغة"
|
||||
NotFound: "المنظمة غير موجودة"
|
||||
NotActive: "المنظمة غير نشطة"
|
||||
NotChanged: "المنظمة لم تتغير"
|
||||
DefaultOrgNotDeletable: "لا يجب حذف المنظمة الافتراضية"
|
||||
ZitadelOrgNotDeletable: "المنظمة التي تحتوي على مشروع ZITADEL لا يجب حذفها"
|
||||
|
||||
@@ -204,6 +204,7 @@ Errors:
|
||||
AlreadyActive: "Организацията вече е активна"
|
||||
Empty: "Организацията е празна"
|
||||
NotFound: "Организацията не е намерена"
|
||||
NotActive: "Организацията не е активна"
|
||||
NotChanged: "Организацията не е променена"
|
||||
DefaultOrgNotDeletable: "Организацията по подразбиране не трябва да се изтрива"
|
||||
ZitadelOrgNotDeletable: "Организация с проект Zitadel не трябва да се изтрива"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -205,6 +205,7 @@ Errors:
|
||||
AlreadyActive: "組織はすでにアクティブです"
|
||||
Empty: "組織は空です"
|
||||
NotFound: "組織が見つかりません"
|
||||
NotActive: "組織はアクティブではありません"
|
||||
NotChanged: "組織は変更されていません"
|
||||
DefaultOrgNotDeletable: "デフォルトの組織は削除できません"
|
||||
ZitadelOrgNotDeletable: "Zitadelプロジェクトの組織は削除できません"
|
||||
|
||||
@@ -205,6 +205,7 @@ Errors:
|
||||
AlreadyActive: "조직이 이미 활성화되었습니다"
|
||||
Empty: "조직이 비어 있습니다"
|
||||
NotFound: "조직을 찾을 수 없습니다"
|
||||
NotActive: "조직이 활성 상태가 아닙니다"
|
||||
NotChanged: "조직이 변경되지 않았습니다"
|
||||
DefaultOrgNotDeletable: "기본 조직은 삭제할 수 없습니다"
|
||||
ZitadelOrgNotDeletable: "Zitadel 프로젝트가 포함된 조직은 삭제할 수 없습니다"
|
||||
|
||||
@@ -203,6 +203,7 @@ Errors:
|
||||
AlreadyActive: "Организацијата е веќе активна"
|
||||
Empty: "Организацијата е празна"
|
||||
NotFound: "Организацијата не е пронајдена"
|
||||
NotActive: "Организацијата не е активна"
|
||||
NotChanged: "Организацијата не е променета"
|
||||
DefaultOrgNotDeletable: "Стандардната организација не смее да биде избришана"
|
||||
ZitadelOrgNotDeletable: "Организацијата со Zitadel проект не смее да биде избришана"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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ă"
|
||||
|
||||
@@ -204,6 +204,7 @@ Errors:
|
||||
AlreadyActive: "Организация уже активна"
|
||||
Empty: "Организация не заполнена"
|
||||
NotFound: "Организация не найдена"
|
||||
NotActive: "Организация неактивна"
|
||||
NotChanged: "Организация не изменена"
|
||||
DefaultOrgNotDeletable: "Организация по умолчанию не может быть удалена"
|
||||
ZitadelOrgNotDeletable: "Невозможно удалить организацию с проектом Zitadel"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -199,6 +199,7 @@ Errors:
|
||||
AlreadyActive: "Організація вже активна"
|
||||
Empty: "Організація порожня"
|
||||
NotFound: "Організація не знайдена"
|
||||
NotActive: "Організація неактивна"
|
||||
NotChanged: "Організація не змінена"
|
||||
DefaultOrgNotDeletable: "Організація за замовчуванням не може бути видалена"
|
||||
ZitadelOrgNotDeletable: "Організація з проектом Zitadel не може бути видалена"
|
||||
|
||||
@@ -205,6 +205,7 @@ Errors:
|
||||
AlreadyActive: "组织已处于启用状态"
|
||||
Empty: "组织为空"
|
||||
NotFound: "未找到组织"
|
||||
NotActive: "组织未激活"
|
||||
NotChanged: "组织信息未改变"
|
||||
DefaultOrgNotDeletable: "默认组织不应删除"
|
||||
ZitadelOrgNotDeletable: "不得删除与Zitadel项目有关的组织"
|
||||
|
||||
Reference in New Issue
Block a user