fix: keep user idp links (#7079)

* login

* auth methods

* NewIDPUserLinksActiveQuery

* use has_login_policy projection

* fix unit tests

* docs

* keep old user links projection

* fix tests

* cleanup

* cleanup comments

* test idp links are not removed

* idempotent auth method test

* idempotent auth method test
This commit is contained in:
Elio Bischof 2023-12-19 11:25:50 +01:00 committed by GitHub
parent 2c4e7070ea
commit c3e6257d68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 57 additions and 235 deletions

View File

@ -1,2 +1,5 @@
Once you created the provider, it is listed in the providers overview.
Activate it by selecting the tick with the tooltip *set as available*.
Activate it by selecting the tick with the tooltip *set as available*.
If you deactivate a provider, your users with links to it will not be able to authenticate anymore.
You can reactivate it and the logins will work again.

View File

@ -7,9 +7,7 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/idp"
"github.com/zitadel/zitadel/internal/api/grpc/object"
policy_grpc "github.com/zitadel/zitadel/internal/api/grpc/policy"
"github.com/zitadel/zitadel/internal/api/grpc/user"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
@ -61,19 +59,7 @@ func (s *Server) AddIDPToLoginPolicy(ctx context.Context, req *admin_pb.AddIDPTo
}
func (s *Server) RemoveIDPFromLoginPolicy(ctx context.Context, req *admin_pb.RemoveIDPFromLoginPolicyRequest) (*admin_pb.RemoveIDPFromLoginPolicyResponse, error) {
idpQuery, err := query.NewIDPUserLinkIDPIDSearchQuery(req.IdpId)
if err != nil {
return nil, err
}
idps, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{
Queries: []query.SearchQuery{idpQuery},
}, true)
if err != nil {
return nil, err
}
objectDetails, err := s.command.RemoveIDPProviderFromDefaultLoginPolicy(ctx, &domain.IDPProvider{IDPConfigID: req.IdpId}, user.ExternalIDPViewsToExternalIDPs(idps.Links)...)
objectDetails, err := s.command.RemoveIDPProviderFromDefaultLoginPolicy(ctx, &domain.IDPProvider{IDPConfigID: req.IdpId})
if err != nil {
return nil, err
}

View File

@ -7,9 +7,7 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/idp"
"github.com/zitadel/zitadel/internal/api/grpc/object"
policy_grpc "github.com/zitadel/zitadel/internal/api/grpc/policy"
"github.com/zitadel/zitadel/internal/api/grpc/user"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management"
)
@ -94,21 +92,7 @@ func (s *Server) AddIDPToLoginPolicy(ctx context.Context, req *mgmt_pb.AddIDPToL
func (s *Server) RemoveIDPFromLoginPolicy(ctx context.Context, req *mgmt_pb.RemoveIDPFromLoginPolicyRequest) (*mgmt_pb.RemoveIDPFromLoginPolicyResponse, error) {
orgID := authz.GetCtxData(ctx).OrgID
idpQuery, err := query.NewIDPUserLinkIDPIDSearchQuery(req.IdpId)
if err != nil {
return nil, err
}
resourceOwnerQuery, err := query.NewIDPUserLinksResourceOwnerSearchQuery(orgID)
if err != nil {
return nil, err
}
userLinks, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{
Queries: []query.SearchQuery{idpQuery, resourceOwnerQuery},
}, false)
if err != nil {
return nil, err
}
objectDetails, err := s.command.RemoveIDPFromLoginPolicy(ctx, orgID, &domain.IDPProvider{IDPConfigID: req.IdpId}, user.ExternalIDPViewsToExternalIDPs(userLinks.Links)...)
objectDetails, err := s.command.RemoveIDPFromLoginPolicy(ctx, orgID, &domain.IDPProvider{IDPConfigID: req.IdpId})
if err != nil {
return nil, err
}

View File

@ -13,11 +13,14 @@ import (
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/idp"
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
@ -1063,12 +1066,27 @@ func TestServer_ListAuthenticationMethodTypes(t *testing.T) {
ClientSecret: "client_secret",
})
require.NoError(t, err)
_, err = Tester.Client.Mgmt.AddCustomLoginPolicy(CTX, &mgmt.AddCustomLoginPolicyRequest{})
require.Condition(t, func() bool {
code := status.Convert(err).Code()
return code == codes.AlreadyExists || code == codes.OK
})
_, err = Tester.Client.Mgmt.AddIDPToLoginPolicy(CTX, &mgmt.AddIDPToLoginPolicyRequest{
IdpId: provider.GetId(),
OwnerType: idp.IDPOwnerType_IDP_OWNER_TYPE_ORG,
})
require.NoError(t, err)
idpLink, err := Tester.Client.UserV2.AddIDPLink(CTX, &user.AddIDPLinkRequest{UserId: userMultipleAuth, IdpLink: &user.IDPLink{
IdpId: provider.GetId(),
UserId: "external-id",
UserName: "displayName",
}})
require.NoError(t, err)
// This should not remove the user IDP links
_, err = Tester.Client.Mgmt.RemoveIDPFromLoginPolicy(CTX, &mgmt.RemoveIDPFromLoginPolicyRequest{
IdpId: provider.GetId(),
})
require.NoError(t, err)
type args struct {
ctx context.Context

View File

@ -382,7 +382,7 @@ func (l *Login) migrateExternalUserID(r *http.Request, authReq *domain.AuthReque
return previousIDMatched, l.command.MigrateUserIDP(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, externalUser.IDPConfigID, previousID, externalUserID)
}
// handleExternalUserAuthenticated maps the IDP user, checks for a corresponding externalID
// handleExternalUserAuthenticated maps the IDP user, checks for a corresponding externalID and that the IDP is allowed
func (l *Login) handleExternalUserAuthenticated(
w http.ResponseWriter,
r *http.Request,
@ -393,6 +393,11 @@ func (l *Login) handleExternalUserAuthenticated(
callback func(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest),
) {
externalUser := mapIDPUserToExternalUser(user, provider.ID)
// ensure the linked IDP is added to the login policy
if err := l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, provider.ID, authReq.AgentID); err != nil {
l.renderError(w, r, authReq, err)
return
}
// check and fill in local linked user
externalErr := l.authRepo.CheckExternalUserLogin(setContext(r.Context(), ""), authReq.ID, authReq.AgentID, externalUser, domain.BrowserInfoFromRequest(r), false)
if externalErr != nil && !zerrors.IsNotFound(externalErr) {

View File

@ -163,7 +163,7 @@ func (c *Commands) RemoveDefaultIDPConfig(ctx context.Context, idpID string, idp
events = append(events, userEvents...)
}
orgAgg := OrgAggregateFromWriteModel(&NewOrgIdentityProviderWriteModel(idpProvider.AggregateID, idpID).WriteModel)
orgEvents := c.removeIDPFromLoginPolicy(ctx, orgAgg, idpID, true)
orgEvents := c.removeIDPFromLoginPolicy(ctx, orgAgg, idpID, true, externalIDPs...)
events = append(events, orgEvents...)
}

View File

@ -66,7 +66,7 @@ func (c *Commands) AddIDPProviderToDefaultLoginPolicy(ctx context.Context, idpPr
return writeModelToIDPProvider(&idpModel.IdentityProviderWriteModel), nil
}
func (c *Commands) RemoveIDPProviderFromDefaultLoginPolicy(ctx context.Context, idpProvider *domain.IDPProvider, cascadeExternalIDPs ...*domain.UserIDPLink) (*domain.ObjectDetails, error) {
func (c *Commands) RemoveIDPProviderFromDefaultLoginPolicy(ctx context.Context, idpProvider *domain.IDPProvider) (*domain.ObjectDetails, error) {
if !idpProvider.IsValid() {
return nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-66m9s", "Errors.IAM.LoginPolicy.IDP.Invalid")
}
@ -89,7 +89,7 @@ func (c *Commands) RemoveIDPProviderFromDefaultLoginPolicy(ctx context.Context,
}
instanceAgg := InstanceAggregateFromWriteModel(&idpModel.IdentityProviderWriteModel.WriteModel)
events := c.removeIDPProviderFromDefaultLoginPolicy(ctx, instanceAgg, idpProvider, false, cascadeExternalIDPs...)
events := c.removeIDPProviderFromDefaultLoginPolicy(ctx, instanceAgg, idpProvider, false)
pushedEvents, err := c.eventstore.Push(ctx, events...)
if err != nil {
return nil, err

View File

@ -13,7 +13,6 @@ import (
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/policy"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
)
@ -458,9 +457,8 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
provider *domain.IDPProvider
cascadeExternalIDPs []*domain.UserIDPLink
ctx context.Context
provider *domain.IDPProvider
}
type res struct {
want *domain.ObjectDetails
@ -707,89 +705,6 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
provider: &domain.IDPProvider{
IDPConfigID: "config1",
},
cascadeExternalIDPs: []*domain.UserIDPLink{
{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
},
IDPConfigID: "config1",
},
},
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
{
name: "remove provider with external idps, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewLoginPolicyAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
time.Hour*2,
time.Hour*3,
time.Hour*4,
time.Hour*5,
),
),
),
expectFilter(
eventFromEventPusher(
instance.NewIdentityProviderAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"config1",
),
),
),
expectFilter(
eventFromEventPusher(
user.NewUserIDPLinkAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"config1", "", "externaluser1"),
),
),
expectPush(
instance.NewIdentityProviderRemovedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"config1"),
user.NewUserIDPLinkCascadeRemovedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"config1", "externaluser1"),
),
),
},
args: args{
ctx: context.Background(),
provider: &domain.IDPProvider{
IDPConfigID: "config1",
},
cascadeExternalIDPs: []*domain.UserIDPLink{
{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
},
IDPConfigID: "config1",
ExternalUserID: "externaluser1",
},
},
},
res: res{
want: &domain.ObjectDetails{
@ -803,7 +718,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.RemoveIDPProviderFromDefaultLoginPolicy(tt.args.ctx, tt.args.provider, tt.args.cascadeExternalIDPs...)
got, err := r.RemoveIDPProviderFromDefaultLoginPolicy(tt.args.ctx, tt.args.provider)
if tt.res.err == nil {
assert.NoError(t, err)
}

View File

@ -178,7 +178,7 @@ func (c *Commands) AddIDPToLoginPolicy(ctx context.Context, resourceOwner string
return writeModelToIDPProvider(&idpModel.IdentityProviderWriteModel), nil
}
func (c *Commands) RemoveIDPFromLoginPolicy(ctx context.Context, resourceOwner string, idpProvider *domain.IDPProvider, cascadeExternalIDPs ...*domain.UserIDPLink) (*domain.ObjectDetails, error) {
func (c *Commands) RemoveIDPFromLoginPolicy(ctx context.Context, resourceOwner string, idpProvider *domain.IDPProvider) (*domain.ObjectDetails, error) {
if resourceOwner == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "Org-M0fs9", "Errors.ResourceOwnerMissing")
}
@ -203,7 +203,7 @@ func (c *Commands) RemoveIDPFromLoginPolicy(ctx context.Context, resourceOwner s
}
orgAgg := OrgAggregateFromWriteModel(&idpModel.IdentityProviderWriteModel.WriteModel)
events := c.removeIDPFromLoginPolicy(ctx, orgAgg, idpProvider.IDPConfigID, false, cascadeExternalIDPs...)
events := c.removeIDPFromLoginPolicy(ctx, orgAgg, idpProvider.IDPConfigID, false)
pushedEvents, err := c.eventstore.Push(ctx, events...)
if err != nil {

View File

@ -13,7 +13,6 @@ import (
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/policy"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
)
@ -1006,10 +1005,9 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
resourceOwner string
provider *domain.IDPProvider
cascadeExternalIDPs []*domain.UserIDPLink
ctx context.Context
resourceOwner string
provider *domain.IDPProvider
}
type res struct {
want *domain.ObjectDetails
@ -1238,7 +1236,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
},
},
{
name: "remove provider external idp not found, ok",
name: "remove provider from login policy, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
@ -1290,93 +1288,6 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
Name: "name",
Type: domain.IdentityProviderTypeOrg,
},
cascadeExternalIDPs: []*domain.UserIDPLink{
{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
},
IDPConfigID: "config1",
},
},
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "remove provider with external idps, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewLoginPolicyAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
time.Hour*2,
time.Hour*3,
time.Hour*4,
time.Hour*5,
),
),
),
expectFilter(
eventFromEventPusher(
org.NewIdentityProviderAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"config1",
domain.IdentityProviderTypeOrg,
),
),
),
expectFilter(
eventFromEventPusher(
user.NewUserIDPLinkAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"config1", "", "externaluser1"),
),
),
expectPush(
org.NewIdentityProviderRemovedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"config1",
),
user.NewUserIDPLinkCascadeRemovedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"config1", "externaluser1",
),
),
),
},
args: args{
ctx: context.Background(),
resourceOwner: "org1",
provider: &domain.IDPProvider{
IDPConfigID: "config1",
},
cascadeExternalIDPs: []*domain.UserIDPLink{
{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
},
IDPConfigID: "config1",
ExternalUserID: "externaluser1",
},
},
},
res: res{
want: &domain.ObjectDetails{
@ -1390,7 +1301,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.RemoveIDPFromLoginPolicy(tt.args.ctx, tt.args.resourceOwner, tt.args.provider, tt.args.cascadeExternalIDPs...)
got, err := r.RemoveIDPFromLoginPolicy(tt.args.ctx, tt.args.resourceOwner, tt.args.provider)
if tt.res.err == nil {
assert.NoError(t, err)
}

View File

@ -146,7 +146,7 @@ Errors:
ExternalIDP:
Invalid: Невалиден външен IDP
IDPConfigNotExisting: Невалиден доставчик на IDP за тази организация
NotAllowed: Външен IDP не е разрешен в тази организация
NotAllowed: Външен IDP не е разрешен
MinimumExternalIDPNeeded: Трябва да се добави поне един IDP
AlreadyExists: Външен IDP вече е зает
NotFound: Външен IDP не е намерен

View File

@ -143,7 +143,7 @@ Errors:
ExternalIDP:
Invalid: Externí IDP je neplatné
IDPConfigNotExisting: Konfigurace poskytovatele IDP je pro tuto organizaci neplatná
NotAllowed: Externí IDP není v této organizaci povoleno
NotAllowed: Externí IDP není povolen
MinimumExternalIDPNeeded: Musí být přidán alespoň jeden IDP
AlreadyExists: Externí IDP již obsazeno
NotFound: Externí IDP nenalezeno

View File

@ -144,7 +144,7 @@ Errors:
ExternalIDP:
Invalid: Externer IDP ungültig
IDPConfigNotExisting: IDP Provider ungültig für diese Organisation
NotAllowed: Externer IDP ist auf dieser Organisation nicht erlaubt.
NotAllowed: Externer IDP nicht erlaubt
MinimumExternalIDPNeeded: Mindestens ein IDP muss hinzugefügt werden.
AlreadyExists: External IDP ist bereits vergeben
NotFound: Externer IDP nicht gefunden

View File

@ -144,7 +144,7 @@ Errors:
ExternalIDP:
Invalid: External IDP invalid
IDPConfigNotExisting: IDP provider invalid for this organization
NotAllowed: External IDP not allowed on this organization
NotAllowed: External IDP not allowed
MinimumExternalIDPNeeded: At least one IDP must be added
AlreadyExists: External IDP already taken
NotFound: External IDP not found

View File

@ -144,7 +144,7 @@ Errors:
ExternalIDP:
Invalid: IDP externo no válido
IDPConfigNotExisting: Proveedor IDP no válido para esta organización
NotAllowed: IDP externo no permitido para esta organización
NotAllowed: IDP externo no permitido
MinimumExternalIDPNeeded: Al menos de añadirse un IDP
AlreadyExists: IDP externo ya cogido
NotFound: IDP no encontrado

View File

@ -144,7 +144,7 @@ Errors:
ExternalIDP:
Invalid: IDP Externer invalide
IDPConfigNotExisting: Le fournisseur IDP n'est pas valide pour cette organisation
NotAllowed: IDP externe non autorisé pour cette organisation
NotAllowed: IDP externe non autorisé
MinimumExternalIDPNeeded: Au moins un IDP doit être ajouté
AlreadyExists: External IDP déjà pris
NotFound: IDP externe non trouvé

View File

@ -144,7 +144,7 @@ Errors:
ExternalIDP:
Invalid: IDP esterno non valido
IDPConfigNotExisting: IDP non valido per questa organizzazione
NotAllowed: IDP esterno non consentito su questa organizzazione
NotAllowed: IDP esterno non consentito
MinimumExternalIDPNeeded: Almeno un IDP deve essere aggiunto
AlreadyExists: IDP esterno già preso
NotFound: IDP esterno non trovato

View File

@ -136,7 +136,7 @@ Errors:
ExternalIDP:
Invalid: 無効な外部IDPです
IDPConfigNotExisting: この組織はIDPプロバイダーが無効です
NotAllowed: この組織では外部IDPが許可されていません
NotAllowed: 外部IDPは許可されていません
MinimumExternalIDPNeeded: 少なくとも1つのIDPを追加する必要があります
AlreadyExists: 外部IDPはすでに使用されています
NotFound: 外部IDPが見つかりません

View File

@ -144,7 +144,7 @@ Errors:
ExternalIDP:
Invalid: Невалиден надворешен IDP
IDPConfigNotExisting: IDP не е валиден за оваа организација
NotAllowed: Надворешниоте IDP не е дозволен на оваа организација
NotAllowed: Надворешниот IDP не е дозволен
MinimumExternalIDPNeeded: Мора да се додаде најмалку еден надворешен IDP
AlreadyExists: Надворешниот IDP е веќе зафатен
NotFound: Надворешниот IDP не е пронајден

View File

@ -144,7 +144,7 @@ Errors:
ExternalIDP:
Invalid: Externe IDP ongeldig
IDPConfigNotExisting: IDP provider ongeldig voor deze organisatie
NotAllowed: Externe IDP niet toegestaan op deze organisatie
NotAllowed: Externe IDP niet toegestaan
MinimumExternalIDPNeeded: Er moet minstens één IDP worden toegevoegd
AlreadyExists: Externe IDP al ingenomen
NotFound: Externe IDP niet gevonden

View File

@ -144,7 +144,7 @@ Errors:
ExternalIDP:
Invalid: Nieprawidłowy IDP zewnętrzny
IDPConfigNotExisting: Dostawca IDP jest nieprawidłowy dla tej organizacji
NotAllowed: IDP zewnętrzne nie jest dozwolone w tej organizacji
NotAllowed: IDP zewnętrzne nie jest dozwolone
MinimumExternalIDPNeeded: Przynajmniej jeden IDP musi być dodany
AlreadyExists: IDP zewnętrzne już istnieje
NotFound: IDP zewnętrzne nie znaleziony

View File

@ -143,7 +143,7 @@ Errors:
ExternalIDP:
Invalid: IDP externo inválido
IDPConfigNotExisting: Provedor de IDP inválido para esta organização
NotAllowed: IDP externo não permitido nesta organização
NotAllowed: IDP externo não permitido
MinimumExternalIDPNeeded: Pelo menos um IDP deve ser adicionado
AlreadyExists: IDP externo já está em uso
NotFound: IDP externo não encontrado

View File

@ -143,7 +143,7 @@ Errors:
ExternalIDP:
Invalid: Внешний идентификационный номер недействителен.
IDPConfigNotExisting: Поставщик МВУ недействителен для этой организации.
NotAllowed: Внешний IDP не разрешен в этой организации.
NotAllowed: Внешний IDP не разрешен
MinimumExternalIDPNeeded: Необходимо добавить хотя бы одного ВПЛ.
AlreadyExists: Внешнее ВПЛ уже занято
NotFound: Внешний IDP не найден

View File

@ -144,7 +144,7 @@ Errors:
ExternalIDP:
Invalid: 外部 IDP 无效
IDPConfigNotExisting: IDP 提供者对此组织无效
NotAllowed: 此组织不允许外部 IDP
NotAllowed: 外部 IDP 不允许
MinimumExternalIDPNeeded: 必须添加至少一个 IDP
AlreadyExists: 外部 IDP 已存在
NotFound: 未找到外部 IDP