Merge commit '416a35537f89b1c3ccd3d123289cea37b3309bba' into next-rc

This commit is contained in:
Stefan Benz
2025-07-29 18:15:55 +02:00
656 changed files with 45737 additions and 41051 deletions

View File

@@ -201,7 +201,7 @@ func (a *API) registerConnectServer(service server.ConnectServer) {
methodNames[i] = string(methods.Get(i).Name())
}
a.connectServices[prefix] = methodNames
a.RegisterHandlerPrefixes(handler, prefix)
a.RegisterHandlerPrefixes(http_mw.CORSInterceptor(handler), prefix)
}
// HandleFunc allows registering a [http.HandlerFunc] on an exact

View File

@@ -477,10 +477,10 @@ func waitForExecutionOnCondition(ctx context.Context, t *testing.T, instance *in
if !assert.NoError(ttt, err) {
return
}
if !assert.Len(ttt, got.GetResult(), 1) {
if !assert.Len(ttt, got.GetExecutions(), 1) {
return
}
gotTargets := got.GetResult()[0].GetTargets()
gotTargets := got.GetExecutions()[0].GetTargets()
// always first check length, otherwise its failed anyway
if assert.Len(ttt, gotTargets, len(targets)) {
for i := range targets {
@@ -506,10 +506,10 @@ func waitForTarget(ctx context.Context, t *testing.T, instance *integration.Inst
if !assert.NoError(ttt, err) {
return
}
if !assert.Len(ttt, got.GetResult(), 1) {
if !assert.Len(ttt, got.GetTargets(), 1) {
return
}
config := got.GetResult()[0]
config := got.GetTargets()[0]
assert.Equal(ttt, config.GetEndpoint(), endpoint)
switch ty {
case domain.TargetTypeWebhook:
@@ -605,7 +605,7 @@ func TestServer_ExecutionTargetPreUserinfo(t *testing.T) {
{Key: "added", Value: "value"},
},
}
return expectPreUserinfoExecution(ctx, t, instance, req, response)
return expectPreUserinfoExecution(ctx, t, instance, client.GetClientId(), req, response)
},
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
@@ -630,7 +630,7 @@ func TestServer_ExecutionTargetPreUserinfo(t *testing.T) {
"addedLog",
},
}
return expectPreUserinfoExecution(ctx, t, instance, req, response)
return expectPreUserinfoExecution(ctx, t, instance, client.GetClientId(), req, response)
},
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
@@ -655,7 +655,7 @@ func TestServer_ExecutionTargetPreUserinfo(t *testing.T) {
{Key: "key", Value: []byte("value")},
},
}
return expectPreUserinfoExecution(ctx, t, instance, req, response)
return expectPreUserinfoExecution(ctx, t, instance, client.GetClientId(), req, response)
},
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
@@ -692,7 +692,7 @@ func TestServer_ExecutionTargetPreUserinfo(t *testing.T) {
{Key: "added3", Value: "value3"},
},
}
return expectPreUserinfoExecution(ctx, t, instance, req, response)
return expectPreUserinfoExecution(ctx, t, instance, client.GetClientId(), req, response)
},
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
@@ -755,7 +755,7 @@ func TestServer_ExecutionTargetPreUserinfo(t *testing.T) {
}
}
func expectPreUserinfoExecution(ctx context.Context, t *testing.T, instance *integration.Instance, req *oidc_pb.CreateCallbackRequest, response *oidc_api.ContextInfoResponse) (string, func()) {
func expectPreUserinfoExecution(ctx context.Context, t *testing.T, instance *integration.Instance, clientID string, req *oidc_pb.CreateCallbackRequest, response *oidc_api.ContextInfoResponse) (string, func()) {
userEmail := gofakeit.Email()
userPhone := "+41" + gofakeit.Phone()
userResp := instance.CreateHumanUserVerified(ctx, instance.DefaultOrg.Id, userEmail, userPhone)
@@ -767,7 +767,7 @@ func expectPreUserinfoExecution(ctx context.Context, t *testing.T, instance *int
SessionToken: sessionResp.GetSessionToken(),
},
}
expectedContextInfo := contextInfoForUserOIDC(instance, "function/preuserinfo", userResp, userEmail, userPhone)
expectedContextInfo := contextInfoForUserOIDC(instance, "function/preuserinfo", clientID, userResp, userEmail, userPhone)
targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response)
@@ -845,7 +845,7 @@ func getAccessTokenClaims(ctx context.Context, t *testing.T, instance *integrati
return claims
}
func contextInfoForUserOIDC(instance *integration.Instance, function string, userResp *user.AddHumanUserResponse, email, phone string) *oidc_api.ContextInfo {
func contextInfoForUserOIDC(instance *integration.Instance, function string, clientID string, userResp *user.AddHumanUserResponse, email, phone string) *oidc_api.ContextInfo {
return &oidc_api.ContextInfo{
Function: function,
UserInfo: &oidc.UserInfo{
@@ -878,6 +878,9 @@ func contextInfoForUserOIDC(instance *integration.Instance, function string, use
},
},
UserMetadata: nil,
Application: &oidc_api.ContextInfoApplication{
ClientID: clientID,
},
Org: &query.UserInfoOrg{
ID: instance.DefaultOrg.GetId(),
Name: instance.DefaultOrg.GetName(),
@@ -918,7 +921,7 @@ func TestServer_ExecutionTargetPreAccessToken(t *testing.T) {
{Key: "added1", Value: "value"},
},
}
return expectPreAccessTokenExecution(ctx, t, instance, req, response)
return expectPreAccessTokenExecution(ctx, t, instance, client.GetClientId(), req, response)
},
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
@@ -943,7 +946,7 @@ func TestServer_ExecutionTargetPreAccessToken(t *testing.T) {
"addedLog",
},
}
return expectPreAccessTokenExecution(ctx, t, instance, req, response)
return expectPreAccessTokenExecution(ctx, t, instance, client.GetClientId(), req, response)
},
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
@@ -968,7 +971,7 @@ func TestServer_ExecutionTargetPreAccessToken(t *testing.T) {
{Key: "key", Value: []byte("value")},
},
}
return expectPreAccessTokenExecution(ctx, t, instance, req, response)
return expectPreAccessTokenExecution(ctx, t, instance, client.GetClientId(), req, response)
},
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
@@ -1005,7 +1008,7 @@ func TestServer_ExecutionTargetPreAccessToken(t *testing.T) {
{Key: "added3", Value: "value3"},
},
}
return expectPreAccessTokenExecution(ctx, t, instance, req, response)
return expectPreAccessTokenExecution(ctx, t, instance, client.GetClientId(), req, response)
},
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
@@ -1060,7 +1063,7 @@ func TestServer_ExecutionTargetPreAccessToken(t *testing.T) {
}
}
func expectPreAccessTokenExecution(ctx context.Context, t *testing.T, instance *integration.Instance, req *oidc_pb.CreateCallbackRequest, response *oidc_api.ContextInfoResponse) (string, func()) {
func expectPreAccessTokenExecution(ctx context.Context, t *testing.T, instance *integration.Instance, clientID string, req *oidc_pb.CreateCallbackRequest, response *oidc_api.ContextInfoResponse) (string, func()) {
userEmail := gofakeit.Email()
userPhone := "+41" + gofakeit.Phone()
userResp := instance.CreateHumanUserVerified(ctx, instance.DefaultOrg.Id, userEmail, userPhone)
@@ -1072,7 +1075,7 @@ func expectPreAccessTokenExecution(ctx context.Context, t *testing.T, instance *
SessionToken: sessionResp.GetSessionToken(),
},
}
expectedContextInfo := contextInfoForUserOIDC(instance, "function/preaccesstoken", userResp, userEmail, userPhone)
expectedContextInfo := contextInfoForUserOIDC(instance, "function/preaccesstoken", clientID, userResp, userEmail, userPhone)
targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response)

View File

