mirror of
https://github.com/zitadel/zitadel.git
synced 2025-03-01 15:17:23 +00:00
Merge remote-tracking branch 'origin/main' into next
This commit is contained in:
commit
65d109ecd1
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@ -1,6 +1,11 @@
|
||||
name: ZITADEL CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
tags-ignore:
|
||||
- "*"
|
||||
branches:
|
||||
- "main"
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
245
cmd/setup/config_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
];
|
||||
|
@ -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) => {
|
||||
|
@ -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">
|
||||
|
@ -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]],
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -304,8 +304,9 @@
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.download-button {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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">
|
||||
|
@ -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',
|
||||
|
@ -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",
|
||||
|
1
console/src/assets/images/smtp/outlook.svg
Normal file
1
console/src/assets/images/smtp/outlook.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.2 KiB |
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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{
|
||||
{
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
57
internal/notification/channels/smtp/plain_or_login_auth.go
Normal file
57
internal/notification/channels/smtp/plain_or_login_auth.go
Normal 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")
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -128,7 +128,7 @@ Errors:
|
||||
Invalid: 無効なパスワードです
|
||||
NotSet: パスワードが未設置です
|
||||
NotChanged: 新しいパスワードは現在のパスワードと同じにすることはできません
|
||||
NotSupported: パスワードハッシュエンコードはサポートされていません
|
||||
NotSupported: パスワードハッシュエンコードはサポートされていません。 https://zitadel.com/docs/concepts/architecture/secrets#hashed-secrets を参照してください。
|
||||
PasswordComplexityPolicy:
|
||||
NotFound: パスワードポリシーが見つかりません
|
||||
MinLength: パスワードが短すぎます
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -136,7 +136,7 @@ Errors:
|
||||
Invalid: Неверный пароль
|
||||
NotSet: Пароль не установлен пользователем
|
||||
NotChanged: Пароль не изменен
|
||||
NotSupported: Кодировка хэша пароля не поддерживается.
|
||||
NotSupported: Кодировка хэша пароля не поддерживается. Проверьте https://zitadel.com/docs/concepts/architecture/secrets#hashed-secrets
|
||||
PasswordComplexityPolicy:
|
||||
NotFound: Политика паролей не найдена
|
||||
MinLength: Пароль слишком короткий
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
}
|
||||
];
|
||||
|
Loading…
x
Reference in New Issue
Block a user