Merge remote-tracking branch 'origin/main' into next

This commit is contained in:
Livio Spring 2024-05-02 15:30:18 +02:00
commit 65d109ecd1
No known key found for this signature in database
GPG Key ID: 26BB1C2FA5952CF0
53 changed files with 1016 additions and 325 deletions

View File

@ -1,6 +1,11 @@
name: ZITADEL CI/CD
on:
push:
tags-ignore:
- "*"
branches:
- "main"
pull_request:
workflow_dispatch:

View File

@ -108,13 +108,13 @@ Please make sure you cover your changes with tests before marking a Pull Request
The code consists of the following parts:
| name | description | language | where to find |
| --------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------- | -------------------------------------------------- |
| backend | Service that serves the grpc(-web) and RESTful API | [go](https://go.dev) | [API implementation](./internal/api/grpc) |
| console | Frontend the user interacts with after log in | [Angular](https://angular.io), [Typescript](https://www.typescriptlang.org) | [./console](./console) |
| name | description | language | where to find |
| --------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -------------------------------------------------- |
| backend | Service that serves the grpc(-web) and RESTful API | [go](https://go.dev) | [API implementation](./internal/api/grpc) |
| console | Frontend the user interacts with after log in | [Angular](https://angular.io), [Typescript](https://www.typescriptlang.org) | [./console](./console) |
| login | Server side rendered frontend the user interacts with during login | [go](https://go.dev), [go templates](https://pkg.go.dev/html/template) | [./internal/api/ui/login](./internal/api/ui/login) |
| API definitions | Specifications of the API | [Protobuf](https://developers.google.com/protocol-buffers) | [./proto/zitadel](./proto/zitadel) |
| docs | Project documentation made with docusaurus | [Docusaurus](https://docusaurus.io/) | [./docs](./docs) |
| API definitions | Specifications of the API | [Protobuf](https://developers.google.com/protocol-buffers) | [./proto/zitadel](./proto/zitadel) |
| docs | Project documentation made with docusaurus | [Docusaurus](https://docusaurus.io/) | [./docs](./docs) |
Please validate and test the code before you contribute.
@ -129,12 +129,12 @@ We add the label "good first issue" for problems we think are a good starting po
We are committed to creating a welcoming and inclusive community for all developers, regardless of their gender identity or expression. To achieve this, we are actively working to ensure that our contribution guidelines are gender-neutral and use inclusive language.
**Use gender-neutral pronouns**:
**Use gender-neutral pronouns**:
Don't use gender-specific pronouns unless the person you're referring to is actually that gender.
In particular, don't use he, him, his, she, or her as gender-neutral pronouns, and don't use he/she or (s)he or other such punctuational approaches. Instead, use the singular they.
**Choose gender-neutral alternatives**:
Opt for gender-neutral terms instead of gendered ones whenever possible.
**Choose gender-neutral alternatives**:
Opt for gender-neutral terms instead of gendered ones whenever possible.
Replace "policeman" with "police officer," "manpower" with "workforce," and "businessman" with "entrepreneur" or "businessperson."
**Avoid ableist language**:
@ -194,7 +194,7 @@ make core_unit_test
To test the database-connected gRPC API, run PostgreSQL and CockroachDB, set up a ZITADEL instance and run the tests including integration tests:
```bash
export INTEGRATION_DB_FLAVOR="postgres" ZITADEL_MASTERKEY="MasterkeyNeedsToHave32Characters"
export INTEGRATION_DB_FLAVOR="cockroach" ZITADEL_MASTERKEY="MasterkeyNeedsToHave32Characters"
docker compose -f internal/integration/config/docker-compose.yaml up --pull always --wait ${INTEGRATION_DB_FLAVOR}
make core_integration_test
docker compose -f internal/integration/config/docker-compose.yaml down

View File

@ -12,13 +12,14 @@ import (
"github.com/zitadel/zitadel/cmd/encryption"
"github.com/zitadel/zitadel/cmd/hooks"
"github.com/zitadel/zitadel/internal/actions"
"github.com/zitadel/zitadel/internal/api/authz"
internal_authz "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/oidc"
"github.com/zitadel/zitadel/internal/api/ui/login"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/config/hook"
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/notification/handlers"
@ -29,7 +30,7 @@ import (
type Config struct {
Database database.Config
SystemDefaults systemdefaults.SystemDefaults
InternalAuthZ authz.Config
InternalAuthZ internal_authz.Config
ExternalDomain string
ExternalPort uint16
ExternalSecure bool
@ -46,7 +47,7 @@ type Config struct {
Login login.Config
WebAuthNName string
Telemetry *handlers.TelemetryPusherConfig
SystemAPIUsers map[string]*authz.SystemAPIUser
SystemAPIUsers map[string]*internal_authz.SystemAPIUser
}
type InitProjections struct {
@ -60,16 +61,18 @@ func MustNewConfig(v *viper.Viper) *Config {
config := new(Config)
err := v.Unmarshal(config,
viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
hooks.SliceTypeStringDecode[*domain.CustomMessageText],
hooks.SliceTypeStringDecode[internal_authz.RoleMapping],
hooks.MapTypeStringDecode[string, *internal_authz.SystemAPIUser],
hooks.MapHTTPHeaderStringDecode,
database.DecodeHook,
actions.HTTPConfigDecodeHook,
hook.EnumHookFunc(internal_authz.MemberTypeString),
hook.Base64ToBytesHookFunc(),
hook.TagToLanguageHookFunc(),
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToTimeHookFunc(time.RFC3339),
mapstructure.StringToSliceHookFunc(","),
database.DecodeHook,
hook.EnumHookFunc(authz.MemberTypeString),
actions.HTTPConfigDecodeHook,
hooks.MapTypeStringDecode[string, *authz.SystemAPIUser],
hooks.SliceTypeStringDecode[authz.RoleMapping],
)),
)
logging.OnError(err).Fatal("unable to read default config")

245
cmd/setup/config_test.go Normal file
View File

@ -0,0 +1,245 @@
package setup
import (
"encoding/base64"
"fmt"
"net/http"
"strings"
"testing"
"github.com/muhlemmer/gu"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
)
func TestMustNewConfig(t *testing.T) {
encodedKey := "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF6aStGRlNKTDdmNXl3NEtUd3pnTQpQMzRlUEd5Y20vTStrVDBNN1Y0Q2d4NVYzRWFESXZUUUtUTGZCYUVCNDV6YjlMdGpJWHpEdzByWFJvUzJoTzZ0CmgrQ1lRQ3ozS0N2aDA5QzBJenhaaUIySVMzSC9hVCs1Qng5RUZZK3ZuQWtaamNjYnlHNVlOUnZtdE9sbnZJZUkKSDdxWjB0RXdrUGZGNUdFWk5QSlB0bXkzVUdWN2lvZmRWUVMxeFJqNzMrYU13NXJ2SDREOElkeWlBQzNWZWtJYgpwdDBWajBTVVgzRHdLdG9nMzM3QnpUaVBrM2FYUkYwc2JGaFFvcWRKUkk4TnFnWmpDd2pxOXlmSTV0eXhZc3duCitKR3pIR2RIdlczaWRPRGxtd0V0NUsycGFzaVJJV0syT0dmcSt3MEVjbHRRSGFidXFFUGdabG1oQ2tSZE5maXgKQndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="
decodedKey, err := base64.StdEncoding.DecodeString(encodedKey)
if err != nil {
t.Fatal(err)
}
type args struct {
yaml string
}
tests := []struct {
name string
args args
want func(*testing.T, *Config)
}{{
name: "features ok",
args: args{yaml: `
DefaultInstance:
Features:
LoginDefaultOrg: true
LegacyIntrospection: true
TriggerIntrospectionProjections: true
UserSchema: true
Log:
Level: info
Actions:
HTTP:
DenyList: []
`},
want: func(t *testing.T, config *Config) {
assert.Equal(t, config.DefaultInstance.Features, &command.InstanceFeatures{
LoginDefaultOrg: gu.Ptr(true),
LegacyIntrospection: gu.Ptr(true),
TriggerIntrospectionProjections: gu.Ptr(true),
UserSchema: gu.Ptr(true),
})
},
}, {
name: "system api users ok",
args: args{yaml: `
SystemAPIUsers:
- superuser:
Memberships:
- MemberType: System
- MemberType: Organization
- MemberType: IAM
Log:
Level: info
Actions:
HTTP:
DenyList: []
`},
want: func(t *testing.T, config *Config) {
assert.Equal(t, config.SystemAPIUsers, map[string]*authz.SystemAPIUser{
"superuser": {
Memberships: authz.Memberships{{
MemberType: authz.MemberTypeSystem,
}, {
MemberType: authz.MemberTypeOrganization,
}, {
MemberType: authz.MemberTypeIAM,
}},
},
})
},
}, {
name: "system api users string ok",
args: args{yaml: fmt.Sprintf(`
SystemAPIUsers: >
{"systemuser": {"path": "/path/to/superuser/key.pem"}, "systemuser2": {"keyData": "%s"}}
Log:
Level: info
Actions:
HTTP:
DenyList: []
`, encodedKey)},
want: func(t *testing.T, config *Config) {
assert.Equal(t, config.SystemAPIUsers, map[string]*authz.SystemAPIUser{
"systemuser": {
Path: "/path/to/superuser/key.pem",
},
"systemuser2": {
KeyData: decodedKey,
},
})
},
}, {
name: "headers ok",
args: args{yaml: `
Telemetry:
Headers:
single-value: single-value
multi-value:
- multi-value1
- multi-value2
Log:
Level: info
Actions:
HTTP:
DenyList: []
`},
want: func(t *testing.T, config *Config) {
assert.Equal(t, config.Telemetry.Headers, http.Header{
"single-value": []string{"single-value"},
"multi-value": []string{"multi-value1", "multi-value2"},
})
},
}, {
name: "headers string ok",
args: args{yaml: `
Telemetry:
Headers: >
{"single-value": "single-value", "multi-value": ["multi-value1", "multi-value2"]}
Log:
Level: info
Actions:
HTTP:
DenyList: []
`},
want: func(t *testing.T, config *Config) {
assert.Equal(t, config.Telemetry.Headers, http.Header{
"single-value": []string{"single-value"},
"multi-value": []string{"multi-value1", "multi-value2"},
})
},
}, {
name: "message texts ok",
args: args{yaml: `
DefaultInstance:
MessageTexts:
- MessageTextType: InitCode
Title: foo
- MessageTextType: PasswordReset
Greeting: bar
Log:
Level: info
Actions:
HTTP:
DenyList: []
`},
want: func(t *testing.T, config *Config) {
assert.Equal(t, config.DefaultInstance.MessageTexts, []*domain.CustomMessageText{{
MessageTextType: "InitCode",
Title: "foo",
}, {
MessageTextType: "PasswordReset",
Greeting: "bar",
}})
},
}, {
name: "message texts string ok",
args: args{yaml: `
DefaultInstance:
MessageTexts: >
[{"messageTextType": "InitCode", "title": "foo"}, {"messageTextType": "PasswordReset", "greeting": "bar"}]
Log:
Level: info
Actions:
HTTP:
DenyList: []
`},
want: func(t *testing.T, config *Config) {
assert.Equal(t, config.DefaultInstance.MessageTexts, []*domain.CustomMessageText{{
MessageTextType: "InitCode",
Title: "foo",
}, {
MessageTextType: "PasswordReset",
Greeting: "bar",
}})
},
}, {
name: "roles ok",
args: args{yaml: `
InternalAuthZ:
RolePermissionMappings:
- Role: IAM_OWNER
Permissions:
- iam.write
- Role: ORG_OWNER
Permissions:
- org.write
- org.read
Log:
Level: info
Actions:
HTTP:
DenyList: []
`},
want: func(t *testing.T, config *Config) {
assert.Equal(t, config.InternalAuthZ, authz.Config{
RolePermissionMappings: []authz.RoleMapping{
{Role: "IAM_OWNER", Permissions: []string{"iam.write"}},
{Role: "ORG_OWNER", Permissions: []string{"org.write", "org.read"}},
},
})
},
}, {
name: "roles string ok",
args: args{yaml: `
InternalAuthZ:
RolePermissionMappings: >
[{"role": "IAM_OWNER", "permissions": ["iam.write"]}, {"role": "ORG_OWNER", "permissions": ["org.write", "org.read"]}]
Log:
Level: info
Actions:
HTTP:
DenyList: []
`},
want: func(t *testing.T, config *Config) {
assert.Equal(t, config.InternalAuthZ, authz.Config{
RolePermissionMappings: []authz.RoleMapping{
{Role: "IAM_OWNER", Permissions: []string{"iam.write"}},
{Role: "ORG_OWNER", Permissions: []string{"org.write", "org.read"}},
},
})
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := viper.New()
v.SetConfigType("yaml")
require.NoError(t, v.ReadConfig(strings.NewReader(tt.args.yaml)))
got := MustNewConfig(v)
tt.want(t, got)
})
}
}

View File

@ -85,19 +85,19 @@ func MustNewConfig(v *viper.Viper) *Config {
err := v.Unmarshal(config,
viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
hooks.SliceTypeStringDecode[*domain.CustomMessageText],
hooks.SliceTypeStringDecode[*command.SetQuota],
hooks.SliceTypeStringDecode[internal_authz.RoleMapping],
hooks.MapTypeStringDecode[string, *internal_authz.SystemAPIUser],
hooks.MapTypeStringDecode[domain.Feature, any],
hooks.MapHTTPHeaderStringDecode,
database.DecodeHook,
actions.HTTPConfigDecodeHook,
hook.EnumHookFunc(internal_authz.MemberTypeString),
hooks.MapTypeStringDecode[domain.Feature, any],
hooks.SliceTypeStringDecode[*command.SetQuota],
hook.Base64ToBytesHookFunc(),
hook.TagToLanguageHookFunc(),
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToTimeHookFunc(time.RFC3339),
mapstructure.StringToSliceHookFunc(","),
database.DecodeHook,
actions.HTTPConfigDecodeHook,
hook.EnumHookFunc(internal_authz.MemberTypeString),
)),
)
logging.OnError(err).Fatal("unable to read config")

View File

@ -96,7 +96,12 @@
[routerLinkActive]="['active']"
[routerLinkActiveOptions]="{ exact: false }"
[routerLink]="['/org-settings']"
*ngIf="(['policy.read'] | hasRole | async) && ((authService.cachedOrgs | async)?.length ?? 1) > 1"
*ngIf="
(['policy.read'] | hasRole | async) &&
((['iam.read$', 'iam.write$'] | hasRole | async) === false ||
(((authService.cachedOrgs | async)?.length ?? 1) > 1 &&
(['iam.read$', 'iam.write$'] | hasRole | async)))
"
>
<span class="label">{{ 'MENU.SETTINGS' | translate }}</span>
</a>

View File

@ -53,6 +53,7 @@ export interface ProviderDefaultSettings {
value: string;
placeholder: string;
};
senderEmailPlaceholder?: string;
image?: string;
routerLinkElement: string;
}
@ -102,6 +103,7 @@ export const MailjetDefaultSettings: ProviderDefaultSettings = {
user: { value: '', placeholder: 'Your Mailjet API key' },
password: { value: '', placeholder: 'Your Mailjet Secret key' },
image: './assets/images/smtp/mailjet.svg',
senderEmailPlaceholder: 'An authorized domain or email address',
routerLinkElement: 'mailjet',
};
@ -114,6 +116,7 @@ export const PostmarkDefaultSettings: ProviderDefaultSettings = {
user: { value: '', placeholder: 'Your Server API token' },
password: { value: '', placeholder: 'Your Server API token' },
image: './assets/images/smtp/postmark.png',
senderEmailPlaceholder: 'An authorized domain or email address',
routerLinkElement: 'postmark',
};
@ -138,6 +141,7 @@ export const MailchimpDefaultSettings: ProviderDefaultSettings = {
user: { value: '', placeholder: 'Your Mailchimp primary contact email' },
password: { value: '', placeholder: 'Your Mailchimp Transactional API key' },
image: './assets/images/smtp/mailchimp.svg',
senderEmailPlaceholder: 'An authorized domain or email address',
routerLinkElement: 'mailchimp',
};
@ -153,6 +157,19 @@ export const BrevoDefaultSettings: ProviderDefaultSettings = {
routerLinkElement: 'brevo',
};
export const OutlookDefaultSettings: ProviderDefaultSettings = {
name: 'outlook.com',
requiredTls: true,
host: 'smtp-mail.outlook.com',
unencryptedPort: 587,
encryptedPort: 587,
user: { value: '', placeholder: 'Your outlook.com email address' },
password: { value: '', placeholder: 'Your outlook.com password' },
image: './assets/images/smtp/outlook.svg',
senderEmailPlaceholder: 'Your outlook.com email address',
routerLinkElement: 'outlook',
};
export const GenericDefaultSettings: ProviderDefaultSettings = {
name: 'generic',
requiredTls: false,
@ -170,5 +187,6 @@ export const SMTPKnownProviders = [
MailjetDefaultSettings,
PostmarkDefaultSettings,
SendgridDefaultSettings,
OutlookDefaultSettings,
GenericDefaultSettings,
];

View File

@ -13,6 +13,7 @@ const types = [
{ path: 'mailjet', component: SMTPProviderComponent },
{ path: 'mailchimp', component: SMTPProviderComponent },
{ path: 'brevo', component: SMTPProviderComponent },
{ path: 'outlook', component: SMTPProviderComponent },
];
const routes: Routes = types.map((value) => {

View File

@ -106,7 +106,13 @@
<cnsl-form-field class="smtp-form-field" label="Sender Address">
<cnsl-label>{{ 'SETTING.SMTP.SENDERADDRESS' | translate }}</cnsl-label>
<input cnslInput name="senderAddress" formControlName="senderAddress" placeholder="sender@example.com" required />
<input
cnslInput
name="senderAddress"
formControlName="senderAddress"
placeholder="{{ senderEmailPlaceholder }}"
required
/>
</cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="Sender Name">

View File

@ -28,6 +28,7 @@ import {
MailjetDefaultSettings,
PostmarkDefaultSettings,
ProviderDefaultSettings,
OutlookDefaultSettings,
SendgridDefaultSettings,
} from './known-smtp-providers-settings';
@ -56,6 +57,8 @@ export class SMTPProviderComponent {
public firstFormGroup!: UntypedFormGroup;
public secondFormGroup!: UntypedFormGroup;
public senderEmailPlaceholder = 'sender@example.com';
constructor(
private service: AdminService,
private _location: Location,
@ -91,6 +94,9 @@ export class SMTPProviderComponent {
case 'brevo':
this.providerDefaultSetting = BrevoDefaultSettings;
break;
case 'outlook':
this.providerDefaultSetting = OutlookDefaultSettings;
break;
}
this.firstFormGroup = this.fb.group({
@ -106,6 +112,8 @@ export class SMTPProviderComponent {
password: [this.providerDefaultSetting?.password.value || ''],
});
this.senderEmailPlaceholder = this.providerDefaultSetting?.senderEmailPlaceholder || 'sender@example.com';
this.secondFormGroup = this.fb.group({
senderAddress: ['', [requiredValidator]],
senderName: ['', [requiredValidator]],

View File

@ -11,7 +11,8 @@
'/instance/smtpprovider/postmark/create',
'/instance/smtpprovider/sendgrid/create',
'/instance/smtpprovider/mailchimp/create',
'/instance/smtpprovider/brevo/create'
'/instance/smtpprovider/brevo/create',
'/instance/smtpprovider/outlook/create'
]"
[timestamp]="configsResult?.details?.viewTimestamp"
[selection]="selection"

View File

@ -422,19 +422,21 @@
</ng-container>
<ng-container *ngIf="currentSetting === 'urls'">
<cnsl-card title=" {{ 'APP.URLS' | translate }}">
<cnsl-info-section *ngIf="(apiMap$ | async)?.issuer as issuer">
<cnsl-info-section *ngIf="issuer$ | async as issuer">
<div
class="link"
[innerHtml]="'APP.OIDC.WELLKNOWN' | translate: { url: issuer + '/.well-known/openid-configuration' }"
></div>
</cnsl-info-section>
<cnsl-copy-row
labelMinWidth="220px"
*ngFor="let apiUrl of apiMap$ | async | keyvalue"
[label]="apiUrl.key"
[value]="apiUrl.value"
></cnsl-copy-row>
<ng-container *ngIf="app?.samlConfig && samlMap$ | async as samlMap">
<ng-container *ngIf="app?.apiConfig && apiURLs$ | async as apiURLs">
<cnsl-copy-row
labelMinWidth="220px"
*ngFor="let apiUrl of apiURLs"
[label]="apiUrl[0]"
[value]="apiUrl[1]"
></cnsl-copy-row>
</ng-container>
<ng-container *ngIf="app?.samlConfig && samlURLs$ | async as samlMap">
<cnsl-copy-row
labelMinWidth="220px"
*ngIf="samlMap['samlCertificateURL'] as url"
@ -456,14 +458,9 @@
[value]="url"
></cnsl-copy-row>
</ng-container>
<ng-container *ngIf="!app?.samlConfig">
<ng-container *ngFor="let wellknown of wellknownMap$ | async | keyvalue">
<cnsl-copy-row
labelMinWidth="220px"
*ngIf="wellknown.key.endsWith('endpoint') || wellknown.key.toString() === 'jwks_uri'"
[label]="wellknown.key"
[value]="wellknown.value"
></cnsl-copy-row>
<ng-container *ngIf="app?.oidcConfig && wellknownURLs$ | async as wellKnownURLs">
<ng-container *ngFor="let wellknown of wellKnownURLs">
<cnsl-copy-row labelMinWidth="220px" [label]="wellknown[0]" [value]="wellknown[1]"></cnsl-copy-row>
</ng-container>
</ng-container>
</cnsl-card>

View File

@ -304,8 +304,9 @@
text-decoration: none;
color: inherit;
font-family: inherit;
&:hover {
text-decoration: underline;
}
text-decoration: underline;
}
.download-button {
margin-left: 1rem;
}

View File

@ -8,7 +8,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Buffer } from 'buffer';
import { Duration } from 'google-protobuf/google/protobuf/duration_pb';
import { Subject, Subscription } from 'rxjs';
import { mergeMap, Subject, Subscription } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { RadioItemAuthType } from 'src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component';
import { ChangeType } from 'src/app/modules/changes/changes.component';
@ -80,30 +80,27 @@ export class AppDetailComponent implements OnInit, OnDestroy {
public appId: string = '';
public app?: App.AsObject;
public environmentMap$ = this.envSvc.env.pipe(
map((env) => {
return {
issuer: env.issuer,
adminServiceUrl: `${env.api}/admin/v1`,
mgmtServiceUrl: `${env.api}/management/v1`,
authServiceUrl: `${env.api}/auth/v1`,
samlCertificateURL: `${env.issuer}/saml/v2/certificate`,
samlSSO: `${env.issuer}/saml/v2/SSO`,
samlSLO: `${env.issuer}/saml/v2/SLO`,
};
}),
public apiURLs$ = this.envSvc.env.pipe(
mergeMap((env) =>
this.wellknownURLs$.pipe(
map((wellknown) => {
return [
['Issuer', env.issuer],
['Admin Service URL', `${env.api}/admin/v1`],
['Management Service URL', `${env.api}/management/v1`],
['Auth Service URL', `${env.api}/auth/v1`],
...wellknown.filter(
([k, v]) => k === 'Revocation Endpoint' || k === 'JKWS URI' || k === 'Introspection Endpoint',
),
];
}),
),
),
);
public apiMap$ = this.envSvc.env.pipe(
map((env) => {
return {
issuer: env.issuer,
adminServiceUrl: `${env.api}/admin/v1`,
mgmtServiceUrl: `${env.api}/management/v1`,
authServiceUrl: `${env.api}/auth/v1`,
};
}),
);
public samlMap$ = this.envSvc.env.pipe(
public issuer$ = this.apiURLs$.pipe(map((urls) => urls.find(([k, v]) => k === 'Issuer')?.[1]));
public samlURLs$ = this.envSvc.env.pipe(
map((env) => {
return {
samlCertificateURL: `${env.issuer}/saml/v2/certificate`,
@ -112,7 +109,21 @@ export class AppDetailComponent implements OnInit, OnDestroy {
};
}),
);
public wellknownMap$ = this.envSvc.wellknown;
public wellknownURLs$ = this.envSvc.wellknown.pipe(
map((wellknown) => {
return [
['Authorization Endpoint', wellknown.authorization_endpoint],
['Device Authorization Endpoint', wellknown.device_authorization_endpoint],
['End Session Endpoint', wellknown.end_session_endpoint],
['Introspection Endpoint', wellknown.introspection_endpoint],
['JKWS URI', wellknown.jwks_uri],
['Revocation Endpoint', wellknown.revocation_endpoint],
['Token Endpoint', wellknown.token_endpoint],
['Userinfo Endpoint', wellknown.userinfo_endpoint],
];
}),
);
public oidcResponseTypes: OIDCResponseType[] = [
OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
@ -217,9 +228,6 @@ export class AppDetailComponent implements OnInit, OnDestroy {
if (!this.app) {
this.app = new App().toObject();
}
if (!this.app.samlConfig) {
this.app.samlConfig = new SAMLConfig().toObject();
}
let minimalMetadata =
this.entityId?.value && this.acsURL?.value
@ -231,6 +239,14 @@ export class AppDetailComponent implements OnInit, OnDestroy {
</md:EntityDescriptor>`
: '';
if (!minimalMetadata && !this.metadataUrl?.value) {
return;
}
if (!this.app.samlConfig) {
this.app.samlConfig = new SAMLConfig().toObject();
}
if (minimalMetadata) {
const base64 = Buffer.from(minimalMetadata, 'utf-8').toString('base64');
this.app.samlConfig.metadataXml = base64;

View File

@ -43,7 +43,7 @@
</div>
<ng-container *ngIf="data.type === EditDialogType.EMAIL && data.isVerifiedTextKey">
<mat-checkbox class="verified-checkbox" [(ngModel)]="isVerified">
<mat-checkbox class="verified-checkbox" [(ngModel)]="isVerified" [ngModelOptions]="{ standalone: true }">
{{ data.isVerifiedTextKey | translate }}
</mat-checkbox>
<cnsl-info-section class="full-width desc">

View File

@ -18,10 +18,13 @@ export interface Environment {
interface WellKnown {
authorization_endpoint: string;
device_authorization_endpoint: string;
end_session_endpoint: string;
introspection_endpoint: string;
token_endpoint: string;
userinfo_endpoint: string;
jwks_uri: string;
revocation_endpoint: string;
}
@Injectable({
providedIn: 'root',

View File

@ -1994,7 +1994,7 @@
},
"CREATE": {
"TITLE": "Add provider",
"DESCRIPTION": "Select one ore more of the following providers.",
"DESCRIPTION": "Select one or more of the following providers.",
"STEPPERTITLE": "Create Provider",
"OIDC": {
"TITLE": "OIDC Provider",
@ -2264,7 +2264,7 @@
},
"CREATE": {
"TITLE": "Add SMTP provider",
"DESCRIPTION": "Select one ore more of the following providers.",
"DESCRIPTION": "Select one or more of the following providers.",
"STEPS": {
"TITLE": "Add {{ value }} SMTP Provider",
"CREATE_DESC_TITLE": "Enter your {{ value }} SMTP settings step by step",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -80,6 +80,9 @@ ZITADEL updates stored hashes when the configured algorithm or its parameters ar
the first time verification succeeds.
This allows to increase cost along with growing computing power.
ZITADEL allows to import user passwords from systems that use any of the above hashing algorithms.
Note however that by default, only `bcrypt` is enabled.
Further `Verifiers` must be enabled in the [configuration](/self-hosting/manage/configure) by the system administrator.
:::
### Encrypted Secrets

View File

@ -2,7 +2,7 @@
Log:
Level: 'info'
# Make ZITADEL accessible over HTTP, not HTTPS
# Make ZITADEL accessible over HTTPs, not HTTP
ExternalSecure: true
ExternalDomain: my.domain
ExternalPort: 443

View File

@ -10,7 +10,7 @@ import (
func (s *Server) UpdateMyPassword(ctx context.Context, req *auth_pb.UpdateMyPasswordRequest) (*auth_pb.UpdateMyPasswordResponse, error) {
ctxData := authz.GetCtxData(ctx)
objectDetails, err := s.command.ChangePassword(ctx, ctxData.ResourceOwner, ctxData.UserID, req.OldPassword, req.NewPassword, "")
objectDetails, err := s.command.ChangePassword(ctx, ctxData.ResourceOwner, ctxData.UserID, req.OldPassword, req.NewPassword, "", false)
if err != nil {
return nil, err
}

View File

@ -51,9 +51,9 @@ func (s *Server) SetPassword(ctx context.Context, req *user.SetPasswordRequest)
switch v := req.GetVerification().(type) {
case *user.SetPasswordRequest_CurrentPassword:
details, err = s.command.ChangePassword(ctx, "", req.GetUserId(), v.CurrentPassword, req.GetNewPassword().GetPassword(), "")
details, err = s.command.ChangePassword(ctx, "", req.GetUserId(), v.CurrentPassword, req.GetNewPassword().GetPassword(), "", req.GetNewPassword().GetChangeRequired())
case *user.SetPasswordRequest_VerificationCode:
details, err = s.command.SetPasswordWithVerifyCode(ctx, "", req.GetUserId(), v.VerificationCode, req.GetNewPassword().GetPassword(), "")
details, err = s.command.SetPasswordWithVerifyCode(ctx, "", req.GetUserId(), v.VerificationCode, req.GetNewPassword().GetPassword(), "", req.GetNewPassword().GetChangeRequired())
case nil:
details, err = s.command.SetPassword(ctx, "", req.GetUserId(), req.GetNewPassword().GetPassword(), req.GetNewPassword().GetChangeRequired())
default:

View File

@ -249,33 +249,12 @@ func SetHumanPasswordToPassword(password *user.SetPassword) *command.Password {
if password == nil {
return nil
}
var changeRequired bool
var passwordStr *string
if password.GetPassword() != nil {
passwordStr = &password.GetPassword().Password
changeRequired = password.GetPassword().GetChangeRequired()
}
var hash *string
if password.GetHashedPassword() != nil {
hash = &password.GetHashedPassword().Hash
changeRequired = password.GetHashedPassword().GetChangeRequired()
}
var code *string
if password.GetVerificationCode() != "" {
codeT := password.GetVerificationCode()
code = &codeT
}
var oldPassword *string
if password.GetCurrentPassword() != "" {
oldPasswordT := password.GetCurrentPassword()
oldPassword = &oldPasswordT
}
return &command.Password{
PasswordCode: code,
OldPassword: oldPassword,
Password: passwordStr,
EncodedPasswordHash: hash,
ChangeRequired: changeRequired,
PasswordCode: password.GetVerificationCode(),
OldPassword: password.GetCurrentPassword(),
Password: password.GetPassword().GetPassword(),
EncodedPasswordHash: password.GetHashedPassword().GetHash(),
ChangeRequired: password.GetPassword().GetChangeRequired() || password.GetHashedPassword().GetChangeRequired(),
}
}

View File

@ -449,6 +449,40 @@ func TestServer_AddHumanUser(t *testing.T) {
},
},
},
{
name: "password not complexity conform",
args: args{
CTX,
&user.AddHumanUserRequest{
Organization: &object.Organization{
Org: &object.Organization_OrgId{
OrgId: Tester.Organisation.ID,
},
},
Profile: &user.SetHumanProfile{
GivenName: "Donald",
FamilyName: "Duck",
NickName: gu.Ptr("Dukkie"),
DisplayName: gu.Ptr("Donald Duck"),
PreferredLanguage: gu.Ptr("en"),
Gender: user.Gender_GENDER_DIVERSE.Enum(),
},
Email: &user.SetHumanEmail{},
Metadata: []*user.SetMetadataEntry{
{
Key: "somekey",
Value: []byte("somevalue"),
},
},
PasswordType: &user.AddHumanUserRequest_Password{
Password: &user.Password{
Password: "insufficient",
},
},
},
},
wantErr: true,
},
{
name: "hashed password",
args: args{

View File

@ -26,7 +26,7 @@ func (l *Login) handleChangePassword(w http.ResponseWriter, r *http.Request) {
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
_, err = l.command.ChangePassword(setContext(r.Context(), authReq.UserOrgID), authReq.UserOrgID, authReq.UserID, data.OldPassword, data.NewPassword, userAgentID)
_, err = l.command.ChangePassword(setContext(r.Context(), authReq.UserOrgID), authReq.UserOrgID, authReq.UserID, data.OldPassword, data.NewPassword, userAgentID, false)
if err != nil {
l.renderChangePassword(w, r, authReq, err)
return

View File

@ -80,7 +80,7 @@ func (l *Login) checkPWCode(w http.ResponseWriter, r *http.Request, authReq *dom
userOrg = authReq.UserOrgID
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
_, err := l.command.SetPasswordWithVerifyCode(setContext(r.Context(), userOrg), userOrg, data.UserID, data.Code, data.Password, userAgentID)
_, err := l.command.SetPasswordWithVerifyCode(setContext(r.Context(), userOrg), userOrg, data.UserID, data.Code, data.Password, userAgentID, false)
if err != nil {
l.renderInitPassword(w, r, authReq, data.UserID, "", err)
return

View File

@ -104,16 +104,17 @@ func (l *Login) checkUserInitCode(w http.ResponseWriter, r *http.Request, authRe
}
func (l *Login) resendUserInit(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID string, loginName string, showPassword bool) {
userOrgID := ""
var userOrgID, authRequestID string
if authReq != nil {
userOrgID = authReq.UserOrgID
authRequestID = authReq.ID
}
initCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeInitCode, l.userCodeAlg)
if err != nil {
l.renderInitUser(w, r, authReq, userID, loginName, "", showPassword, err)
return
}
_, err = l.command.ResendInitialMail(setContext(r.Context(), userOrgID), userID, "", userOrgID, initCodeGenerator, authReq.ID)
_, err = l.command.ResendInitialMail(setContext(r.Context(), userOrgID), userID, "", userOrgID, initCodeGenerator, authRequestID)
l.renderInitUser(w, r, authReq, userID, loginName, "", showPassword, err)
}

View File

@ -76,7 +76,7 @@ func (l *Login) renderOTPVerification(w http.ResponseWriter, r *http.Request, au
func (l *Login) handleOTPVerificationCheck(w http.ResponseWriter, r *http.Request) {
formData := new(mfaOTPFormData)
authReq, err := l.getAuthRequestAndParseData(r, formData)
if err != nil {
if authReq == nil || err != nil {
l.renderError(w, r, authReq, err)
return
}

View File

@ -689,16 +689,17 @@ func AddHumanFromDomain(user *domain.Human, metadataList []*domain.Metadata, aut
human.DisplayName = user.DisplayName
human.PreferredLanguage = user.PreferredLanguage
human.Gender = user.Gender
human.Password = user.Password.SecretString
human.Register = true
human.Metadata = addMetadata
}
if authRequest != nil {
human.UserAgentID = authRequest.AgentID
human.AuthRequestID = authRequest.ID
}
if user.Email != nil {
human.Email = Email{
Address: user.EmailAddress,
Verified: user.IsEmailVerified,
Address: user.Email.EmailAddress,
Verified: user.Email.IsEmailVerified,
}
}
if user.Phone != nil {
@ -707,6 +708,9 @@ func AddHumanFromDomain(user *domain.Human, metadataList []*domain.Metadata, aut
Verified: user.Phone.IsPhoneVerified,
}
}
if user.Password != nil {
human.Password = user.Password.SecretString
}
if idp != nil {
human.Links = []*AddLink{
{

View File

@ -83,7 +83,7 @@ func (c *Commands) HumanVerifyInitCode(ctx context.Context, userID, resourceOwne
commands = append(commands, user.NewHumanEmailVerifiedEvent(ctx, userAgg))
}
if password != "" {
passwordCommand, err := c.setPasswordCommand(ctx, userAgg, domain.UserStateActive, password, userAgentID, false, false)
passwordCommand, err := c.setPasswordCommand(ctx, userAgg, domain.UserStateActive, password, "", userAgentID, false, nil)
if err != nil {
return err
}

View File

@ -3,6 +3,7 @@ package command
import (
"context"
"errors"
"time"
"github.com/zitadel/logging"
"github.com/zitadel/passwap"
@ -25,16 +26,18 @@ func (c *Commands) SetPassword(ctx context.Context, orgID, userID, password stri
if err != nil {
return nil, err
}
if !wm.UserState.Exists() {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-3M0fs", "Errors.User.NotFound")
}
if err = c.checkPermission(ctx, domain.PermissionUserWrite, wm.ResourceOwner, userID); err != nil {
return nil, err
}
return c.setPassword(ctx, wm, password, "", oneTime)
return c.setPassword(
ctx,
wm,
password,
"", // current api implementations never provide an encoded password
"",
oneTime,
c.setPasswordWithPermission(wm.AggregateID, wm.ResourceOwner),
)
}
func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID, code, password, userAgentID string) (objectDetails *domain.ObjectDetails, err error) {
func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID, code, password, userAgentID string, changeRequired bool) (objectDetails *domain.ObjectDetails, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@ -48,66 +51,19 @@ func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID,
if err != nil {
return nil, err
}
if wm.Code == nil || wm.UserState == domain.UserStateUnspecified || wm.UserState == domain.UserStateDeleted {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-2M9fs", "Errors.User.Code.NotFound")
}
err = crypto.VerifyCode(wm.CodeCreationDate, wm.CodeExpiry, wm.Code, code, c.userEncryption)
if err != nil {
return nil, err
}
return c.setPassword(ctx, wm, password, userAgentID, false)
}
// setEncodedPassword add change event from already encoded password to HumanPasswordWriteModel and return the necessary object details for response
func (c *Commands) setEncodedPassword(ctx context.Context, wm *HumanPasswordWriteModel, password, userAgentID string, changeRequired bool) (objectDetails *domain.ObjectDetails, err error) {
agg := user.NewAggregate(wm.AggregateID, wm.ResourceOwner)
command, err := c.setPasswordCommand(ctx, &agg.Aggregate, wm.UserState, password, userAgentID, changeRequired, true)
if err != nil {
return nil, err
}
err = c.pushAppendAndReduce(ctx, wm, command)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&wm.WriteModel), nil
}
// setPassword add change event to HumanPasswordWriteModel and return the necessary object details for response
func (c *Commands) setPassword(ctx context.Context, wm *HumanPasswordWriteModel, password, userAgentID string, changeRequired bool) (objectDetails *domain.ObjectDetails, err error) {
agg := user.NewAggregate(wm.AggregateID, wm.ResourceOwner)
command, err := c.setPasswordCommand(ctx, &agg.Aggregate, wm.UserState, password, userAgentID, changeRequired, false)
if err != nil {
return nil, err
}
err = c.pushAppendAndReduce(ctx, wm, command)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&wm.WriteModel), nil
}
func (c *Commands) setPasswordCommand(ctx context.Context, agg *eventstore.Aggregate, userState domain.UserState, password, userAgentID string, changeRequired, encoded bool) (_ eventstore.Command, err error) {
if err = c.canUpdatePassword(ctx, password, agg.ResourceOwner, userState); err != nil {
return nil, err
}
if !encoded {
ctx, span := tracing.NewNamedSpan(ctx, "passwap.Hash")
encodedPassword, err := c.userPasswordHasher.Hash(password)
span.EndWithError(err)
if err = convertPasswapErr(err); err != nil {
return nil, err
}
return user.NewHumanPasswordChangedEvent(ctx, agg, encodedPassword, changeRequired, userAgentID), nil
}
return user.NewHumanPasswordChangedEvent(ctx, agg, password, changeRequired, userAgentID), nil
return c.setPassword(
ctx,
wm,
password,
"",
userAgentID,
changeRequired,
c.setPasswordWithVerifyCode(wm.CodeCreationDate, wm.CodeExpiry, wm.Code, code),
)
}
// ChangePassword change password of existing user
func (c *Commands) ChangePassword(ctx context.Context, orgID, userID, oldPassword, newPassword, userAgentID string) (objectDetails *domain.ObjectDetails, err error) {
func (c *Commands) ChangePassword(ctx context.Context, orgID, userID, oldPassword, newPassword, userAgentID string, changeRequired bool) (objectDetails *domain.ObjectDetails, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@ -121,12 +77,125 @@ func (c *Commands) ChangePassword(ctx context.Context, orgID, userID, oldPasswor
if err != nil {
return nil, err
}
return c.setPassword(
ctx,
wm,
newPassword,
"",
userAgentID,
changeRequired,
c.checkCurrentPassword(newPassword, "", oldPassword, wm.EncodedHash),
)
}
newPasswordHash, err := c.verifyAndUpdatePassword(ctx, wm.EncodedHash, oldPassword, newPassword)
type setPasswordVerification func(ctx context.Context) (newEncodedPassword string, err error)
// setPasswordWithPermission returns a permission check as [setPasswordVerification] implementation
func (c *Commands) setPasswordWithPermission(userID, orgID string) setPasswordVerification {
return func(ctx context.Context) (_ string, err error) {
return "", c.checkPermission(ctx, domain.PermissionUserWrite, orgID, userID)
}
}
// setPasswordWithVerifyCode returns a password code check as [setPasswordVerification] implementation
func (c *Commands) setPasswordWithVerifyCode(
passwordCodeCreationDate time.Time,
passwordCodeExpiry time.Duration,
passwordCode *crypto.CryptoValue,
code string,
) setPasswordVerification {
return func(ctx context.Context) (_ string, err error) {
if passwordCode == nil {
return "", zerrors.ThrowPreconditionFailed(nil, "COMMAND-2M9fs", "Errors.User.Code.NotFound")
}
_, spanCrypto := tracing.NewNamedSpan(ctx, "crypto.VerifyCode")
defer func() {
spanCrypto.EndWithError(err)
}()
return "", crypto.VerifyCode(passwordCodeCreationDate, passwordCodeExpiry, passwordCode, code, c.userEncryption)
}
}
// checkCurrentPassword returns a password check as [setPasswordVerification] implementation
func (c *Commands) checkCurrentPassword(
newPassword, newEncodedPassword, currentPassword, currentEncodePassword string,
) setPasswordVerification {
// in case the new password is already encoded, we only need to verify the current
if newEncodedPassword != "" {
return func(ctx context.Context) (_ string, err error) {
_, spanPasswap := tracing.NewNamedSpan(ctx, "passwap.Verify")
_, err = c.userPasswordHasher.Verify(currentEncodePassword, currentPassword)
spanPasswap.EndWithError(err)
return "", convertPasswapErr(err)
}
}
// otherwise let's directly verify and return the new generate hash, so we can reuse it in the event
return func(ctx context.Context) (string, error) {
return c.verifyAndUpdatePassword(ctx, currentEncodePassword, currentPassword, newPassword)
}
}
// setPassword directly pushes the intent of [setPasswordCommand] to the eventstore and returns the [domain.ObjectDetails]
func (c *Commands) setPassword(
ctx context.Context,
wm *HumanPasswordWriteModel,
password, encodedPassword, userAgentID string,
changeRequired bool,
verificationCheck setPasswordVerification,
) (*domain.ObjectDetails, error) {
agg := user.NewAggregate(wm.AggregateID, wm.ResourceOwner)
command, err := c.setPasswordCommand(ctx, &agg.Aggregate, wm.UserState, password, encodedPassword, userAgentID, changeRequired, verificationCheck)
if err != nil {
return nil, err
}
return c.setEncodedPassword(ctx, wm, newPasswordHash, userAgentID, false)
err = c.pushAppendAndReduce(ctx, wm, command)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&wm.WriteModel), nil
}
// setPasswordCommand creates the command / intent for changing a user's password.
// It will check the user's [domain.UserState] to be existing and not initial,
// if the caller is allowed to change the password (permission, by code or by providing the current password),
// and it will ensure the new password (if provided as plain) corresponds to the password complexity policy.
// If not already encoded, the new password will be hashed.
func (c *Commands) setPasswordCommand(ctx context.Context, agg *eventstore.Aggregate, userState domain.UserState, password, encodedPassword, userAgentID string, changeRequired bool, verificationCheck setPasswordVerification) (_ eventstore.Command, err error) {
if !isUserStateExists(userState) {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-G8dh3", "Errors.User.Password.NotFound")
}
if isUserStateInitial(userState) {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-M9dse", "Errors.User.NotInitialised")
}
if verificationCheck != nil {
newEncodedPassword, err := verificationCheck(ctx)
if err != nil {
return nil, err
}
// use the new hash from the verification in case there is one (e.g. existing pw check)
if newEncodedPassword != "" {
encodedPassword = newEncodedPassword
}
}
// If password is provided, let's check if is compliant with the policy.
// If only a encodedPassword is passed, we can skip this.
if password != "" {
if err = c.checkPasswordComplexity(ctx, password, agg.ResourceOwner); err != nil {
return nil, err
}
}
// In case only a plain password was passed, we need to hash it.
if encodedPassword == "" {
_, span := tracing.NewNamedSpan(ctx, "passwap.Hash")
encodedPassword, err = c.userPasswordHasher.Hash(password)
span.EndWithError(err)
if err = convertPasswapErr(err); err != nil {
return nil, err
}
}
return user.NewHumanPasswordChangedEvent(ctx, agg, encodedPassword, changeRequired, userAgentID), nil
}
// verifyAndUpdatePassword verify if the old password is correct with the encoded hash and
@ -142,17 +211,11 @@ func (c *Commands) verifyAndUpdatePassword(ctx context.Context, encodedHash, old
return updated, convertPasswapErr(err)
}
// canUpdatePassword checks uf the given password can be used to be the password of a user
func (c *Commands) canUpdatePassword(ctx context.Context, newPassword string, resourceOwner string, state domain.UserState) (err error) {
// checkPasswordComplexity checks uf the given password can be used to be the password of a user
func (c *Commands) checkPasswordComplexity(ctx context.Context, newPassword string, resourceOwner string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if !isUserStateExists(state) {
return zerrors.ThrowNotFound(nil, "COMMAND-G8dh3", "Errors.User.Password.NotFound")
}
if state == domain.UserStateInitial {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-M9dse", "Errors.User.NotInitialised")
}
policy, err := c.getOrgPasswordComplexityPolicy(ctx, resourceOwner)
if err != nil {
return err

View File

@ -267,12 +267,13 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
userPasswordHasher *crypto.Hasher
}
type args struct {
ctx context.Context
userID string
code string
resourceOwner string
password string
userAgentID string
ctx context.Context
userID string
code string
resourceOwner string
password string
userAgentID string
changeRequired bool
}
type res struct {
want *domain.ObjectDetails
@ -562,6 +563,84 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
},
},
},
{
name: "set password with changeRequired, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"nickname",
"displayname",
language.German,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
eventFromEventPusher(
user.NewHumanEmailVerifiedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
),
),
eventFromEventPusherWithCreationDateNow(
user.NewHumanPasswordCodeAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("a"),
},
time.Hour*1,
domain.NotificationTypeEmail,
"",
),
),
),
expectFilter(
eventFromEventPusher(
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
1,
false,
false,
false,
false,
),
),
),
expectPush(
user.NewHumanPasswordChangedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"$plain$x$password",
true,
"",
),
),
),
userPasswordHasher: mockPasswordHasher("x"),
userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: context.Background(),
userID: "user1",
resourceOwner: "org1",
password: "password",
code: "a",
userAgentID: "",
changeRequired: true,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -570,7 +649,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
userPasswordHasher: tt.fields.userPasswordHasher,
userEncryption: tt.fields.userEncryption,
}
got, err := r.SetPasswordWithVerifyCode(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.code, tt.args.password, tt.args.userAgentID)
got, err := r.SetPasswordWithVerifyCode(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.code, tt.args.password, tt.args.userAgentID, tt.args.changeRequired)
if tt.res.err == nil {
assert.NoError(t, err)
}
@ -589,12 +668,13 @@ func TestCommandSide_ChangePassword(t *testing.T) {
userPasswordHasher *crypto.Hasher
}
type args struct {
ctx context.Context
userID string
resourceOwner string
oldPassword string
newPassword string
userAgentID string
ctx context.Context
userID string
resourceOwner string
oldPassword string
newPassword string
userAgentID string
changeRequired bool
}
type res struct {
want *domain.ObjectDetails
@ -700,6 +780,64 @@ func TestCommandSide_ChangePassword(t *testing.T) {
err: zerrors.IsPreconditionFailed,
},
},
{
name: "password not matching complexity policy, invalid argument error",
fields: fields{
userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
userID: "user1",
oldPassword: "password-old",
newPassword: "password1",
resourceOwner: "org1",
},
expect: []expect{
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"nickname",
"displayname",
language.German,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
eventFromEventPusher(
user.NewHumanEmailVerifiedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
),
),
eventFromEventPusher(
user.NewHumanPasswordChangedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"$plain$x$password-old",
false,
"")),
),
expectFilter(
eventFromEventPusher(
org.NewPasswordComplexityPolicyAddedEvent(
context.Background(),
&org.NewAggregate("org1").Aggregate,
1,
true,
true,
true,
true,
),
),
),
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "password not matching, invalid argument error",
fields: fields{
@ -788,7 +926,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
expectFilter(
eventFromEventPusher(
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
&org.NewAggregate("org1").Aggregate,
1,
false,
false,
@ -880,6 +1018,75 @@ func TestCommandSide_ChangePassword(t *testing.T) {
},
},
},
{
name: "change password with changeRequired, ok",
fields: fields{
userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
userID: "user1",
resourceOwner: "org1",
oldPassword: "password",
newPassword: "password1",
userAgentID: "",
changeRequired: true,
},
expect: []expect{
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"nickname",
"displayname",
language.German,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
eventFromEventPusher(
user.NewHumanEmailVerifiedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
),
),
eventFromEventPusher(
user.NewHumanPasswordChangedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"$plain$x$password",
false,
"")),
),
expectFilter(
eventFromEventPusher(
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
1,
false,
false,
false,
false,
),
),
),
expectPush(
user.NewHumanPasswordChangedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"$plain$x$password1",
true,
"",
),
),
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -887,7 +1094,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
eventstore: eventstoreExpect(t, tt.expect...),
userPasswordHasher: tt.fields.userPasswordHasher,
}
got, err := r.ChangePassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.oldPassword, tt.args.newPassword, tt.args.userAgentID)
got, err := r.ChangePassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.oldPassword, tt.args.newPassword, tt.args.userAgentID, tt.args.changeRequired)
if tt.res.err == nil {
assert.NoError(t, err)
}

View File

@ -43,10 +43,10 @@ type Profile struct {
type Password struct {
// Either you have to have permission, a password code or the old password to change
PasswordCode *string
OldPassword *string
Password *string
EncodedPasswordHash *string
PasswordCode string
OldPassword string
Password string
EncodedPasswordHash string
ChangeRequired bool
}
@ -73,12 +73,12 @@ func (h *ChangeHuman) Validate(hasher *crypto.Hasher) (err error) {
}
func (p *Password) Validate(hasher *crypto.Hasher) error {
if p.EncodedPasswordHash != nil {
if !hasher.EncodingSupported(*p.EncodedPasswordHash) {
if p.EncodedPasswordHash != "" {
if !hasher.EncodingSupported(p.EncodedPasswordHash) {
return zerrors.ThrowInvalidArgument(nil, "USER-oz74onzvqr", "Errors.User.Password.NotSupported")
}
}
if p.Password == nil && p.EncodedPasswordHash == nil {
if p.Password == "" && p.EncodedPasswordHash == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-3klek4sbns", "Errors.User.Password.Empty")
}
return nil
@ -285,7 +285,7 @@ func (c *Commands) ChangeUserHuman(ctx context.Context, human *ChangeHuman, alg
}
}
if human.Password != nil {
cmds, err = c.changeUserPassword(ctx, cmds, existingHuman, human.Password, alg)
cmds, err = c.changeUserPassword(ctx, cmds, existingHuman, human.Password)
if err != nil {
return err
}
@ -370,57 +370,34 @@ func changeUserProfile(ctx context.Context, cmds []eventstore.Command, wm *UserV
return cmds, err
}
func (c *Commands) changeUserPassword(ctx context.Context, cmds []eventstore.Command, wm *UserV2WriteModel, password *Password, alg crypto.EncryptionAlgorithm) ([]eventstore.Command, error) {
func (c *Commands) changeUserPassword(ctx context.Context, cmds []eventstore.Command, wm *UserV2WriteModel, password *Password) ([]eventstore.Command, error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
// Either have a code to set the password
if password.PasswordCode != nil {
if err := crypto.VerifyCode(wm.PasswordCodeCreationDate, wm.PasswordCodeExpiry, wm.PasswordCode, *password.PasswordCode, alg); err != nil {
return cmds, err
}
// if no verification is set, the user must have the permission to change the password
verification := c.setPasswordWithPermission(wm.AggregateID, wm.ResourceOwner)
// otherwise check the password code...
if password.PasswordCode != "" {
verification = c.setPasswordWithVerifyCode(wm.PasswordCodeCreationDate, wm.PasswordCodeExpiry, wm.PasswordCode, password.PasswordCode)
}
var encodedPassword string
// or have the old password to change it
if password.OldPassword != nil {
// newly encode old password if no new and already encoded password is set
pw := *password.OldPassword
if password.Password != nil {
pw = *password.Password
}
alreadyEncodedPassword, err := c.verifyAndUpdatePassword(ctx, wm.PasswordEncodedHash, *password.OldPassword, pw)
if err != nil {
return cmds, err
}
encodedPassword = alreadyEncodedPassword
// ...or old password
if password.OldPassword != "" {
verification = c.checkCurrentPassword(password.Password, password.EncodedPasswordHash, password.OldPassword, wm.PasswordEncodedHash)
}
// password already hashed in request
if password.EncodedPasswordHash != nil {
cmd, err := c.setPasswordCommand(ctx, &wm.Aggregate().Aggregate, wm.UserState, *password.EncodedPasswordHash, "", password.ChangeRequired, true)
if cmd != nil {
return append(cmds, cmd), err
}
return cmds, err
cmd, err := c.setPasswordCommand(
ctx,
&wm.Aggregate().Aggregate,
wm.UserState,
password.Password,
password.EncodedPasswordHash,
"",
password.ChangeRequired,
verification,
)
if cmd != nil {
return append(cmds, cmd), err
}
// password already hashed in verify
if encodedPassword != "" {
cmd, err := c.setPasswordCommand(ctx, &wm.Aggregate().Aggregate, wm.UserState, encodedPassword, "", password.ChangeRequired, true)
if cmd != nil {
return append(cmds, cmd), err
}
return cmds, err
}
// password still to be hashed
if password.Password != nil {
cmd, err := c.setPasswordCommand(ctx, &wm.Aggregate().Aggregate, wm.UserState, *password.Password, "", password.ChangeRequired, false)
if cmd != nil {
return append(cmds, cmd), err
}
return cmds, err
}
// no password changes necessary
return cmds, nil
return cmds, err
}
func (c *Commands) userExistsWriteModel(ctx context.Context, userID string) (writeModel *UserV2WriteModel, err error) {

View File

@ -2014,8 +2014,8 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
orgID: "org1",
human: &ChangeHuman{
Password: &Password{
Password: gu.Ptr("password2"),
OldPassword: gu.Ptr("password"),
Password: "password2",
OldPassword: "password",
ChangeRequired: true,
},
},
@ -2061,8 +2061,8 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
orgID: "org1",
human: &ChangeHuman{
Password: &Password{
Password: gu.Ptr("password2"),
OldPassword: gu.Ptr("password"),
Password: "password2",
OldPassword: "password",
ChangeRequired: true,
},
},
@ -2085,7 +2085,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
orgID: "org1",
human: &ChangeHuman{
Password: &Password{
OldPassword: gu.Ptr("password"),
OldPassword: "password",
ChangeRequired: true,
},
},
@ -2119,7 +2119,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
orgID: "org1",
human: &ChangeHuman{
Password: &Password{
Password: gu.Ptr("password2"),
Password: "password2",
ChangeRequired: true,
},
},
@ -2173,7 +2173,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
orgID: "org1",
human: &ChangeHuman{
Password: &Password{
Password: gu.Ptr("password2"),
Password: "password2",
ChangeRequired: true,
},
},
@ -2229,8 +2229,8 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
orgID: "org1",
human: &ChangeHuman{
Password: &Password{
Password: gu.Ptr("password2"),
OldPassword: gu.Ptr("password"),
Password: "password2",
OldPassword: "password",
ChangeRequired: true,
},
},
@ -2266,8 +2266,8 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
orgID: "org1",
human: &ChangeHuman{
Password: &Password{
Password: gu.Ptr("password2"),
OldPassword: gu.Ptr("wrong"),
Password: "password2",
OldPassword: "wrong",
ChangeRequired: true,
},
},
@ -2336,8 +2336,8 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
orgID: "org1",
human: &ChangeHuman{
Password: &Password{
Password: gu.Ptr("password2"),
PasswordCode: gu.Ptr("code"),
Password: "password2",
PasswordCode: "code",
ChangeRequired: true,
},
},
@ -2389,8 +2389,8 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
orgID: "org1",
human: &ChangeHuman{
Password: &Password{
Password: gu.Ptr("password2"),
PasswordCode: gu.Ptr("wrong"),
Password: "password2",
PasswordCode: "wrong",
ChangeRequired: true,
},
},
@ -2403,7 +2403,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
},
},
{
name: "change human password encoded, password code, ok",
name: "change human password, password code, not matching policy",
fields: fields{
eventstore: expectEventstore(
expectFilter(
@ -2436,9 +2436,58 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
1,
false,
false,
false,
true,
true,
true,
true,
),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
orgID: "org1",
human: &ChangeHuman{
Password: &Password{
Password: "password2",
PasswordCode: "code",
ChangeRequired: true,
},
},
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "change human password encoded, password code, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
newAddHumanEvent("$plain$x$password", true, true, "", language.English),
),
eventFromEventPusher(
user.NewHumanInitializedCheckSucceededEvent(context.Background(),
&userAgg.Aggregate,
),
),
eventFromEventPusherWithCreationDateNow(
user.NewHumanPasswordCodeAddedEventV2(context.Background(),
&userAgg.Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("code"),
},
time.Hour*1,
domain.NotificationTypeEmail,
"",
false,
),
),
@ -2460,8 +2509,8 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
orgID: "org1",
human: &ChangeHuman{
Password: &Password{
EncodedPasswordHash: gu.Ptr("$plain$x$password2"),
PasswordCode: gu.Ptr("code"),
EncodedPasswordHash: "$plain$x$password2",
PasswordCode: "code",
ChangeRequired: true,
},
},
@ -2533,9 +2582,9 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
orgID: "org1",
human: &ChangeHuman{
Password: &Password{
Password: gu.Ptr("passwordnotused"),
EncodedPasswordHash: gu.Ptr("$plain$x$password2"),
PasswordCode: gu.Ptr("code"),
Password: "passwordnotused",
EncodedPasswordHash: "$plain$x$password2",
PasswordCode: "code",
ChangeRequired: true,
},
},
@ -2557,6 +2606,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
userPasswordHasher: tt.fields.userPasswordHasher,
newEncryptedCode: tt.fields.newCode,
checkPermission: tt.fields.checkPermission,
userEncryption: tt.args.codeAlg,
}
err := r.ChangeUserHuman(tt.args.ctx, tt.args.human, tt.args.codeAlg)
if tt.res.err == nil {

View File

@ -152,10 +152,7 @@ func (smtpConfig SMTP) smtpAuth(client *smtp.Client, host string) error {
return nil
}
// Auth
auth := unencryptedAuth{
smtp.PlainAuth("", smtpConfig.User, smtpConfig.Password, host),
}
err := client.Auth(auth)
err := client.Auth(PlainOrLoginAuth(smtpConfig.User, smtpConfig.Password, host))
if err != nil {
return zerrors.ThrowInternalf(err, "EMAIL-s9kfs", "could not add smtp auth for user %s", smtpConfig.User)
}

View File

@ -1,22 +0,0 @@
package smtp
import (
"net/smtp"
)
type unencryptedAuth struct {
smtp.Auth
}
// PlainAuth returns an Auth that implements the PLAIN authentication
// mechanism as defined in RFC 4616. The returned Auth uses the given
// username and password to authenticate to host and act as identity.
// Usually identity should be the empty string, to act as username.
//
// This reimplementation allows it to work over non-TLS connections
func (a unencryptedAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
s := *server
s.TLS = true
return a.Auth.Start(&s)
}

View File

@ -0,0 +1,57 @@
package smtp
import (
"bytes"
"net/smtp"
"slices"
"github.com/zitadel/zitadel/internal/zerrors"
)
// golang net/smtp SMTP AUTH LOGIN or PLAIN Auth Handler
// Reference: https://gist.github.com/andelf/5118732?permalink_comment_id=4825669#gistcomment-4825669
func PlainOrLoginAuth(username, password, host string) smtp.Auth {
return &plainOrLoginAuth{username: username, password: password, host: host}
}
type plainOrLoginAuth struct {
username string
password string
host string
authMethod string
}
func (a *plainOrLoginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
if server.Name != a.host {
return "", nil, zerrors.ThrowInternal(nil, "SMTP-RRi75", "wrong host name")
}
if !slices.Contains(server.Auth, "PLAIN") {
a.authMethod = "LOGIN"
return a.authMethod, nil, nil
} else {
a.authMethod = "PLAIN"
resp := []byte("\x00" + a.username + "\x00" + a.password)
return a.authMethod, resp, nil
}
}
func (a *plainOrLoginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if !more {
return nil, nil
}
if a.authMethod == "PLAIN" {
// We've already sent everything.
return nil, zerrors.ThrowInternal(nil, "SMTP-AAf43", "unexpected server challenge for PLAIN auth method")
}
switch {
case bytes.Equal(fromServer, []byte("Username:")):
return []byte(a.username), nil
case bytes.Equal(fromServer, []byte("Password:")):
return []byte(a.password), nil
default:
return nil, zerrors.ThrowInternal(nil, "SMTP-HjW21", "unexpected server challenge")
}
}

View File

@ -51,9 +51,9 @@ func (msg *Email) GetContent() (string, error) {
}
//default mime-type is html
mime := "MIME-version: 1.0;" + lineBreak + "Content-Type: text/html; charset=\"UTF-8\";" + lineBreak + lineBreak
mime := "MIME-Version: 1.0" + lineBreak + "Content-Type: text/html; charset=\"UTF-8\"" + lineBreak + lineBreak
if !isHTML(msg.Content) {
mime = "MIME-version: 1.0;" + lineBreak + "Content-Type: text/plain; charset=\"UTF-8\";" + lineBreak + lineBreak
mime = "MIME-Version: 1.0" + lineBreak + "Content-Type: text/plain; charset=\"UTF-8\"" + lineBreak + lineBreak
}
subject := "Subject: " + bEncodeSubject(msg.Subject) + lineBreak
message += subject + mime + lineBreak + msg.Content

View File

@ -138,7 +138,7 @@ Errors:
Invalid: Паролата е невалидна
NotSet: Потребителят не е задал парола
NotChanged: Новата парола не може да съвпада с текущата парола
NotSupported: Хеш кодирането на паролата не се поддържа
NotSupported: Хеш кодирането на паролата не се поддържа. Вижте https://zitadel.com/docs/concepts/architecture/secrets#hashed-secrets
PasswordComplexityPolicy:
NotFound: Политиката за парола не е намерена
MinLength: Паролата е твърде кратка
@ -1032,6 +1032,9 @@ EventTypes:
check:
succeeded: Проверката на OIDC Client Secret е успешна
failed: Проверката на OIDC Client Secret е неуспешна
key:
added: Добавен е ключ за приложение OIDC
removed: Отстранен ключ за приложение OIDC
api:
secret:
check:

View File

@ -136,7 +136,7 @@ Errors:
Invalid: Heslo je neplatné
NotSet: Uživatel nenastavil heslo
NotChanged: Nové heslo nesmí být stejné jako současné heslo
NotSupported: Kódování hash hesla není podporováno
NotSupported: Kódování hash hesla není podporováno. Podívejte se na https://zitadel.com/docs/concepts/architecture/secrets#hashed-secrets
PasswordComplexityPolicy:
NotFound: Politika složitosti hesla nenalezena
MinLength: Heslo je příliš krátké
@ -1011,6 +1011,9 @@ EventTypes:
check:
succeeded: Kontrola tajného klíče OIDC klienta byla úspěšná
failed: Kontrola tajného klíče OIDC klienta selhala
key:
added: Přidán klíč k aplikaci OIDC
removed: Odstranění klíče aplikace OIDC
api:
secret:
check:

View File

@ -136,7 +136,7 @@ Errors:
Invalid: Passwort ungültig
NotSet: Benutzer hat kein Passwort gesetzt
NotChanged: Das neue Passwort darf nicht mit deinem aktuellen Passwort übereinstimmen
NotSupported: Passwort-Hash-Kodierung wird nicht unterstützt
NotSupported: Passwort-Hash-Kodierung wird nicht unterstützt. Siehe https://zitadel.com/docs/concepts/architecture/secrets#hashed-secrets
PasswordComplexityPolicy:
NotFound: Passwort Policy konnte nicht gefunden werden
MinLength: Passwort ist zu kurz
@ -1013,6 +1013,9 @@ EventTypes:
check:
succeeded: OIDC Client Secret Validierung erfolgreich
failed: OIDC Client Secret Validierung fehlgeschlagen
key:
added: OIDC App Key wurde hinzugefügt
removed: OIDC App Key wurde gelöscht
api:
secret:
check:

View File

@ -136,7 +136,7 @@ Errors:
Invalid: Password is invalid
NotSet: User has not set a password
NotChanged: New password cannot be the same as your current password
NotSupported: Password hash encoding not supported
NotSupported: Password hash encoding not supported. Check out https://zitadel.com/docs/concepts/architecture/secrets#hashed-secrets
PasswordComplexityPolicy:
NotFound: Password policy not found
MinLength: Password is too short
@ -1013,6 +1013,9 @@ EventTypes:
check:
succeeded: OIDC Client Secret check succeeded
failed: OIDC Client Secret check failed
key:
added: OIDC App Key added
removed: OIDC App Key removed
api:
secret:
check:

View File

@ -136,7 +136,7 @@ Errors:
Invalid: La contraseña no es válida
NotSet: El usuario no ha establecido una contraseña
NotChanged: La nueva contraseña no puede coincidir con la contraseña actual
NotSupported: No se admite la codificación hash de contraseña
NotSupported: No se admite la codificación hash de contraseña. Consulte https://zitadel.com/docs/concepts/architecture/secrets#hashed-secrets
PasswordComplexityPolicy:
NotFound: Política de contraseñas no encontrada
MinLength: La contraseña es demasiado corta
@ -1013,6 +1013,9 @@ EventTypes:
check:
succeeded: Comprobación con éxito del secreto del cliente OIDC
failed: Comprobación fallida del secreto del cliente OIDC
key:
added: OIDC App Key añadida
removed: OIDC App Key eliminada
api:
secret:
check:

View File

@ -136,7 +136,7 @@ Errors:
Invalid: Le mot de passe n'est pas valide
NotSet: L'utilisateur n'a pas défini de mot de passe
NotChanged: Le nouveau mot de passe ne peut pas être le même que votre mot de passe actuel
NotSupported: Encodage de hachage de mot de passe non pris en charge
NotSupported: Encodage de hachage de mot de passe non pris en charge. Consultez https://zitadel.com/docs/concepts/architecture/secrets#hashed-secrets
PasswordComplexityPolicy:
NotFound: Politique de mot de passe non trouvée
MinLength: Le mot de passe est trop court
@ -1008,6 +1008,9 @@ EventTypes:
verified:
check: Vérification du secret du client OIDC réussie
failed: La vérification du secret du client OIDC a échoué
key:
added: Clé d'application de l'OIDC ajoutée
removed: Clé d'application de l'OIDC supprimée
api:
secret:
check:

View File

@ -136,7 +136,7 @@ Errors:
Invalid: La password non è valida
NotSet: L'utente non ha impostato una password
NotChanged: La nuova password non può essere uguale alla password attuale
NotSupported: Codifica hash password non supportata
NotSupported: Codifica hash password non supportata. Consulta https://zitadel.com/docs/concepts/architecture/secrets#hashed-secrets
PasswordComplexityPolicy:
NotFound: Impostazioni di complessità password non trovati
MinLength: La password è troppo corta

View File

@ -128,7 +128,7 @@ Errors:
Invalid: 無効なパスワードです
NotSet: パスワードが未設置です
NotChanged: 新しいパスワードは現在のパスワードと同じにすることはできません
NotSupported: パスワードハッシュエンコードはサポートされていません
NotSupported: パスワードハッシュエンコードはサポートされていません。 https://zitadel.com/docs/concepts/architecture/secrets#hashed-secrets を参照してください。
PasswordComplexityPolicy:
NotFound: パスワードポリシーが見つかりません
MinLength: パスワードが短すぎます

View File

@ -135,7 +135,7 @@ Errors:
Invalid: Невалидна лозинка
NotSet: Корисникот нема поставено лозинка
NotChanged: Новата лозинка не може да биде иста со вашата тековна лозинка
NotSupported: Не е поддржано хаш-кодирањето на лозинката
NotSupported: Не е поддржано хаш-кодирањето на лозинката. Проверете го https://zitadel.com/docs/concepts/architecture/secrets#hashed-secrets
PasswordComplexityPolicy:
NotFound: Политиката за комплексност на лозинката не е пронајдена
MinLength: Лозинката е прекратка
@ -1012,6 +1012,9 @@ EventTypes:
check:
succeeded: Проверката на OIDC клиентска тајна е успешна
failed: Проверката на OIDC клиентска тајна е неуспешна
key:
added: Додаден е OIDC клуч за апликација
removed: OIDC клучот за апликација е отстранет
api:
secret:
check:

View File

@ -135,7 +135,7 @@ Errors:
Invalid: Wachtwoord is ongeldig
NotSet: Gebruiker heeft geen wachtwoord ingesteld
NotChanged: Nieuw wachtwoord kan niet hetzelfde zijn als uw huidige wachtwoord
NotSupported: Wachtwoord hash codering wordt niet ondersteund
NotSupported: Wachtwoord hash codering wordt niet ondersteund. Raadpleeg https://zitadel.com/docs/concepts/architecture/secrets#hashed-secrets
PasswordComplexityPolicy:
NotFound: Wachtwoordbeleid niet gevonden
MinLength: Wachtwoord is te kort
@ -1012,6 +1012,9 @@ EventTypes:
check:
succeeded: OIDC Client Secret controle geslaagd
failed: OIDC Client Secret controle mislukt
key:
added: OIDC app-sleutel toegevoegd
removed: OIDC app-sleutel verwijderd
api:
secret:
check:

View File

@ -136,7 +136,7 @@ Errors:
Invalid: Hasło jest nieprawidłowe
NotSet: Użytkownik nie ustawił hasła
NotChanged: Nowe hasło nie może być takie samo jak Twoje obecne hasło
NotSupported: Kodowanie skrótu hasła nie jest obsługiwane
NotSupported: Kodowanie skrótu hasła nie jest obsługiwane. Sprawdź https://zitadel.com/docs/concepts/architecture/secrets#hashed-secrets
PasswordComplexityPolicy:
NotFound: Polityka hasła nie znaleziona
MinLength: Hasło jest zbyt krótkie
@ -1013,6 +1013,9 @@ EventTypes:
check:
succeeded: Sprawdzenie sekretu OIDC Klienta powiodło się
failed: Sprawdzenie sekretu OIDC Klienta nie powiodło się
key:
added: Dodano klucz aplikacji OIDC
removed: Klucz aplikacji OIDC usunięty
api:
secret:
check:

View File

@ -136,6 +136,7 @@ Errors:
Invalid: Senha é inválida
NotSet: O usuário não definiu uma senha
NotChanged: A nova senha não pode ser igual à sua senha atual
NotSupported: Codificação hash da senha não suportada. Confira https://zitadel.com/docs/concepts/architecture/secrets#hashed-secrets
PasswordComplexityPolicy:
NotFound: Política de complexidade de senha não encontrada
MinLength: A senha é muito curta

View File

@ -136,7 +136,7 @@ Errors:
Invalid: Неверный пароль
NotSet: Пароль не установлен пользователем
NotChanged: Пароль не изменен
NotSupported: Кодировка хэша пароля не поддерживается.
NotSupported: Кодировка хэша пароля не поддерживается. Проверьте https://zitadel.com/docs/concepts/architecture/secrets#hashed-secrets
PasswordComplexityPolicy:
NotFound: Политика паролей не найдена
MinLength: Пароль слишком короткий

View File

@ -136,7 +136,7 @@ Errors:
Invalid: 密码无效
NotSet: 用户未设置密码
NotChanged: 新密码不能与您当前的密码相同
NotSupported: 不支持密码哈希编码
NotSupported: 不支持密码哈希编码。查看 https://zitadel.com/docs/concepts/architecture/secrets#hashed-secrets
PasswordComplexityPolicy:
NotFound: 未找到密码策略
MinLength: 密码太短
@ -1012,6 +1012,9 @@ EventTypes:
check:
succeeded: 检查 OIDC Client Secret 成功
failed: 检查 OIDC Client Secret 失败
key:
added: 添加了 OIDC 应用密钥
removed: OIDC 应用密钥已删除
api:
secret:
check:

View File

@ -7857,7 +7857,7 @@ message ImportHumanUserRequest {
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
example: "\"idp-config-id\"";
example: "\"external-user-id\"";
description: "The id of the user in the external identity provider"
}
];