@@ -206,7 +206,7 @@ func TestServer_GetTarget(t *testing.T) {
}
assert.NoError(ttt, err)
assert.EqualExportedValues(ttt, tt.want, got)
}, retryDuration, tick, "timeout waiting for expected target result")
}, retryDuration, tick, "timeout waiting for expected target Executions")
})
}
}
@@ -253,7 +253,7 @@ func TestServer_ListTargets(t *testing.T) {
TotalResult: 0,
AppliedLimit: 100,
},
Result: []*action.Target{},
Targets: []*action.Target{},
},
},
{
@@ -269,11 +269,11 @@ func TestServer_ListTargets(t *testing.T) {
},
}
response.Result[0].Id = resp.GetId()
response.Result[0].Name = name
response.Result[0].CreationDate = resp.GetCreationDate()
response.Result[0].ChangeDate = resp.GetCreationDate()
response.Result[0].SigningKey = resp.GetSigningKey()
response.Targets[0].Id = resp.GetId()
response.Targets[0].Name = name
response.Targets[0].CreationDate = resp.GetCreationDate()
response.Targets[0].ChangeDate = resp.GetCreationDate()
response.Targets[0].SigningKey = resp.GetSigningKey()
},
req: &action.ListTargetsRequest{
Filters: []*action.TargetSearchFilter{{}},
@@ -284,7 +284,7 @@ func TestServer_ListTargets(t *testing.T) {
TotalResult: 1,
AppliedLimit: 100,
},
Result: []*action.Target{
Targets: []*action.Target{
{
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
@@ -309,11 +309,11 @@ func TestServer_ListTargets(t *testing.T) {
},
}
response.Result[0].Id = resp.GetId()
response.Result[0].Name = name
response.Result[0].CreationDate = resp.GetCreationDate()
response.Result[0].ChangeDate = resp.GetCreationDate()
response.Result[0].SigningKey = resp.GetSigningKey()
response.Targets[0].Id = resp.GetId()
response.Targets[0].Name = name
response.Targets[0].CreationDate = resp.GetCreationDate()
response.Targets[0].ChangeDate = resp.GetCreationDate()
response.Targets[0].SigningKey = resp.GetSigningKey()
},
req: &action.ListTargetsRequest{
Filters: []*action.TargetSearchFilter{{}},
@@ -324,7 +324,7 @@ func TestServer_ListTargets(t *testing.T) {
TotalResult: 1,
AppliedLimit: 100,
},
Result: []*action.Target{
Targets: []*action.Target{
{
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
@@ -354,23 +354,23 @@ func TestServer_ListTargets(t *testing.T) {
},
}
response.Result[2].Id = resp1.GetId()
response.Result[2].Name = name1
response.Result[2].CreationDate = resp1.GetCreationDate()
response.Result[2].ChangeDate = resp1.GetCreationDate()
response.Result[2].SigningKey = resp1.GetSigningKey()
response.Targets[2].Id = resp1.GetId()
response.Targets[2].Name = name1
response.Targets[2].CreationDate = resp1.GetCreationDate()
response.Targets[2].ChangeDate = resp1.GetCreationDate()
response.Targets[2].SigningKey = resp1.GetSigningKey()
response.Result[1].Id = resp2.GetId()
response.Result[1].Name = name2
response.Result[1].CreationDate = resp2.GetCreationDate()
response.Result[1].ChangeDate = resp2.GetCreationDate()
response.Result[1].SigningKey = resp2.GetSigningKey()
response.Targets[1].Id = resp2.GetId()
response.Targets[1].Name = name2
response.Targets[1].CreationDate = resp2.GetCreationDate()
response.Targets[1].ChangeDate = resp2.GetCreationDate()
response.Targets[1].SigningKey = resp2.GetSigningKey()
response.Result[0].Id = resp3.GetId()
response.Result[0].Name = name3
response.Result[0].CreationDate = resp3.GetCreationDate()
response.Result[0].ChangeDate = resp3.GetCreationDate()
response.Result[0].SigningKey = resp3.GetSigningKey()
response.Targets[0].Id = resp3.GetId()
response.Targets[0].Name = name3
response.Targets[0].CreationDate = resp3.GetCreationDate()
response.Targets[0].ChangeDate = resp3.GetCreationDate()
response.Targets[0].SigningKey = resp3.GetSigningKey()
},
req: &action.ListTargetsRequest{
Filters: []*action.TargetSearchFilter{{}},
@@ -381,7 +381,7 @@ func TestServer_ListTargets(t *testing.T) {
TotalResult: 3,
AppliedLimit: 100,
},
Result: []*action.Target{
Targets: []*action.Target{
{
Endpoint: "https://example.com",
TargetType: &action.Target_RestAsync{
@@ -427,13 +427,13 @@ func TestServer_ListTargets(t *testing.T) {
require.NoError(ttt, listErr)
// always first check length, otherwise its failed anyway
if assert.Len(ttt, got.Result, len(tt.want.Result)) {
for i := range tt.want.Result {
assert.EqualExportedValues(ttt, tt.want.Result[i], got.Result[i])
if assert.Len(ttt, got.Targets, len(tt.want.Targets)) {
for i := range tt.want.Targets {
assert.EqualExportedValues(ttt, tt.want.Targets[i], got.Targets[i])
}
}
assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination)
}, retryDuration, tick, "timeout waiting for expected execution result")
}, retryDuration, tick, "timeout waiting for expected execution Executions")
})
}
}
@@ -476,9 +476,9 @@ func TestServer_ListExecutions(t *testing.T) {
resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()})
// Set expected response with used values for SetExecution
response.Result[0].CreationDate = resp.GetSetDate()
response.Result[0].ChangeDate = resp.GetSetDate()
response.Result[0].Condition = cond
response.Executions[0].CreationDate = resp.GetSetDate()
response.Executions[0].ChangeDate = resp.GetSetDate()
response.Executions[0].Condition = cond
},
req: &action.ListExecutionsRequest{
Filters: []*action.ExecutionSearchFilter{{
@@ -503,7 +503,7 @@ func TestServer_ListExecutions(t *testing.T) {
TotalResult: 1,
AppliedLimit: 100,
},
Result: []*action.Execution{
Executions: []*action.Execution{
{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
@@ -544,10 +544,10 @@ func TestServer_ListExecutions(t *testing.T) {
}
resp := instance.SetExecution(ctx, t, cond, []string{target.GetId()})
response.Result[0].CreationDate = resp.GetSetDate()
response.Result[0].ChangeDate = resp.GetSetDate()
response.Result[0].Condition = cond
response.Result[0].Targets = []string{target.GetId()}
response.Executions[0].CreationDate = resp.GetSetDate()
response.Executions[0].ChangeDate = resp.GetSetDate()
response.Executions[0].Condition = cond
response.Executions[0].Targets = []string{target.GetId()}
},
req: &action.ListExecutionsRequest{
Filters: []*action.ExecutionSearchFilter{{}},
@@ -558,7 +558,7 @@ func TestServer_ListExecutions(t *testing.T) {
TotalResult: 1,
AppliedLimit: 100,
},
Result: []*action.Execution{
Executions: []*action.Execution{
{
Condition: &action.Condition{},
Targets: []string{""},
@@ -604,7 +604,7 @@ func TestServer_ListExecutions(t *testing.T) {
cond1 := request.Filters[0].GetInConditionsFilter().GetConditions()[0]
resp1 := instance.SetExecution(ctx, t, cond1, []string{targetResp.GetId()})
response.Result[2] = &action.Execution{
response.Executions[2] = &action.Execution{
CreationDate: resp1.GetSetDate(),
ChangeDate: resp1.GetSetDate(),
Condition: cond1,
@@ -613,7 +613,7 @@ func TestServer_ListExecutions(t *testing.T) {
cond2 := request.Filters[0].GetInConditionsFilter().GetConditions()[1]
resp2 := instance.SetExecution(ctx, t, cond2, []string{targetResp.GetId()})
response.Result[1] = &action.Execution{
response.Executions[1] = &action.Execution{
CreationDate: resp2.GetSetDate(),
ChangeDate: resp2.GetSetDate(),
Condition: cond2,
@@ -622,7 +622,7 @@ func TestServer_ListExecutions(t *testing.T) {
cond3 := request.Filters[0].GetInConditionsFilter().GetConditions()[2]
resp3 := instance.SetExecution(ctx, t, cond3, []string{targetResp.GetId()})
response.Result[0] = &action.Execution{
response.Executions[0] = &action.Execution{
CreationDate: resp3.GetSetDate(),
ChangeDate: resp3.GetSetDate(),
Condition: cond3,
@@ -640,7 +640,7 @@ func TestServer_ListExecutions(t *testing.T) {
TotalResult: 3,
AppliedLimit: 100,
},
Result: []*action.Execution{
Executions: []*action.Execution{
{}, {}, {},
},
},
@@ -653,7 +653,7 @@ func TestServer_ListExecutions(t *testing.T) {
conditions := request.Filters[0].GetInConditionsFilter().GetConditions()
for i, cond := range conditions {
resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()})
response.Result[(len(conditions)-1)-i] = &action.Execution{
response.Executions[(len(conditions)-1)-i] = &action.Execution{
CreationDate: resp.GetSetDate(),
ChangeDate: resp.GetSetDate(),
Condition: cond,
@@ -687,7 +687,7 @@ func TestServer_ListExecutions(t *testing.T) {
TotalResult: 10,
AppliedLimit: 100,
},
Result: []*action.Execution{
Executions: []*action.Execution{
{},
{},
{},
@@ -709,7 +709,7 @@ func TestServer_ListExecutions(t *testing.T) {
conditions := request.Filters[0].GetInConditionsFilter().GetConditions()
for i, cond := range conditions {
resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()})
response.Result[i] = &action.Execution{
response.Executions[i] = &action.Execution{
CreationDate: resp.GetSetDate(),
ChangeDate: resp.GetSetDate(),
Condition: cond,
@@ -744,7 +744,7 @@ func TestServer_ListExecutions(t *testing.T) {
TotalResult: 10,
AppliedLimit: 100,
},
Result: []*action.Execution{
Executions: []*action.Execution{
{},
{},
{},
@@ -774,11 +774,11 @@ func TestServer_ListExecutions(t *testing.T) {
}
require.NoError(ttt, listErr)
// always first check length, otherwise its failed anyway
if assert.Len(ttt, got.Result, len(tt.want.Result)) {
assert.EqualExportedValues(ttt, got.Result, tt.want.Result)
if assert.Len(ttt, got.Executions, len(tt.want.Executions)) {
assert.EqualExportedValues(ttt, got.Executions, tt.want.Executions)
}
assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination)
}, retryDuration, tick, "timeout waiting for expected execution result")
}, retryDuration, tick, "timeout waiting for expected execution Executions")
})
}
}

View File

@@ -52,7 +52,7 @@ func (s *Server) ListTargets(ctx context.Context, req *connect.Request[action.Li
return nil, err
}
return connect.NewResponse(&action.ListTargetsResponse{
Result: targetsToPb(resp.Targets),
Targets: targetsToPb(resp.Targets),
Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse),
}), nil
}
@@ -67,7 +67,7 @@ func (s *Server) ListExecutions(ctx context.Context, req *connect.Request[action
return nil, err
}
return connect.NewResponse(&action.ListExecutionsResponse{
Result: executionsToPb(resp.Executions),
Executions: executionsToPb(resp.Executions),
Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse),
}), nil
}

View File

@@ -30,6 +30,7 @@ func (s *Server) ListInstanceDomains(ctx context.Context, req *admin_pb.ListInst
}
return &admin_pb.ListInstanceDomainsResponse{
Result: instance_grpc.DomainsToPb(domains.Domains),
SortingColumn: req.SortingColumn,
Details: object.ToListDetails(
domains.Count,
domains.Sequence,
@@ -49,6 +50,7 @@ func (s *Server) ListInstanceTrustedDomains(ctx context.Context, req *admin_pb.L
}
return &admin_pb.ListInstanceTrustedDomainsResponse{
Result: instance_grpc.TrustedDomainsToPb(domains.Domains),
SortingColumn: req.SortingColumn,
Details: object.ToListDetails(
domains.Count,
domains.Sequence,

View File

@@ -51,8 +51,23 @@ func ListInstanceTrustedDomainsRequestToModel(req *admin_pb.ListInstanceTrustedD
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: fieldNameToInstanceDomainColumn(req.SortingColumn),
SortingColumn: fieldNameToInstanceTrustedDomainColumn(req.SortingColumn),
},
Queries: queries,
}, nil
}
func fieldNameToInstanceTrustedDomainColumn(fieldName instance.DomainFieldName) query.Column {
switch fieldName {
case instance.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN:
return query.InstanceTrustedDomainDomainCol
case instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE:
return query.InstanceTrustedDomainCreationDateCol
case instance.DomainFieldName_DOMAIN_FIELD_NAME_UNSPECIFIED,
instance.DomainFieldName_DOMAIN_FIELD_NAME_PRIMARY,
instance.DomainFieldName_DOMAIN_FIELD_NAME_GENERATED:
return query.InstanceTrustedDomainCreationDateCol
default:
return query.Column{}
}
}

View File

@@ -9,7 +9,6 @@ import (
http_utils "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
@@ -104,17 +103,5 @@ func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (*
}
func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain string) ([]string, error) {
loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+orgDomain, query.TextEndsWithIgnoreCase)
if err != nil {
return nil, err
}
users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}}, nil)
if err != nil {
return nil, err
}
userIDs := make([]string, len(users.Users))
for i, user := range users.Users {
userIDs[i] = user.ID
}
return userIDs, nil
return s.query.SearchClaimedUserIDsOfOrgDomain(ctx, orgDomain, "")
}

View File

@@ -316,28 +316,7 @@ func (s *Server) RemoveOrgMember(ctx context.Context, req *mgmt_pb.RemoveOrgMemb
}
func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain, orgID string) ([]string, error) {
queries := make([]query.SearchQuery, 0, 2)
loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+orgDomain, query.TextEndsWithIgnoreCase)
if err != nil {
return nil, err
}
queries = append(queries, loginName)
if orgID != "" {
owner, err := query.NewUserResourceOwnerSearchQuery(orgID, query.TextNotEquals)
if err != nil {
return nil, err
}
queries = append(queries, owner)
}
users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: queries}, nil)
if err != nil {
return nil, err
}
userIDs := make([]string, len(users.Users))
for i, user := range users.Users {
userIDs[i] = user.ID
}
return userIDs, nil
return s.query.SearchClaimedUserIDsOfOrgDomain(ctx, orgDomain, orgID)
}
func (s *Server) ListOrgMetadata(ctx context.Context, req *mgmt_pb.ListOrgMetadataRequest) (*mgmt_pb.ListOrgMetadataResponse, error) {

View File

@@ -47,14 +47,17 @@ func TestMain(m *testing.M) {
func TestServer_CreateOrganization(t *testing.T) {
idpResp := Instance.AddGenericOAuthProvider(CTX, Instance.DefaultOrg.Id)
tests := []struct {
name string
ctx context.Context
req *v2beta_org.CreateOrganizationRequest
id string
want *v2beta_org.CreateOrganizationResponse
wantErr bool
}{
type test struct {
name string
ctx context.Context
req *v2beta_org.CreateOrganizationRequest
id string
testFunc func(ctx context.Context, t *testing.T)
want *v2beta_org.CreateOrganizationResponse
wantErr bool
}
tests := []test{
{
name: "missing permission",
ctx: Instance.WithAuthorization(CTX, integration.UserTypeOrgOwner),
@@ -73,6 +76,25 @@ func TestServer_CreateOrganization(t *testing.T) {
},
wantErr: true,
},
func() test {
orgName := gofakeit.Name()
return test{
name: "adding org with same name twice",
ctx: CTX,
req: &v2beta_org.CreateOrganizationRequest{
Name: orgName,
Admins: nil,
},
testFunc: func(ctx context.Context, t *testing.T) {
// create org initially
_, err := Client.CreateOrganization(ctx, &v2beta_org.CreateOrganizationRequest{
Name: orgName,
})
require.NoError(t, err)
},
wantErr: true,
}
}(),
{
name: "invalid admin type",
ctx: CTX,
@@ -208,11 +230,38 @@ func TestServer_CreateOrganization(t *testing.T) {
Name: gofakeit.AppName(),
Id: gu.Ptr("custom_id"),
},
want: &v2beta_org.CreateOrganizationResponse{},
want: &v2beta_org.CreateOrganizationResponse{
Id: "custom_id",
},
},
func() test {
orgID := gofakeit.Name()
return test{
name: "adding org with same ID twice",
ctx: CTX,
req: &v2beta_org.CreateOrganizationRequest{
Id: &orgID,
Name: gofakeit.Name(),
Admins: nil,
},
testFunc: func(ctx context.Context, t *testing.T) {
// create org initially
_, err := Client.CreateOrganization(ctx, &v2beta_org.CreateOrganizationRequest{
Id: &orgID,
Name: gofakeit.Name(),
})
require.NoError(t, err)
},
wantErr: true,
}
}(),
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.testFunc != nil {
tt.testFunc(tt.ctx, t)
}
got, err := Client.CreateOrganization(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
@@ -1044,6 +1093,70 @@ func TestServer_AddOrganizationDomain(t *testing.T) {
}
}
func TestServer_AddOrganizationDomain_ClaimDomain(t *testing.T) {
domain := gofakeit.DomainName()
// create an organization, ensure it has globally unique usernames
// and create a user with a loginname that matches the domain later on
organization, err := Client.CreateOrganization(CTX, &v2beta_org.CreateOrganizationRequest{
Name: gofakeit.AppName(),
})
require.NoError(t, err)
_, err = Instance.Client.Admin.AddCustomDomainPolicy(CTX, &admin.AddCustomDomainPolicyRequest{
OrgId: organization.GetId(),
UserLoginMustBeDomain: false,
})
require.NoError(t, err)
username := gofakeit.Username() + "@" + domain
ownUser := Instance.CreateHumanUserVerified(CTX, organization.GetId(), username, "")
// create another organization, ensure it has globally unique usernames
// and create a user with a loginname that matches the domain later on
otherOrg, err := Client.CreateOrganization(CTX, &v2beta_org.CreateOrganizationRequest{
Name: gofakeit.AppName(),
})
require.NoError(t, err)
_, err = Instance.Client.Admin.AddCustomDomainPolicy(CTX, &admin.AddCustomDomainPolicyRequest{
OrgId: otherOrg.GetId(),
UserLoginMustBeDomain: false,
})
require.NoError(t, err)
otherUsername := gofakeit.Username() + "@" + domain
otherUser := Instance.CreateHumanUserVerified(CTX, otherOrg.GetId(), otherUsername, "")
// if we add the domain now to the first organization, it should be claimed on the second organization, resp. its user(s)
_, err = Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{
OrganizationId: organization.GetId(),
Domain: domain,
})
require.NoError(t, err)
// check both users: the first one must be untouched, the second one must be updated
users, err := Instance.Client.UserV2.ListUsers(CTX, &user.ListUsersRequest{
Queries: []*user.SearchQuery{
{
Query: &user.SearchQuery_InUserIdsQuery{
InUserIdsQuery: &user.InUserIDQuery{UserIds: []string{ownUser.GetUserId(), otherUser.GetUserId()}},
},
},
},
})
require.NoError(t, err)
require.Len(t, users.GetResult(), 2)
for _, u := range users.GetResult() {
if u.GetUserId() == ownUser.GetUserId() {
assert.Equal(t, username, u.GetPreferredLoginName())
continue
}
if u.GetUserId() == otherUser.GetUserId() {
assert.NotEqual(t, otherUsername, u.GetPreferredLoginName())
assert.Contains(t, u.GetPreferredLoginName(), "@temporary.")
}
}
}
func TestServer_ListOrganizationDomains(t *testing.T) {
domain := gofakeit.URL()
tests := []struct {

View File

@@ -250,26 +250,5 @@ func createOrganizationRequestAdminToCommand(admin *v2beta_org.CreateOrganizatio
}
func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain, orgID string) ([]string, error) {
queries := make([]query.SearchQuery, 0, 2)
loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+orgDomain, query.TextEndsWithIgnoreCase)
if err != nil {
return nil, err
}
queries = append(queries, loginName)
if orgID != "" {
owner, err := query.NewUserResourceOwnerSearchQuery(orgID, query.TextNotEquals)
if err != nil {
return nil, err
}
queries = append(queries, owner)
}
users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: queries}, nil)
if err != nil {
return nil, err
}
userIDs := make([]string, len(users.Users))
for i, user := range users.Users {
userIDs[i] = user.ID
}
return userIDs, nil
return s.query.SearchClaimedUserIDsOfOrgDomain(ctx, orgDomain, orgID)
}

View File

@@ -10,30 +10,36 @@ import (
)
const (
Authorization = "authorization"
Accept = "accept"
AcceptLanguage = "accept-language"
CacheControl = "cache-control"
ContentType = "content-type"
ContentLength = "content-length"
ContentLocation = "content-location"
Expires = "expires"
Location = "location"
Origin = "origin"
Pragma = "pragma"
UserAgentHeader = "user-agent"
ForwardedFor = "x-forwarded-for"
ForwardedHost = "x-forwarded-host"
ForwardedProto = "x-forwarded-proto"
Forwarded = "forwarded"
ZitadelForwarded = "x-zitadel-forwarded"
XUserAgent = "x-user-agent"
XGrpcWeb = "x-grpc-web"
XRequestedWith = "x-requested-with"
XRobotsTag = "x-robots-tag"
IfNoneMatch = "If-None-Match"
LastModified = "Last-Modified"
Etag = "Etag"
Authorization = "authorization"
Accept = "accept"
AcceptLanguage = "accept-language"
CacheControl = "cache-control"
ContentType = "content-type"
ContentLength = "content-length"
ContentLocation = "content-location"
Expires = "expires"
Location = "location"
Origin = "origin"
Pragma = "pragma"
UserAgentHeader = "user-agent"
ForwardedFor = "x-forwarded-for"
ForwardedHost = "x-forwarded-host"
ForwardedProto = "x-forwarded-proto"
Forwarded = "forwarded"
ZitadelForwarded = "x-zitadel-forwarded"
XUserAgent = "x-user-agent"
XGrpcWeb = "x-grpc-web"
XRequestedWith = "x-requested-with"
XRobotsTag = "x-robots-tag"
IfNoneMatch = "if-none-match"
LastModified = "last-modified"
Etag = "etag"
GRPCTimeout = "grpc-timeout"
ConnectProtocolVersion = "connect-protocol-version"
ConnectTimeoutMS = "connect-timeout-ms"
GrpcStatus = "grpc-status"
GrpcMessage = "grpc-message"
GrpcStatusDetailsBin = "grpc-status-details-bin"
ContentSecurityPolicy = "content-security-policy"
XXSSProtection = "x-xss-protection"

View File

@@ -21,6 +21,9 @@ var (
http_utils.XUserAgent,
http_utils.XGrpcWeb,
http_utils.XRequestedWith,
http_utils.ConnectProtocolVersion,
http_utils.ConnectTimeoutMS,
http_utils.GRPCTimeout,
},
AllowedMethods: []string{
http.MethodOptions,
@@ -34,6 +37,9 @@ var (
ExposedHeaders: []string{
http_utils.Location,
http_utils.ContentLength,
http_utils.GrpcStatus,
http_utils.GrpcMessage,
http_utils.GrpcStatusDetailsBin,
},
AllowOriginFunc: func(_ string) bool {
return true

View File

@@ -13,6 +13,7 @@ import (
"github.com/zitadel/logging"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/authz"
http_utils "github.com/zitadel/zitadel/internal/api/http"
@@ -30,6 +31,8 @@ import (
const (
LoginClientHeader = "x-zitadel-login-client"
LoginPostLogoutRedirectParam = "post_logout_redirect"
LoginLogoutHintParam = "logout_hint"
LoginUILocalesParam = "ui_locales"
LoginPath = "/login"
LogoutPath = "/logout"
LogoutDonePath = "/logout/done"
@@ -283,14 +286,19 @@ func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionR
// we'll redirect to the UI (V2) and let it decide which session to terminate
//
// If there's no id_token_hint and for v1 logins, we handle them separately
if endSessionRequest.IDTokenHintClaims == nil &&
(authz.GetFeatures(ctx).LoginV2.Required || headers.Get(LoginClientHeader) != "") {
if endSessionRequest.IDTokenHintClaims == nil && (authz.GetFeatures(ctx).LoginV2.Required || headers.Get(LoginClientHeader) != "") {
redirectURI := v2PostLogoutRedirectURI(endSessionRequest.RedirectURI)
// if no base uri is set, fallback to the default configured in the runtime config
if authz.GetFeatures(ctx).LoginV2.BaseURI == nil || authz.GetFeatures(ctx).LoginV2.BaseURI.String() == "" {
return o.defaultLogoutURLV2 + redirectURI, nil
logoutURI := authz.GetFeatures(ctx).LoginV2.BaseURI
// if no logout uri is set, fallback to the default configured in the runtime config
if logoutURI == nil || logoutURI.String() == "" {
logoutURI, err = url.Parse(o.defaultLogoutURLV2)
if err != nil {
return "", err
}
} else {
logoutURI = logoutURI.JoinPath(LogoutPath)
}
return buildLoginV2LogoutURL(authz.GetFeatures(ctx).LoginV2.BaseURI, redirectURI), nil
return buildLoginV2LogoutURL(logoutURI, redirectURI, endSessionRequest.LogoutHint, endSessionRequest.UILocales), nil
}
// V1:
@@ -367,12 +375,25 @@ func (o *OPStorage) federatedLogout(ctx context.Context, sessionID string, postL
return login.ExternalLogoutPath(sessionID)
}
func buildLoginV2LogoutURL(baseURI *url.URL, redirectURI string) string {
baseURI.JoinPath(LogoutPath)
q := baseURI.Query()
func buildLoginV2LogoutURL(logoutURI *url.URL, redirectURI, logoutHint string, uiLocales []language.Tag) string {
if strings.HasSuffix(logoutURI.Path, "/") && len(logoutURI.Path) > 1 {
logoutURI.Path = strings.TrimSuffix(logoutURI.Path, "/")
}
q := logoutURI.Query()
q.Set(LoginPostLogoutRedirectParam, redirectURI)
baseURI.RawQuery = q.Encode()
return baseURI.String()
if logoutHint != "" {
q.Set(LoginLogoutHintParam, logoutHint)
}
if len(uiLocales) > 0 {
locales := make([]string, len(uiLocales))
for i, locale := range uiLocales {
locales[i] = locale.String()
}
q.Set(LoginUILocalesParam, strings.Join(locales, " "))
}
logoutURI.RawQuery = q.Encode()
return logoutURI.String()
}
// v2PostLogoutRedirectURI will take care that the post_logout_redirect_uri is correctly set for v2 logins.

View File

@@ -0,0 +1,98 @@
package oidc
import (
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
)
func TestBuildLoginV2LogoutURL(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
logoutURIStr string
redirectURI string
logoutHint string
uiLocales []language.Tag
expectedParams map[string]string
}{
{
testName: "basic with only redirectURI",
logoutURIStr: "https://example.com/logout",
redirectURI: "https://client/cb",
expectedParams: map[string]string{
"post_logout_redirect": "https://client/cb",
},
},
{
testName: "with logout hint",
logoutURIStr: "https://example.com/logout",
redirectURI: "https://client/cb",
logoutHint: "user@example.com",
expectedParams: map[string]string{
"post_logout_redirect": "https://client/cb",
"logout_hint": "user@example.com",
},
},
{
testName: "with ui_locales",
logoutURIStr: "https://example.com/logout",
redirectURI: "https://client/cb",
uiLocales: []language.Tag{language.English, language.Italian},
expectedParams: map[string]string{
"post_logout_redirect": "https://client/cb",
"ui_locales": "en it",
},
},
{
testName: "with all params",
logoutURIStr: "https://example.com/logout",
redirectURI: "https://client/cb",
logoutHint: "logoutme",
uiLocales: []language.Tag{language.Make("de-CH"), language.Make("fr")},
expectedParams: map[string]string{
"post_logout_redirect": "https://client/cb",
"logout_hint": "logoutme",
"ui_locales": "de-CH fr",
},
},
{
testName: "base with trailing slash",
logoutURIStr: "https://example.com/logout/",
redirectURI: "https://client/cb",
expectedParams: map[string]string{
"post_logout_redirect": "https://client/cb",
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
// t.Parallel()
// Given
logoutURI, err := url.Parse(tc.logoutURIStr)
require.NoError(t, err)
// When
got := buildLoginV2LogoutURL(logoutURI, tc.redirectURI, tc.logoutHint, tc.uiLocales)
// Then
gotURL, err := url.Parse(got)
require.NoError(t, err)
require.NotContains(t, gotURL.String(), "/logout/")
q := gotURL.Query()
// Ensure no unexpected params
require.Len(t, q, len(tc.expectedParams))
for k, v := range tc.expectedParams {
assert.Equal(t, v, q.Get(k))
}
})
}
}

View File

@@ -498,7 +498,7 @@ func TestOPStorage_TerminateSession(t *testing.T) {
_, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
require.NoError(t, err)
postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state")
postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state", "", nil)
require.NoError(t, err)
assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String())
@@ -535,7 +535,7 @@ func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) {
_, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
require.NoError(t, err)
postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state")
postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state", "", nil)
require.NoError(t, err)
assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String())
@@ -551,6 +551,17 @@ func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) {
require.NoError(t, err)
}
func buildLogoutURL(origin, logoutURLV2 string, redirectURI string, extraParams map[string]string) string {
u, _ := url.Parse(origin + logoutURLV2 + redirectURI)
q := u.Query()
for k, v := range extraParams {
q.Set(k, v)
}
u.RawQuery = q.Encode()
// Append the redirect URI as a URL-escaped string
return u.String()
}
func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
tests := []struct {
name string
@@ -565,7 +576,7 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
return clientID
}(),
authRequestID: createAuthRequest,
logoutURL: http_utils.BuildOrigin(Instance.Host(), Instance.Config.Secure) + Instance.Config.LogoutURLV2 + logoutRedirectURI + "?state=state",
logoutURL: buildLogoutURL(http_utils.BuildOrigin(Instance.Host(), Instance.Config.Secure), Instance.Config.LogoutURLV2, logoutRedirectURI+"?state=state", map[string]string{"logout_hint": "hint", "ui_locales": "it-IT en-US"}),
},
{
name: "login v2 config",
@@ -574,7 +585,7 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
return clientID
}(),
authRequestID: createAuthRequestNoLoginClientHeader,
logoutURL: http_utils.BuildOrigin(Instance.Host(), Instance.Config.Secure) + Instance.Config.LogoutURLV2 + logoutRedirectURI + "?state=state",
logoutURL: buildLogoutURL(http_utils.BuildOrigin(Instance.Host(), Instance.Config.Secure), Instance.Config.LogoutURLV2, logoutRedirectURI+"?state=state", map[string]string{"logout_hint": "hint", "ui_locales": "it-IT en-US"}),
},
}
for _, tt := range tests {
@@ -601,7 +612,7 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
assertTokens(t, tokens, false)
assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
postLogoutRedirect, err := rp.EndSession(CTX, provider, "", logoutRedirectURI, "state")
postLogoutRedirect, err := rp.EndSession(CTX, provider, "", logoutRedirectURI, "state", "hint", oidc.ParseLocales([]string{"it-IT", "en-US"}))
require.NoError(t, err)
assert.Equal(t, tt.logoutURL, postLogoutRedirect.String())

View File

@@ -311,7 +311,7 @@ func Test_ZITADEL_API_terminated_session(t *testing.T) {
require.Equal(t, User.GetUserId(), myUserResp.GetUser().GetId())
// end session
postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state")
postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state", "", nil)
require.NoError(t, err)
assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String())

View File

@@ -100,6 +100,7 @@ func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionR
token.userID,
token.scope,
client.projectID,
client.clientID,
client.projectRoleAssertion,
true,
true,

View File

@@ -31,7 +31,7 @@ for example the v2 code exchange and refresh token.
*/
func (s *Server) accessTokenResponseFromSession(ctx context.Context, client op.Client, session *command.OIDCSession, state, projectID string, projectRoleAssertion, accessTokenRoleAssertion, idTokenRoleAssertion, userInfoAssertion bool) (_ *oidc.AccessTokenResponse, err error) {
getUserInfo := s.getUserInfo(session.UserID, projectID, projectRoleAssertion, userInfoAssertion, session.Scope)
getUserInfo := s.getUserInfo(session.UserID, projectID, client.GetID(), projectRoleAssertion, userInfoAssertion, session.Scope)
getSigner := s.getSignerOnce()
resp := &oidc.AccessTokenResponse{
@@ -113,8 +113,8 @@ type userInfoFunc func(ctx context.Context, roleAssertion bool, triggerType doma
// getUserInfo returns a function which retrieves userinfo from the database once.
// However, each time, role claims are asserted and also action flows will trigger.
func (s *Server) getUserInfo(userID, projectID string, projectRoleAssertion, userInfoAssertion bool, scope []string) userInfoFunc {
userInfo := s.userInfo(userID, scope, projectID, projectRoleAssertion, userInfoAssertion, false)
func (s *Server) getUserInfo(userID, projectID, clientID string, projectRoleAssertion, userInfoAssertion bool, scope []string) userInfoFunc {
userInfo := s.userInfo(userID, scope, projectID, clientID, projectRoleAssertion, userInfoAssertion, false)
return func(ctx context.Context, roleAssertion bool, triggerType domain.TriggerType) (*oidc.UserInfo, error) {
return userInfo(ctx, roleAssertion, triggerType)
}

View File

@@ -218,7 +218,7 @@ func validateTokenExchangeAudience(requestedAudience, subjectAudience, actorAudi
// Both tokens may point to the same object (subjectToken) in case of a regular Token Exchange.
// When the subject and actor Tokens point to different objects, the new tokens will be for impersonation / delegation.
func (s *Server) createExchangeTokens(ctx context.Context, tokenType oidc.TokenType, client *Client, subjectToken, actorToken *exchangeToken, audience, scopes []string) (_ *oidc.TokenExchangeResponse, err error) {
getUserInfo := s.getUserInfo(subjectToken.userID, client.client.ProjectID, client.client.ProjectRoleAssertion, client.IDTokenUserinfoClaimsAssertion(), scopes)
getUserInfo := s.getUserInfo(subjectToken.userID, client.client.ProjectID, client.GetID(), client.client.ProjectRoleAssertion, client.IDTokenUserinfoClaimsAssertion(), scopes)
getSigner := s.getSignerOnce()
resp := &oidc.TokenExchangeResponse{

View File

@@ -54,6 +54,7 @@ func (s *Server) UserInfo(ctx context.Context, r *op.Request[oidc.UserInfoReques
token.userID,
token.scope,
projectID,
token.clientID,
assertion,
true,
false,
@@ -86,6 +87,7 @@ func (s *Server) userInfo(
userID string,
scope []string,
projectID string,
clientID string,
projectRoleAssertion, userInfoAssertion, currentProjectOnly bool,
) func(ctx context.Context, roleAssertion bool, triggerType domain.TriggerType) (_ *oidc.UserInfo, err error) {
var (
@@ -120,7 +122,7 @@ func (s *Server) userInfo(
Claims: maps.Clone(rawUserInfo.Claims),
}
assertRoles(projectID, qu, roleAudience, requestedRoles, roleAssertion, userInfo)
return userInfo, s.userinfoFlows(ctx, qu, userInfo, triggerType)
return userInfo, s.userinfoFlows(ctx, qu, userInfo, triggerType, clientID)
}
}
@@ -285,7 +287,8 @@ func setUserInfoRoleClaims(userInfo *oidc.UserInfo, roles *projectsRoles) {
}
}
func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, userInfo *oidc.UserInfo, triggerType domain.TriggerType) (err error) {
//nolint:gocognit
func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, userInfo *oidc.UserInfo, triggerType domain.TriggerType, clientID string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -319,6 +322,13 @@ func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, user
}
}),
),
actions.SetFields("application",
actions.SetFields("getClientId", func(c *actions.FieldConfig) interface{} {
return func(goja.FunctionCall) goja.Value {
return c.Runtime.ToValue(clientID)
}
}),
),
),
)
@@ -427,6 +437,7 @@ func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, user
User: qu.User,
UserMetadata: qu.Metadata,
Org: qu.Org,
Application: &ContextInfoApplication{ClientID: clientID},
UserGrants: qu.UserGrants,
}
@@ -463,13 +474,17 @@ func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, user
}
type ContextInfo struct {
Function string `json:"function,omitempty"`
UserInfo *oidc.UserInfo `json:"userinfo,omitempty"`
User *query.User `json:"user,omitempty"`
UserMetadata []query.UserMetadata `json:"user_metadata,omitempty"`
Org *query.UserInfoOrg `json:"org,omitempty"`
UserGrants []query.UserGrant `json:"user_grants,omitempty"`
Response *ContextInfoResponse `json:"response,omitempty"`
Function string `json:"function,omitempty"`
UserInfo *oidc.UserInfo `json:"userinfo,omitempty"`
User *query.User `json:"user,omitempty"`
UserMetadata []query.UserMetadata `json:"user_metadata,omitempty"`
Org *query.UserInfoOrg `json:"org,omitempty"`
UserGrants []query.UserGrant `json:"user_grants,omitempty"`
Application *ContextInfoApplication `json:"application,omitempty"`
Response *ContextInfoResponse `json:"response,omitempty"`
}
type ContextInfoApplication struct {
ClientID string `json:"client_id,omitempty"`
}
type ContextInfoResponse struct {

View File

@@ -432,3 +432,78 @@ func TestCreateUser_scopedExternalID(t *testing.T) {
assert.Equal(tt, "701984", string(md.Metadata.Value))
}, retryDuration, tick)
}
func TestCreateUser_ignorePasswordOnCreate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
ignorePassword string
scimErrorType string
scimErrorDetail string
wantUser *resources.ScimUser
wantErr bool
}{
{
name: "ignorePasswordOnCreate set to false",
ignorePassword: "false",
wantErr: true,
scimErrorType: "invalidValue",
scimErrorDetail: "Password is too short",
},
{
name: "ignorePasswordOnCreate set to an invalid value",
ignorePassword: "random",
wantErr: true,
scimErrorType: "invalidValue",
scimErrorDetail: "Invalid value for metadata key urn:zitadel:scim:ignorePasswordOnCreate: random",
},
{
name: "ignorePasswordOnCreate set to true",
ignorePassword: "true",
wantUser: &resources.ScimUser{
UserName: "acmeUser1",
Name: &resources.ScimUserName{
FamilyName: "Ross",
GivenName: "Bethany",
},
Emails: []*resources.ScimEmail{
{
Value: "user1@example.com",
Primary: true,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// create a machine user
callingUserId, callingUserPat, err := Instance.CreateMachineUserPATWithMembership(CTX, "ORG_OWNER")
require.NoError(t, err)
ctx := integration.WithAuthorizationToken(CTX, callingUserPat)
// set urn:zitadel:scim:ignorePasswordOnCreate metadata for the machine user
setAndEnsureMetadata(t, callingUserId, "urn:zitadel:scim:ignorePasswordOnCreate", tt.ignorePassword)
// create a user with an invalid password
createdUser, err := Instance.Client.SCIM.Users.Create(ctx, Instance.DefaultOrg.Id, withUsername(invalidPasswordUserJson, "acmeUser1"))
require.Equal(t, tt.wantErr, err != nil)
if err != nil {
scimErr := scim.RequireScimError(t, http.StatusBadRequest, err)
assert.Equal(t, tt.scimErrorType, scimErr.Error.ScimType)
assert.Equal(t, tt.scimErrorDetail, scimErr.Error.Detail)
return
}
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
// ensure the user is really stored and not just returned to the caller
fetchedUser, err := Instance.Client.SCIM.Users.Get(CTX, Instance.DefaultOrg.Id, createdUser.ID)
require.NoError(ttt, err)
assert.True(ttt, test.PartiallyDeepEqual(tt.wantUser, fetchedUser))
}, retryDuration, tick)
})
}
}

View File

@@ -15,6 +15,7 @@ var scimContextKey scimContextKeyType
type ScimContextData struct {
ProvisioningDomain string
IgnorePasswordOnCreate bool
ExternalIDScopedMetadataKey ScopedKey
bulkIDMapping map[string]string
}

View File

@@ -13,8 +13,9 @@ type ScopedKey string
const (
externalIdProvisioningDomainPlaceholder = "{provisioningDomain}"
KeyPrefix = "urn:zitadel:scim:"
KeyProvisioningDomain Key = KeyPrefix + "provisioningDomain"
KeyPrefix = "urn:zitadel:scim:"
KeyProvisioningDomain Key = KeyPrefix + "provisioningDomain"
KeyIgnorePasswordOnCreate Key = KeyPrefix + "ignorePasswordOnCreate"
KeyExternalId Key = KeyPrefix + "externalId"
keyScopedExternalIdTemplate = KeyPrefix + externalIdProvisioningDomainPlaceholder + ":externalId"

View File

@@ -3,10 +3,15 @@ package middleware
import (
"context"
"net/http"
"strconv"
"strings"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
zhttp "github.com/zitadel/zitadel/internal/api/http/middleware"
smetadata "github.com/zitadel/zitadel/internal/api/scim/metadata"
sresources "github.com/zitadel/zitadel/internal/api/scim/resources"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -29,22 +34,43 @@ func initScimContext(ctx context.Context, q *query.Queries) (context.Context, er
ctx = smetadata.SetScimContextData(ctx, data)
userID := authz.GetCtxData(ctx).UserID
metadata, err := q.GetUserMetadataByKey(ctx, false, userID, string(smetadata.KeyProvisioningDomain), false)
// get the provisioningDomain and ignorePasswordOnCreate metadata keys associated with the service user
metadataKeys := []smetadata.Key{
smetadata.KeyProvisioningDomain,
smetadata.KeyIgnorePasswordOnCreate,
}
queries := sresources.BuildMetadataQueries(ctx, metadataKeys)
metadataList, err := q.SearchUserMetadata(ctx, false, userID, queries, nil)
if err != nil {
if zerrors.IsNotFound(err) {
return ctx, nil
}
return ctx, err
}
if metadata == nil {
if metadataList == nil || len(metadataList.Metadata) == 0 {
return ctx, nil
}
data.ProvisioningDomain = string(metadata.Value)
if data.ProvisioningDomain != "" {
data.ExternalIDScopedMetadataKey = smetadata.ScopeExternalIdKey(data.ProvisioningDomain)
for _, metadata := range metadataList.Metadata {
switch metadata.Key {
case string(smetadata.KeyProvisioningDomain):
data.ProvisioningDomain = string(metadata.Value)
if data.ProvisioningDomain != "" {
data.ExternalIDScopedMetadataKey = smetadata.ScopeExternalIdKey(data.ProvisioningDomain)
}
case string(smetadata.KeyIgnorePasswordOnCreate):
ignorePasswordOnCreate, parseErr := strconv.ParseBool(strings.TrimSpace(string(metadata.Value)))
if parseErr != nil {
return ctx,
zerrors.ThrowInvalidArgumentf(nil, "SMCM-yvw2rt", "Invalid value for metadata key %s: %s", smetadata.KeyIgnorePasswordOnCreate, metadata.Value)
}
data.IgnorePasswordOnCreate = ignorePasswordOnCreate
default:
logging.WithFields("user_metadata_key", metadata.Key).Warn("unexpected metadata key")
}
}
return smetadata.SetScimContextData(ctx, data), nil
}

View File

@@ -45,7 +45,12 @@ func (h *UsersHandler) mapToAddHuman(ctx context.Context, scimUser *ScimUser) (*
}
human.Metadata = md
if scimUser.Password != nil {
// Okta sends a random password during SCIM provisioning
// irrespective of whether the Sync Password option is enabled or disabled on Okta.
// This password does not comply with Zitadel's password complexity, and
// the following workaround ignores the random password as it does not add any value.
ignorePasswordOnCreate := metadata.GetScimContextData(ctx).IgnorePasswordOnCreate
if scimUser.Password != nil && !ignorePasswordOnCreate {
human.Password = scimUser.Password.String()
scimUser.Password = nil
}

View File

@@ -21,7 +21,7 @@ import (
)
func (h *UsersHandler) queryMetadataForUsers(ctx context.Context, userIds []string) (map[string]map[metadata.ScopedKey][]byte, error) {
queries := h.buildMetadataQueries(ctx)
queries := BuildMetadataQueries(ctx, metadata.ScimUserRelevantMetadataKeys)
md, err := h.query.SearchUserMetadataForUsers(ctx, false, userIds, queries)
if err != nil {
@@ -43,7 +43,7 @@ func (h *UsersHandler) queryMetadataForUsers(ctx context.Context, userIds []stri
}
func (h *UsersHandler) queryMetadataForUser(ctx context.Context, id string) (map[metadata.ScopedKey][]byte, error) {
queries := h.buildMetadataQueries(ctx)
queries := BuildMetadataQueries(ctx, metadata.ScimUserRelevantMetadataKeys)
md, err := h.query.SearchUserMetadata(ctx, false, id, queries, nil)
if err != nil {
@@ -53,9 +53,9 @@ func (h *UsersHandler) queryMetadataForUser(ctx context.Context, id string) (map
return metadata.MapListToScopedKeyMap(md.Metadata), nil
}
func (h *UsersHandler) buildMetadataQueries(ctx context.Context) *query.UserMetadataSearchQueries {
keyQueries := make([]query.SearchQuery, len(metadata.ScimUserRelevantMetadataKeys))
for i, key := range metadata.ScimUserRelevantMetadataKeys {
func BuildMetadataQueries(ctx context.Context, metadataKeys []metadata.Key) *query.UserMetadataSearchQueries {
keyQueries := make([]query.SearchQuery, len(metadataKeys))
for i, key := range metadataKeys {
keyQueries[i] = buildMetadataKeyQuery(ctx, key)
}

View File

@@ -523,7 +523,7 @@ func (l *Login) handleExternalUserAuthenticated(
// The decision, which information will be checked is based on the IdP template option.
// The function returns a boolean whether a user was found or not.
// If single a user was found, it will be automatically linked.
func (l *Login) checkAutoLinking(r *http.Request, authReq *domain.AuthRequest, provider *query.IDPTemplate, externalUser *domain.ExternalUser) (bool, error) {
func (l *Login) checkAutoLinking(r *http.Request, authReq *domain.AuthRequest, provider *query.IDPTemplate, externalUser *domain.ExternalUser, human *domain.Human) (bool, error) {
queries := make([]query.SearchQuery, 0, 2)
switch provider.AutoLinking {
case domain.AutoLinkingOptionUnspecified:
@@ -532,7 +532,7 @@ func (l *Login) checkAutoLinking(r *http.Request, authReq *domain.AuthRequest, p
case domain.AutoLinkingOptionUsername:
// if we're checking for usernames there are to options:
//
// If no specific org has been requested (by id or domain scope), we'll check the provided username against
// If no specific org has been requested (by id or domain scope), we'll check the provided username (loginname) against
// all existing loginnames and directly use that result to either prompt or continue with other idp options.
if authReq.RequestedOrgID == "" {
user, err := l.query.GetNotifyUserByLoginName(r.Context(), false, externalUser.PreferredUsername)
@@ -544,8 +544,9 @@ func (l *Login) checkAutoLinking(r *http.Request, authReq *domain.AuthRequest, p
}
return true, nil
}
// If a specific org has been requested, we'll check the provided username against usernames (of that org).
usernameQuery, err := query.NewUserUsernameSearchQuery(externalUser.PreferredUsername, query.TextEqualsIgnoreCase)
// If a specific org has been requested, we'll check the username (org policy (suffixed or not) is already applied)
// against usernames (of that org).
usernameQuery, err := query.NewUserUsernameSearchQuery(human.Username, query.TextEqualsIgnoreCase)
if err != nil {
return false, nil
}
@@ -605,7 +606,7 @@ func (l *Login) createOrLinkUser(w http.ResponseWriter, r *http.Request, authReq
human, idpLink, _ := mapExternalUserToLoginUser(externalUser, orgIAMPolicy.UserLoginMustBeDomain)
// let's check if auto-linking is enabled and if the user would be found by the corresponding option
if provider.AutoLinking != domain.AutoLinkingOptionUnspecified {
userLinked, err = l.checkAutoLinking(r, authReq, provider, externalUser)
userLinked, err = l.checkAutoLinking(r, authReq, provider, externalUser, human)
if err != nil {
l.renderError(w, r, authReq, err)
return false

View File

@@ -178,19 +178,7 @@ func (l *Login) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgName string
if err != nil {
return nil, err
}
loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+orgDomain, query.TextEndsWithIgnoreCase)
if err != nil {
return nil, err
}
users, err := l.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}}, nil)
if err != nil {
return nil, err
}
userIDs := make([]string, len(users.Users))
for i, user := range users.Users {
userIDs[i] = user.ID
}
return userIDs, nil
return l.query.SearchClaimedUserIDsOfOrgDomain(ctx, orgDomain, "")
}
func setContext(ctx context.Context, resourceOwner string) context.Context {

View File

@@ -17,7 +17,11 @@ func (l *Login) handlePasswordReset(w http.ResponseWriter, r *http.Request) {
l.renderError(w, r, authReq, err)
return
}
user, err := l.query.GetUserByLoginName(setContext(r.Context(), authReq.UserOrgID), true, authReq.LoginName)
// We check if the user really exists or if it is just a placeholder or an unknown user.
// In theory, we could also check for the unknownUserID constant. However, that could disclose
// information about the existence of a user to an attacker if they check response times,
// since those requests would take shorter than the ones for real users.
user, err := l.query.GetUserByID(setContext(r.Context(), authReq.UserOrgID), true, authReq.UserID)
if err != nil {
if authReq.LoginPolicy.IgnoreUnknownUsernames && zerrors.IsNotFound(err) {
err = nil

View File

@@ -262,6 +262,7 @@ RegistrationUser:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
GenderLabel: Пол
Female: Женски пол
Male: Мъжки
@@ -305,6 +306,7 @@ ExternalRegistrationUserOverview:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
TosAndPrivacyLabel: Правила и условия
TosConfirm: Приемам
TosLinkText: TOS
@@ -377,6 +379,7 @@ ExternalNotFound:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
DeviceAuth:
Title: Упълномощаване на устройството
UserCode:

View File

@@ -266,6 +266,7 @@ RegistrationUser:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
GenderLabel: Pohlaví
Female: Žena
Male: Muž
@@ -310,6 +311,7 @@ ExternalRegistrationUserOverview:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
TosAndPrivacyLabel: Obchodní podmínky
TosConfirm: Souhlasím s
TosLinkText: obchodními podmínkami
@@ -388,6 +390,7 @@ ExternalNotFound:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
DeviceAuth:
Title: Autorizace zařízení
UserCode:

View File

@@ -265,6 +265,7 @@ RegistrationUser:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
GenderLabel: Geschlecht
Female: weiblich
Male: männlich
@@ -309,6 +310,7 @@ ExternalRegistrationUserOverview:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
TosAndPrivacyLabel: Allgemeine Geschäftsbedingungen und Datenschutz
TosConfirm: Ich akzeptiere die
TosLinkText: AGB
@@ -387,6 +389,7 @@ ExternalNotFound:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
DeviceAuth:
Title: Gerät verbinden
UserCode:

View File

@@ -266,6 +266,7 @@ RegistrationUser:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
GenderLabel: Gender
Female: Female
Male: Male
@@ -310,6 +311,7 @@ ExternalRegistrationUserOverview:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
TosAndPrivacyLabel: Terms and conditions
TosConfirm: I accept the
TosLinkText: TOS
@@ -388,6 +390,7 @@ ExternalNotFound:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
DeviceAuth:
Title: Device Authorization
UserCode:

View File

@@ -266,6 +266,7 @@ RegistrationUser:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
GenderLabel: Género
Female: Mujer
Male: Hombre
@@ -310,6 +311,7 @@ ExternalRegistrationUserOverview:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
TosAndPrivacyLabel: Términos y condiciones
TosConfirm: Acepto los
TosLinkText: TDS
@@ -388,6 +390,7 @@ ExternalNotFound:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
Footer:
PoweredBy: Powered By

View File

@@ -266,6 +266,7 @@ RegistrationUser:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
GenderLabel: Genre
Female: Femme
Male: Homme
@@ -310,6 +311,7 @@ ExternalRegistrationUserOverview:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
TosAndPrivacyLabel: Termes et conditions
TosConfirm: J'accepte les
TosLinkText: TOS
@@ -388,6 +390,7 @@ ExternalNotFound:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
DeviceAuth:
Title: Autorisation de l'appareil

View File

@@ -236,6 +236,7 @@ RegistrationUser:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
GenderLabel: Nem
Female:
Male: Férfi
@@ -279,6 +280,7 @@ ExternalRegistrationUserOverview:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
TosAndPrivacyLabel: Felhasználási feltételek
TosConfirm: Elfogadom a
TosLinkText: TOS
@@ -351,6 +353,7 @@ ExternalNotFound:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
DeviceAuth:
Title: Eszköz engedélyezése
UserCode:

View File

@@ -236,6 +236,7 @@ RegistrationUser:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
GenderLabel: Jenis kelamin
Female: Perempuan
Male: Pria
@@ -279,6 +280,7 @@ ExternalRegistrationUserOverview:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
TosAndPrivacyLabel: Syarat dan Ketentuan
TosConfirm: Saya menerima itu
TosLinkText: KL
@@ -351,6 +353,7 @@ ExternalNotFound:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
DeviceAuth:
Title: Otorisasi Perangkat
UserCode:

View File

@@ -266,6 +266,7 @@ RegistrationUser:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
GenderLabel: Genere
Female: Femminile
Male: Maschile
@@ -310,6 +311,7 @@ ExternalRegistrationUserOverview:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
TosAndPrivacyLabel: Termini di servizio
TosConfirm: Accetto i
TosLinkText: Termini di servizio
@@ -388,6 +390,7 @@ ExternalNotFound:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
DeviceAuth:
Title: Autorizzazione del dispositivo

View File

@@ -266,6 +266,7 @@ RegistrationUser:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
GenderLabel: 性別
Female: 女性
Male: 男性
@@ -310,6 +311,7 @@ ExternalRegistrationUserOverview:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
TosAndPrivacyLabel: 利用規約
TosConfirm: 私は利用規約を承諾します。
TosLinkText: TOS
@@ -388,6 +390,7 @@ ExternalNotFound:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
DeviceAuth:
Title: デバイス認証

View File

@@ -266,6 +266,7 @@ RegistrationUser:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
GenderLabel: 성별
Female: 여성
Male: 남성
@@ -310,6 +311,7 @@ ExternalRegistrationUserOverview:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
TosAndPrivacyLabel: 동의사항
TosConfirm: 이용 약관에 동의합니다.
TosLinkText: 이용 약관
@@ -388,6 +390,7 @@ ExternalNotFound:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
DeviceAuth:
Title: 기기 인증
UserCode:

View File

@@ -266,6 +266,7 @@ RegistrationUser:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
GenderLabel: Пол
Female: Женски
Male: Машки
@@ -310,6 +311,7 @@ ExternalRegistrationUserOverview:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
TosAndPrivacyLabel: Правила и услови
TosConfirm: Се согласувам со
TosLinkText: правилата за користење
@@ -388,6 +390,7 @@ ExternalNotFound:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
DeviceAuth:
Title: Овластување преку уред

View File

@@ -266,6 +266,7 @@ RegistrationUser:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
GenderLabel: Geslacht
Female: Vrouw
Male: Man
@@ -310,6 +311,7 @@ ExternalRegistrationUserOverview:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
TosAndPrivacyLabel: Algemene voorwaarden
TosConfirm: Ik accepteer de
TosLinkText: AV
@@ -388,6 +390,7 @@ ExternalNotFound:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
DeviceAuth:
Title: Apparaat Autorisatie
UserCode:

View File

@@ -266,6 +266,7 @@ RegistrationUser:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
GenderLabel: Płeć
Female: Kobieta
Male: Mężczyzna
@@ -310,6 +311,7 @@ ExternalRegistrationUserOverview:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
TosAndPrivacyLabel: Warunki i zasady
TosConfirm: Akceptuję
TosLinkText: Warunki korzystania
@@ -388,6 +390,7 @@ ExternalNotFound:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
DeviceAuth:
Title: Autoryzacja urządzenia

View File

@@ -262,6 +262,7 @@ RegistrationUser:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
GenderLabel: Gênero
Female: Feminino
Male: Masculino
@@ -306,6 +307,7 @@ ExternalRegistrationUserOverview:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
TosAndPrivacyLabel: Termos e condições
TosConfirm: Eu aceito os
TosLinkText: termos de serviço
@@ -384,6 +386,7 @@ ExternalNotFound:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
DeviceAuth:
Title: Autorização de dispositivo

View File

@@ -266,6 +266,7 @@ RegistrationUser:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
GenderLabel: Gen
Female: Femeie
Male: Bărbat
@@ -310,6 +311,7 @@ ExternalRegistrationUserOverview:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
TosAndPrivacyLabel: Termeni și condiții
TosConfirm: Accept
TosLinkText: TOS
@@ -388,6 +390,7 @@ ExternalNotFound:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
DeviceAuth:
Title: Autorizare dispozitiv
UserCode:

View File

@@ -266,6 +266,7 @@ RegistrationUser:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
GenderLabel: Пол
Female: Женский
Male: Мужской
@@ -310,6 +311,7 @@ ExternalRegistrationUserOverview:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
TosAndPrivacyLabel: Условия использования
TosConfirm: Я согласен с
TosLinkText: Пользовательским соглашением
@@ -388,6 +390,7 @@ ExternalNotFound:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
DeviceAuth:
Title: Авторизация устройства

View File

@@ -266,6 +266,7 @@ RegistrationUser:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
GenderLabel: Kön
Female: Man
Male: Kvinna
@@ -310,6 +311,7 @@ ExternalRegistrationUserOverview:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
TosAndPrivacyLabel: Användarvillkor
TosConfirm: Jag accepterar
TosLinkText: Användarvillkoren
@@ -388,6 +390,7 @@ ExternalNotFound:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
DeviceAuth:
Title: Tillgång från hårdvaruenhet
UserCode:

View File

@@ -0,0 +1,531 @@
Login:
Title: Tekrar Hoş Geldiniz!
Description: Giriş bilgilerinizi girin.
TitleLinking: Kullanıcı bağlama için giriş
DescriptionLinking: Harici kullanıcınızı bağlamak için giriş bilgilerinizi girin.
LoginNameLabel: Giriş Adı
UsernamePlaceHolder: kullanıcıadı
LoginnamePlaceHolder: kullanıcıadı@domain
ExternalUserDescription: Harici kullanıcı ile giriş yapın.
MustBeMemberOfOrg: Kullanıcı {{.OrgName}} organizasyonunun üyesi olmalıdır.
RegisterButtonText: Kayıt Ol
NextButtonText: İleri
LDAP:
Title: Giriş
Description: Giriş bilgilerinizi girin.
LoginNameLabel: Giriş Adı
PasswordLabel: Şifre
NextButtonText: İleri
SelectAccount:
Title: Hesap Seç
Description: Hesabınızı kullanın
TitleLinking: Kullanıcı bağlama için hesap seç
DescriptionLinking: Harici kullanıcınızla bağlamak için hesabınızı seçin.
OtherUser: Diğer Kullanıcı
SessionState0: aktif
SessionState1: Çıkış yapıldı
MustBeMemberOfOrg: Kullanıcı {{.OrgName}} organizasyonunun üyesi olmalıdır.
Password:
Title: Şifre
Description: Giriş bilgilerinizi girin.
PasswordLabel: Şifre
MinLength: En az
MinLengthp2: karakter uzunluğunda olmalıdır.
MaxLength: 70 karakterden az olmalıdır.
HasUppercase: Büyük harf içermelidir.
HasLowercase: Küçük harf içermelidir.
HasNumber: Rakam içermelidir.
HasSymbol: Sembol içermelidir.
Confirmation: Şifre onayı eşleşti.
ResetLinkText: Şifreyi Sıfırla
BackButtonText: Geri
NextButtonText: İleri
UsernameChange:
Title: Kullanıcı Adını Değiştir
Description: Yeni kullanıcı adınızı belirleyin
UsernameLabel: Kullanıcı Adı
CancelButtonText: İptal
NextButtonText: İleri
UsernameChangeDone:
Title: Kullanıcı Adı Değiştirildi
Description: Kullanıcı adınız başarıyla değiştirildi.
NextButtonText: İleri
InitPassword:
Title: Şifre Belirle
Description: Yeni şifrenizi belirlemek için aşağıdaki forma girmeniz gereken bir kod aldınız.
CodeLabel: Kod
NewPasswordLabel: Yeni Şifre
NewPasswordConfirmLabel: Şifreyi Onayla
ResendButtonText: Kodu Tekrar Gönder
NextButtonText: İleri
InitPasswordDone:
Title: Şifre Belirlendi
Description: Şifre başarıyla belirlendi
NextButtonText: İleri
CancelButtonText: İptal
InitUser:
Title: Kullanıcıyı Etkinleştir
Description: E-postanızı aşağıdaki kod ile doğrulayın ve şifrenizi belirleyin.
CodeLabel: Kod
NewPasswordLabel: Yeni Şifre
NewPasswordConfirm: Şifreyi Onayla
NextButtonText: İleri
ResendButtonText: Kodu Tekrar Gönder
InitUserDone:
Title: Kullanıcı Etkinleştirildi
Description: E-posta doğrulandı ve şifre başarıyla belirlendi
NextButtonText: İleri
CancelButtonText: İptal
InviteUser:
Title: Kullanıcıyı Etkinleştir
Description: E-postanızı aşağıdaki kod ile doğrulayın ve şifrenizi belirleyin.
CodeLabel: Kod
NewPasswordLabel: Yeni Şifre
NewPasswordConfirm: Şifreyi Onayla
NextButtonText: İleri
ResendButtonText: Kodu Tekrar Gönder
InitMFAPrompt:
Title: 2-Faktör Kurulumu
Description: 2-faktörlü kimlik doğrulama, kullanıcı hesabınız için ek güvenlik sağlar. Bu sayede hesabınıza yalnızca sizin erişiminiz olması sağlanır.
Provider0: Kimlik Doğrulayıcı Uygulama (örn. Google/Microsoft Authenticator, Authy)
Provider1: Cihaza bağımlı (örn. FaceID, Windows Hello, Parmak izi)
Provider3: OTP SMS
Provider4: OTP E-posta
NextButtonText: İleri
SkipButtonText: Atla
InitMFAOTP:
Title: 2-Faktör Doğrulama
Description: 2-faktörünüzü oluşturun. Henüz yoksa bir kimlik doğrulayıcı uygulama indirin.
OTPDescription: Kodu kimlik doğrulayıcı uygulamanızla (örn. Google/Microsoft Authenticator, Authy) tarayın veya gizli anahtarı kopyalayın ve aşağıda oluşturulan kodu girin.
SecretLabel: Gizli Anahtar
CodeLabel: Kod
NextButtonText: İleri
CancelButtonText: İptal
InitMFAOTPSMS:
Title: 2-Faktör Doğrulama
DescriptionPhone: 2-faktörünüzü oluşturun. Doğrulamak için telefon numaranızı girin.
DescriptionCode: 2-faktörünüzü oluşturun. Telefon numaranızı doğrulamak için aldığınız kodu girin.
PhoneLabel: Telefon
CodeLabel: Kod
EditButtonText: Düzenle
ResendButtonText: Kodu Tekrar Gönder
NextButtonText: İleri
InitMFAU2F:
Title: Güvenlik Anahtarı Ekle
Description: Güvenlik anahtarı, telefonunuza yerleştirilmiş, Bluetooth kullanan veya bilgisayarınızın USB portuna doğrudan takılan bir doğrulama yöntemidir.
TokenNameLabel: Güvenlik anahtarı / cihaz adı
NotSupported: WebAuthN tarayıcınız tarafından desteklenmiyor. Lütfen güncel olduğundan emin olun veya farklı bir tarayıcı kullanın (örn. Chrome, Safari, Firefox)
RegisterTokenButtonText: Güvenlik anahtarı ekle
ErrorRetry: Tekrar deneyin, yeni bir challenge oluşturun veya farklı bir yöntem seçin.
InitMFADone:
Title: 2-faktör Doğrulandı
Description: Harika! 2-faktörünüzü başarıyla kurdunuz ve hesabınızı çok daha güvenli hale getirdiniz. Faktör her girişte girilmek zorundadır.
NextButtonText: İleri
CancelButtonText: İptal
MFAProvider:
Provider0: Kimlik Doğrulayıcı Uygulama (örn. Google/Microsoft Authenticator, Authy)
Provider1: Cihaza bağımlı (örn. FaceID, Windows Hello, Parmak izi)
Provider3: OTP SMS
Provider4: OTP E-posta
ChooseOther: veya başka bir seçenek seçin
VerifyMFAOTP:
Title: 2-Faktör Doğrula
Description: İkinci faktörünüzü doğrulayın
CodeLabel: Kod
NextButtonText: İleri
VerifyOTP:
Title: 2-Faktör Doğrula
Description: İkinci faktörünüzü doğrulayın
CodeLabel: Kod
ResendButtonText: Kodu Tekrar Gönder
NextButtonText: İleri
VerifyMFAU2F:
Title: 2-Faktör Doğrulama
Description: Kayıtlı cihazınızla (örn. FaceID, Windows Hello, Parmak izi) 2-Faktörünüzü doğrulayın
NotSupported: WebAuthN tarayıcınız tarafından desteklenmiyor. En yeni sürümü kullandığınızdan emin olun veya desteklenen bir tarayıcıya geçin (Chrome, Safari, Firefox)
ErrorRetry: Tekrar deneyin, yeni bir istek oluşturun veya başka bir yöntem seçin.
ValidateTokenButtonText: 2-Faktör Doğrula
Passwordless:
Title: Şifresiz Giriş
Description: FaceID, Windows Hello veya Parmak izi gibi cihazınızın sağladığı kimlik doğrulama yöntemleriyle giriş yapın.
NotSupported: WebAuthN tarayıcınız tarafından desteklenmiyor. Lütfen güncel olduğundan emin olun veya farklı bir tarayıcı kullanın (örn. Chrome, Safari, Firefox)
ErrorRetry: Tekrar deneyin, yeni bir challenge oluşturun veya farklı bir yöntem seçin.
LoginWithPwButtonText: Şifre ile giriş yap
ValidateTokenButtonText: Şifresiz giriş yap
PasswordlessPrompt:
Title: Şifresiz Kurulum
Description: Şifresiz giriş kurmak ister misiniz? (FaceID, Windows Hello veya Parmak izi gibi cihazınızın kimlik doğrulama yöntemleri)
DescriptionInit: Şifresiz giriş kurmanız gerekiyor. Cihazınızı kaydetmek için size verilen bağlantıyı kullanın.
PasswordlessButtonText: Şifresiz devam et
NextButtonText: İleri
SkipButtonText: Atla
PasswordlessRegistration:
Title: Şifresiz Kurulum
Description: Bir isim vererek (örn. BenimTelefonum, MacBook, vb.) ve ardından aşağıdaki 'Şifresiz kaydet' düğmesine tıklayarak kimlik doğrulamanızı ekleyin.
TokenNameLabel: Cihazın adı
NotSupported: WebAuthN tarayıcınız tarafından desteklenmiyor. Lütfen güncel olduğundan emin olun veya farklı bir tarayıcı kullanın (örn. Chrome, Safari, Firefox)
RegisterTokenButtonText: Şifresiz kaydet
ErrorRetry: Tekrar deneyin, yeni bir challenge oluşturun veya farklı bir yöntem seçin.
PasswordlessRegistrationDone:
Title: Şifresiz Kurulum Tamamlandı
Description: Şifresiz cihaz başarıyla eklendi.
DescriptionClose: Artık bu pencereyi kapatabilirsiniz.
NextButtonText: İleri
CancelButtonText: İptal
PasswordChange:
Title: Şifre Değiştir
Description: Şifrenizi değiştirin. Eski ve yeni şifrenizi girin.
ExpiredDescription: Şifrenizin süresi dolmuş ve değiştirilmesi gerekiyor. Eski ve yeni şifrenizi girin.
OldPasswordLabel: Eski Şifre
NewPasswordLabel: Yeni Şifre
NewPasswordConfirmLabel: Şifre onayı
CancelButtonText: İptal
NextButtonText: İleri
Footer: Alt Bilgi
PasswordChangeDone:
Title: Şifre Değiştir
Description: Şifreniz başarıyla değiştirildi.
NextButtonText: İleri
PasswordResetDone:
Title: Şifre Sıfırlama Bağlantısı Gönderildi
Description: Şifrenizi sıfırlamak için e-postanızı kontrol edin.
NextButtonText: İleri
EmailVerification:
Title: E-Posta Doğrulama
Description: Adresinizi doğrulamak için size bir e-posta gönderdik. Lütfen aşağıdaki forma kodu girin.
CodeLabel: Kod
NextButtonText: İleri
ResendButtonText: Kodu Tekrar Gönder
EmailVerificationDone:
Title: E-Posta Doğrulama
Description: E-posta adresiniz başarıyla doğrulandı.
NextButtonText: İleri
CancelButtonText: Cancel
LoginButtonText: Login
RegisterOption:
Title: Kayıt Seçenekleri
Description: Nasıl kayıt olmak istediğinizi seçin
RegisterUsernamePasswordButtonText: Kullanıcı adı ve şifre ile
ExternalLoginDescription: veya harici bir kullanıcı ile kayıt olun
LoginButtonText: Giriş
RegistrationUser:
Title: Kayıt
Description: Kullanıcı verilerinizi girin. E-posta adresiniz giriş adınız olarak kullanılacaktır.
DescriptionOrgRegister: Kullanıcı verilerinizi girin.
EmailLabel: E-Posta
UsernameLabel: Kullanıcı adı
FirstnameLabel: Ad
LastnameLabel: Soyadı
LanguageLabel: Dil
German: Deutsch
English: English
Italian: Italiano
French: Français
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
Bulgarian: Български
Portuguese: Português
Macedonian: Македонски
Czech: Čeština
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
GenderLabel: Cinsiyet
Female: Kadın
Male: Erkek
Diverse: Çeşitli / X
PasswordLabel: Şifre
PasswordConfirmLabel: Şifre onayı
TosAndPrivacyLabel: Şartlar ve koşullar
TosConfirm: Kabul ediyorum
TosLinkText: Kullanım Şartları
PrivacyConfirm: Kabul ediyorum
PrivacyLinkText: gizlilik politikası
ExternalLogin: veya harici bir kullanıcı ile kayıt olun
BackButtonText: Giriş
NextButtonText: İleri
ExternalRegistrationUserOverview:
Title: Harici Kullanıcı Kaydı
Description: Seçilen sağlayıcıdan kullanıcı bilgilerinizi aldık. Artık bunları değiştirebilir veya tamamlayabilirsiniz.
EmailLabel: E-Posta
UsernameLabel: Kullanıcı adı
FirstnameLabel: Ad
LastnameLabel: Soyadı
NicknameLabel: Takma ad
PhoneLabel: Telefon numarası
LanguageLabel: Dil
German: Deutsch
English: English
Italian: Italiano
French: Français
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
Bulgarian: Български
Portuguese: Português
Macedonian: Македонски
Czech: Čeština
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
TosAndPrivacyLabel: Şartlar ve koşullar
TosConfirm: Kabul ediyorum
TosLinkText: Kullanım Şartları
PrivacyConfirm: Kabul ediyorum
PrivacyLinkText: gizlilik politikası
ExternalLogin: veya harici bir kullanıcı ile kayıt olun
BackButtonText: Geri
NextButtonText: Kaydet
RegistrationOrg:
Title: Organizasyon Kaydı
Description: Organizasyon adınızı ve kullanıcı verilerinizi girin.
OrgNameLabel: Organizasyon adı
EmailLabel: E-Posta
UsernameLabel: Kullanıcı adı
FirstnameLabel: Ad
LastnameLabel: Soyadı
PasswordLabel: Şifre
PasswordConfirmLabel: Şifre onayı
TosAndPrivacyLabel: Şartlar ve koşullar
TosConfirm: Kabul ediyorum
TosLinkText: Kullanım Şartları
PrivacyConfirm: Kabul ediyorum
PrivacyLinkText: gizlilik politikası
SaveButtonText: Organizasyon oluştur
LoginSuccess:
Title: Giriş Başarılı
AutoRedirectDescription: Uygulamanıza otomatik olarak yönlendirileceksiniz. Değilse, aşağıdaki düğmeye tıklayın. Daha sonra pencereyi kapatabilirsiniz.
RedirectedDescription: Artık bu pencereyi kapatabilirsiniz.
NextButtonText: İleri
LogoutDone:
Title: Çıkış Yapıldı
Description: Başarıyla çıkış yaptınız.
LoginButtonText: Giriş
LinkingUserPrompt:
Title: Mevcut Kullanıcı Bulundu
Description: "Mevcut hesabınızı bağlamak ister misiniz:"
LinkButtonText: Bağla
OtherButtonText: Diğer seçenekler
LinkingUsersDone:
Title: Kullanıcı Bağlama
Description: Kullanıcı bağlandı.
CancelButtonText: İptal
NextButtonText: İleri
ExternalNotFound:
Title: Harici Kullanıcı Bulunamadı
Description: Harici kullanıcı bulunamadı. Kullanıcınızı bağlamak mı yoksa yeni bir kullanıcı otomatik kaydetmek mi istiyorsunuz?
LinkButtonText: Bağla
AutoRegisterButtonText: Kayıt Ol
TosAndPrivacyLabel: Şartlar ve koşullar
TosConfirm: Kabul ediyorum
TosLinkText: Kullanım Şartları
PrivacyConfirm: Kabul ediyorum
PrivacyLinkText: gizlilik politikası
German: Deutsch
English: English
Italian: Italiano
French: Français
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
Bulgarian: Български
Portuguese: Português
Macedonian: Македонски
Czech: Čeština
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
DeviceAuth:
Title: Cihaz Yetkilendirme
UserCode:
Label: Kullanıcı Kodu
Description: Cihazda sunulan kullanıcı kodunu girin.
ButtonNext: İleri
Action:
Description: Cihaz erişimi verin.
GrantDevice: cihaza yetki vermek üzeresiniz
AccessToScopes: aşağıdaki kapsam alanlarına erişim
Button:
Allow: İzin Ver
Deny: Reddet
Done:
Description: Tamamlandı.
Approved: Cihaz yetkilendirmesi onaylandı. Artık cihaza dönebilirsiniz.
Denied: Cihaz yetkilendirmesi reddedildi. Artık cihaza dönebilirsiniz.
Footer:
PoweredBy: Teknoloji Desteği
Tos: Kullanım Şartları
PrivacyPolicy: Gizlilik politikası
Help: Yardım
SupportEmail: Destek E-posta
SignIn: "{{.Provider}} ile giriş yap"
Errors:
Internal: Dahili bir hata oluştu
AuthRequest:
NotFound: Kimlik doğrulama isteği bulunamadı
UserAgentNotCorresponding: User Agent uyuşmuyor
UserAgentNotFound: User Agent ID bulunamadı
TokenNotFound: Token bulunamadı
RequestTypeNotSupported: İstek türü desteklenmiyor
MissingParameters: Gerekli parametreler eksik
User:
NotFound: Kullanıcı bulunamadı
AlreadyExists: Kullanıcı zaten mevcut
Inactive: Kullanıcı aktif değil
NotFoundOnOrg: Kullanıcı seçilen organizasyonda bulunamadı
NotAllowedOrg: Kullanıcı gerekli organizasyonun üyesi değil
NotMatchingUserID: Kullanıcı ve kimlik doğrulama isteğindeki kullanıcı uyuşmuyor
UserIDMissing: Kullanıcı ID'si boş
Invalid: Geçersiz kullanıcı verisi
DomainNotAllowedAsUsername: Alan adı zaten rezerve edilmiş ve kullanılamaz
NotAllowedToLink: Kullanıcının harici giriş sağlayıcısı ile bağlantı kurma izni yok
Profile:
NotFound: Profil bulunamadı
NotChanged: Profil değişmedi
Empty: Profil boş
FirstNameEmpty: Profildeki ad boş
LastNameEmpty: Profildeki soyadı boş
IDMissing: Profil ID'si eksik
Email:
NotFound: E-posta bulunamadı
Invalid: E-posta geçersiz
AlreadyVerified: E-posta zaten doğrulanmış
NotChanged: E-posta değişmedi
Empty: E-posta boş
IDMissing: E-posta ID'si eksik
Phone:
NotFound: Telefon bulunamadı
Invalid: Telefon geçersiz
AlreadyVerified: Telefon zaten doğrulanmış
Empty: Telefon boş
NotChanged: Telefon değişmedi
Address:
NotFound: Adres bulunamadı
NotChanged: Adres değişmedi
Username:
AlreadyExists: Kullanıcı adı zaten alınmış
Reserved: Kullanıcı adı zaten alınmış
Empty: Kullanıcı adı boş
Password:
ConfirmationWrong: Şifre onayı yanlış
Empty: Şifre boş
Invalid: Şifre geçersiz
InvalidAndLocked: Şifre geçersiz ve kullanıcı kilitli, yöneticinize başvurun.
NotChanged: Yeni şifre mevcut şifrenizle aynı olamaz
UsernameOrPassword:
Invalid: Kullanıcı adı veya Şifre geçersiz
PasswordComplexityPolicy:
NotFound: Şifre politikası bulunamadı
MinLength: Şifre çok kısa
HasLower: Şifre küçük harf içermeli
HasUpper: Şifre büyük harf içermeli
HasNumber: Şifre sayı içermeli
HasSymbol: Şifre sembol içermeli
Code:
Expired: Kod süresi doldu
Invalid: Kod geçersiz
Empty: Kod boş
CryptoCodeNil: Kripto kodu boş
NotFound: Kod bulunamadı
GeneratorAlgNotSupported: Desteklenmeyen oluşturucu algoritması
EmailVerify:
UserIDEmpty: Kullanıcı ID'si boş
ExternalData:
CouldNotRead: Harici veri doğru okunamadı
MFA:
NoProviders: Kullanılabilir çok faktörlü kimlik doğrulama sağlayıcısı yok
OTP:
AlreadyReady: Çok faktörlü OTP (Tek Seferlik Şifre) zaten ayarlanmış
NotExisting: Çok faktörlü OTP (Tek Seferlik Şifre) mevcut değil
InvalidCode: Geçersiz kod
NotReady: Çok faktörlü OTP (Tek Seferlik Şifre) hazır değil
Locked: Kullanıcı kilitli
SomethingWentWrong: Bir şeyler yanlış gitti
NotActive: Kullanıcı aktif değil
ExternalIDP:
IDPTypeNotImplemented: IDP Türü uygulanmamış
NotAllowed: Harici Giriş Sağlayıcısına izin verilmiyor
IDPConfigIDEmpty: Kimlik Sağlayıcı ID'si boş
ExternalUserIDEmpty: Harici Kullanıcı ID'si boş
UserDisplayNameEmpty: Kullanıcı Görünen Adı boş
NoExternalUserData: Harici Kullanıcı Verisi alınmadı
CreationNotAllowed: Bu sağlayıcıda yeni kullanıcı oluşturmaya izin verilmiyor
LinkingNotAllowed: Bu sağlayıcıda kullanıcı bağlamaya izin verilmiyor
NoOptionAllowed: Bu sağlayıcıda ne oluşturmaya ne de bağlamaya izin verilmiyor. Lütfen yöneticinizle iletişime geçin.
LoginFailedSwitchLocal: |
Harici IDP'de giriş başarısız oldu. Yerel girişe geri dönülüyor.
Hata detayları: {{.Details}}
GrantRequired: Giriş mümkün değil. Kullanıcının uygulamada en az bir yetkisi olması gerekiyor. Lütfen yöneticinizle iletişime geçin.
ProjectRequired: Giriş mümkün değil. Kullanıcının organizasyonuna proje için yetki verilmiş olması gerekiyor. Lütfen yöneticinizle iletişime geçin.
IdentityProvider:
InvalidConfig: Kimlik Sağlayıcı yapılandırması geçersiz
IAM:
LockoutPolicy:
NotExisting: Kilitleme Politikası mevcut değil
Org:
LoginPolicy:
RegistrationNotAllowed: Kayıt olmasına izin verilmiyor
DeviceAuth:
NotExisting: Kullanıcı Kodu mevcut değil
optional: (isteğe bağlı)

View File

@@ -266,6 +266,7 @@ RegistrationUser:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
GenderLabel: 性别
Female: 女性
Male: 男性
@@ -310,6 +311,7 @@ ExternalRegistrationUserOverview:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
TosAndPrivacyLabel: 条款和条款
TosConfirm: 我接受
TosLinkText: 服务条款
@@ -388,6 +390,7 @@ ExternalNotFound:
Hungarian: Magyar
Korean: 한국어
Romanian: Română
Turkish: Türkçe
DeviceAuth:
Title: 设备授权
UserCode:

View File

@@ -102,6 +102,8 @@
</option>
<option value="ro" id="ro" {{if (selectedLanguage "ro")}} selected {{end}}>{{t "ExternalNotFound.Romanian"}}
</option>
<option value="tr" id="tr" {{if (selectedLanguage "tr")}} selected {{end}}>{{t "ExternalNotFound.Turkish"}}
</option>
</select>
</div>
</div>