Merge branch 'main' into next-rc

# Conflicts:
#	internal/query/user_auth_method_test.go
This commit is contained in:
Livio Spring 2024-06-20 06:59:22 +02:00
commit abfad8b3c2
No known key found for this signature in database
GPG Key ID: 26BB1C2FA5952CF0
217 changed files with 10172 additions and 855 deletions

View File

@ -7,7 +7,17 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
network_mode: service:db
command: sleep infinity
environment:
- 'ZITADEL_DATABASE_POSTGRES_HOST=db'
- 'ZITADEL_DATABASE_POSTGRES_PORT=5432'
- 'ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel'
- 'ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel'
- 'ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=zitadel'
- 'ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable'
- 'ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=postgres'
- 'ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD=postgres'
- 'ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable'
- 'ZITADEL_EXTERNALSECURE=false'
db:
image: postgres:latest
restart: unless-stopped

3
.gitignore vendored
View File

@ -85,4 +85,5 @@ go.work.sum
load-test/node_modules
load-test/yarn-error.log
load-test/dist
load-test/dist
.vercel

View File

@ -141,6 +141,20 @@ Replace "policeman" with "police officer," "manpower" with "workforce," and "bus
Ableist language includes words or phrases such as crazy, insane, blind to or blind eye to, cripple, dumb, and others.
Choose alternative words depending on the context.
### Developing ZITADEL with Dev Containers
Follow the instructions provided by your code editor/IDE to initiate the development container. This typically involves opening the "Command Palette" or similar functionality and searching for commands related to "Dev Containers" or "Remote Containers". The quick start guide for VS Code can found [here](https://code.visualstudio.com/docs/devcontainers/containers#_quick-start-open-an-existing-folder-in-a-container)
When you are connected to the container run the following commands to start ZITADEL.
```bash
make compile && ./zitadel start-from-init --masterkey MasterkeyNeedsToHave32Characters --tlsMode disabled
```
ZITADEL serves traffic as soon as you can see the following log line:
`INFO[0001] server is listening on [::]:8080`
### Backend/login
By executing the commands from this section, you run everything you need to develop the ZITADEL backend locally.
@ -398,6 +412,13 @@ ZITADEL loads translations from four files:
You may edit the texts in these files or create a new file for additional language support. Make sure you set the locale (ISO 639-1 code) as the name of the new language file.
Please make sure that the languages within the files remain in their own language, e.g. German must always be `Deutsch.
If you have added support for a new language, please also ensure that it is added in the list of languages in all the other language files.
You also have to add some changes to the following files:
- [Register Local File](./console/src/app/app.module.ts)
- [Add Supported Language](./console/src/app/utils/language.ts)
- [Customized Text Docs](./docs/docs/guides/manage/customize/texts.md)
- [Add language option](./internal/api/ui/login/static/templates/external_not_found_option.html)
## Want to start ZITADEL?

View File

@ -3,6 +3,40 @@
Dear community!
We're excited to announce bi-weekly office hours.
## #2 New Resources and Settings APIs
**Shape the future of ZITADEL Let's redesign the API for a better developer experience!**
Dear community,
Following the great success of our first office hours, we're back for round two! This time, we're focusing on YOU and how we can build the best possible ZITADEL API together.
We've been working on some ideas for the API, and we're excited to share them with you during the session. But more importantly, we want to hear YOUR thoughts! What does your dream ZITADEL API look like? What improvements would make your development life easier?
Join the open discussion next Wednesday in the office hours voice channel on Discord. We're ready for your honest feedback and fresh perspectives that help us shape the future of ZITADEL!
**What to expect**:
* **Our Suggestions**: @eliobischof will walk you through the improvement suggestions.
* **Open Discussion**: Get your questions answered directly by the ZITADEL team, describe your pain points and drop your thoughts in an
open discussion.
**Details**:
* **Target Audience**: Developers and IT Ops personnel using ZITADEL
* **Topic**: API Redesign and Q&A
* **When**: Wednesday 12th of June 12 pm PST / 3 pm EST / 9 pm CEST
* **Duration**: about 1 hour
* **Platform**: ZITADEL Discord Server (Join us here: https://zitadel.com/office-hours?event=1248016231936692274 )
**In the meantime**:
KUDOS, if you already [have a look at our proposal](https://zitadel.com/docs/apis/v3) before the start of the event. Share any inputs in the chat of the [office hours channel](https://zitadel.com/office-hours) on our Discord server.
We look forward to seeing you there!
P.S. Spread the word! Share this announcement with your fellow ZITADEL users who might be interested 📢
## #1 Dive Deep into Actions v2
The first office hour is dedicated to exploring the [new Actions v2 feature](https://zitadel.com/docs/concepts/features/actions_v2).
@ -26,4 +60,4 @@ Feel free to share any questions you already have about Actions v2 in the chat o
We look forward to seeing you there!
P.S. Spread the word! Share this announcement with your fellow ZITADEL users who might be interested.
P.S. Spread the word! Share this announcement with your fellow ZITADEL users who might be interested.

View File

@ -120,6 +120,10 @@ Database:
Cert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_CERT
Key: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_KEY
Admin:
# By default, ExistingDatabase is not specified in the connection string
# If the connection resolves to a database that is not existing in your system, configure an existing one here
# It is used in zitadel init to connect to cockroach and create a dedicated database for ZITADEL.
ExistingDatabase: # ZITADEL_DATABASE_COCKROACH_ADMIN_EXISTINGDATABASE
Username: root # ZITADEL_DATABASE_COCKROACH_ADMIN_USERNAME
Password: "" # ZITADEL_DATABASE_COCKROACH_ADMIN_PASSWORD
SSL:
@ -147,6 +151,10 @@ Database:
Cert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT
Key: # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY
Admin:
# The default ExistingDatabase is postgres
# If your db system doesn't have a database named postgres, configure an existing database here
# It is used in zitadel init to connect to postgres and create a dedicated database for ZITADEL.
ExistingDatabase: # ZITADEL_DATABASE_POSTGRES_ADMIN_EXISTINGDATABASE
Username: # ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME
Password: # ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD
SSL:

View File

@ -23,5 +23,5 @@ func (mig *User11AddLowerFieldsToVerifiedEmail) Execute(ctx context.Context, _ e
}
func (mig *User11AddLowerFieldsToVerifiedEmail) String() string {
return "25_user12_add_lower_fields_to_verified_email"
return "25_user13_add_lower_fields_to_verified_email"
}

View File

@ -1,2 +1,2 @@
ALTER TABLE IF EXISTS projections.users12_notifications ADD COLUMN IF NOT EXISTS verified_email_lower TEXT GENERATED ALWAYS AS (lower(verified_email)) STORED;
CREATE INDEX IF NOT EXISTS users12_notifications_email_search ON projections.users12_notifications (instance_id, verified_email_lower);
ALTER TABLE IF EXISTS projections.users13_notifications ADD COLUMN IF NOT EXISTS verified_email_lower TEXT GENERATED ALWAYS AS (lower(verified_email)) STORED;
CREATE INDEX IF NOT EXISTS users13_notifications_email_search ON projections.users13_notifications (instance_id, verified_email_lower);

View File

@ -14,6 +14,7 @@ import localePt from '@angular/common/locales/pt';
import localeZh from '@angular/common/locales/zh';
import localeRu from '@angular/common/locales/ru';
import localeNl from '@angular/common/locales/nl';
import localeSv from '@angular/common/locales/sv';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { MatNativeDateModule } from '@angular/material/core';
import { MatDialogModule } from '@angular/material/dialog';
@ -99,6 +100,8 @@ registerLocaleData(localeCs);
i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/cs.json'));
registerLocaleData(localeNl);
i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/nl.json'));
registerLocaleData(localeSv);
i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/sv.json'));
export class WebpackTranslateLoader implements TranslateLoader {
getTranslation(lang: string): Observable<any> {

View File

@ -248,12 +248,16 @@
display: none;
}
@media only screen and (min-width: 600px) {
display: inline;
display: inline;
i {
margin-right: -0.5rem;
margin-left: 0.25rem;
i {
margin-right: -0.5rem;
margin-left: 0.25rem;
}
@media only screen and (max-width: 375px) {
.iam-label {
font-size: x-small;
}
}
}

View File

@ -188,6 +188,7 @@ export function mapRequestValues(map: Partial<Map>, req: Req): Req {
const r17 = new PasswordChangeScreenText();
r17.setDescription(map.passwordChangeText?.description ?? '');
r17.setExpiredDescription(map.passwordChangeText?.expiredDescription ?? '');
r17.setNextButtonText(map.passwordChangeText?.nextButtonText ?? '');
r17.setTitle(map.passwordChangeText?.title ?? '');
r17.setNewPasswordLabel(map.passwordChangeText?.newPasswordLabel ?? '');

View File

@ -406,7 +406,7 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.tempUsername', value: '{{.TempUsername}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.username', value: '{{.UserName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.firstname', value: '{{.FirstName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.Lastname}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.LastName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.nickName', value: '{{.NickName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.displayName', value: '{{.DisplayName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastEmail', value: '{{.LastEmail}}' },
@ -421,7 +421,7 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.preferredLoginName', value: '{{.PreferredLoginName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.username', value: '{{.UserName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.firstname', value: '{{.FirstName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.Lastname}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.LastName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.nickName', value: '{{.NickName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.displayName', value: '{{.DisplayName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastEmail', value: '{{.LastEmail}}' },
@ -436,7 +436,7 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.preferredLoginName', value: '{{.PreferredLoginName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.username', value: '{{.UserName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.firstname', value: '{{.FirstName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.Lastname}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.LastName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.nickName', value: '{{.NickName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.displayName', value: '{{.DisplayName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastEmail', value: '{{.LastEmail}}' },
@ -451,7 +451,7 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.preferredLoginName', value: '{{.PreferredLoginName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.username', value: '{{.UserName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.firstname', value: '{{.FirstName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.Lastname}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.LastName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.nickName', value: '{{.NickName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.displayName', value: '{{.DisplayName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastEmail', value: '{{.LastEmail}}' },
@ -466,7 +466,7 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.preferredLoginName', value: '{{.PreferredLoginName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.username', value: '{{.UserName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.firstname', value: '{{.FirstName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.Lastname}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.LastName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.nickName', value: '{{.NickName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.displayName', value: '{{.DisplayName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastEmail', value: '{{.LastEmail}}' },
@ -482,7 +482,7 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.preferredLoginName', value: '{{.PreferredLoginName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.username', value: '{{.UserName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.firstname', value: '{{.FirstName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.Lastname}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.LastName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.nickName', value: '{{.NickName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.displayName', value: '{{.DisplayName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastEmail', value: '{{.LastEmail}}' },
@ -498,7 +498,7 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.preferredLoginName', value: '{{.PreferredLoginName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.username', value: '{{.UserName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.firstname', value: '{{.FirstName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.Lastname}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.LastName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.nickName', value: '{{.NickName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.displayName', value: '{{.DisplayName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastEmail', value: '{{.LastEmail}}' },
@ -512,7 +512,7 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.preferredLoginName', value: '{{.PreferredLoginName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.username', value: '{{.UserName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.firstname', value: '{{.FirstName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.Lastname}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.LastName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.nickName', value: '{{.NickName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.displayName', value: '{{.DisplayName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastEmail', value: '{{.LastEmail}}' },
@ -526,7 +526,7 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.preferredLoginName', value: '{{.PreferredLoginName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.username', value: '{{.UserName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.firstname', value: '{{.FirstName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.Lastname}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.LastName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.nickName', value: '{{.NickName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.displayName', value: '{{.DisplayName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastEmail', value: '{{.LastEmail}}' },

View File

@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PasswordAgePolicyComponent } from './password-age-policy.component';
const routes: Routes = [
{
path: '',
component: PasswordAgePolicyComponent,
data: {
animation: 'DetailPage',
},
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class PasswordAgePolicyRoutingModule {}

View File

@ -0,0 +1,50 @@
<h2>{{ 'POLICY.PWD_AGE.TITLE' | translate }}</h2>
<p class="cnsl-secondary-text">{{ 'POLICY.PWD_AGE.DESCRIPTION' | translate }}</p>
<div *ngIf="loading" class="spinner-wr">
<mat-spinner diameter="30" color="primary"></mat-spinner>
</div>
<ng-template cnslHasRole [hasRole]="['policy.delete']">
<button
*ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault"
matTooltip="{{ 'POLICY.RESET' | translate }}"
color="warn"
(click)="resetPolicy()"
mat-stroked-button
>
{{ 'POLICY.RESET' | translate }}
</button>
</ng-template>
<form class="lifetime-form" (ngSubmit)="savePolicy()" [formGroup]="passwordAgeForm" autocomplete="off">
<cnsl-card *ngIf="passwordAgeData">
<div class="age-content">
<div class="row">
<cnsl-form-field class="pwd-age-form-field" required="true">
<cnsl-label>{{ 'POLICY.DATA.MAXAGEDAYS' | translate }}</cnsl-label>
<input cnslInput type="number" name="maxAgeDays" formControlName="maxAgeDays" />
</cnsl-form-field>
</div>
<div class="row">
<cnsl-form-field class="pwd-age-form-field" required="true">
<cnsl-label>{{ 'POLICY.DATA.EXPIREWARNDAYS' | translate }}</cnsl-label>
<input cnslInput type="number" name="expireWarnDays" formControlName="expireWarnDays" />
</cnsl-form-field>
</div>
</div>
</cnsl-card>
</form>
<div class="btn-container">
<button
(click)="savePolicy()"
[disabled]="(['policy.write'] | hasRole | async) === false"
color="primary"
type="submit"
mat-raised-button
>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>

View File

@ -0,0 +1,37 @@
.default {
display: block;
margin-bottom: 1rem;
}
.policy-applied-to {
margin: -1rem 0 0 0;
font-size: 14px;
}
.age-content {
display: flex;
flex-direction: column;
width: 100%;
.row {
display: flex;
align-items: center;
padding: 0.3rem 0;
.pwd-age-form-field {
width: 100%;
max-width: 300px;
}
}
}
.btn-container {
display: flex;
justify-content: flex-start;
width: 100%;
button {
display: block;
margin-right: 1rem;
}
}

View File

@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { PasswordAgePolicyComponent } from './password-age-policy.component';
describe('PasswordLockoutPolicyComponent', () => {
let component: PasswordAgePolicyComponent;
let fixture: ComponentFixture<PasswordAgePolicyComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [PasswordAgePolicyComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PasswordAgePolicyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,178 @@
import { Component, Injector, Input, OnInit, Type } from '@angular/core';
import { AbstractControl, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { GetPasswordAgePolicyResponse as AdminGetPasswordAgePolicyResponse } from 'src/app/proto/generated/zitadel/admin_pb';
import { GetPasswordAgePolicyResponse as MgmtGetPasswordAgePolicyResponse } from 'src/app/proto/generated/zitadel/management_pb';
import { PasswordAgePolicy } from 'src/app/proto/generated/zitadel/policy_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { InfoSectionType } from '../../info-section/info-section.component';
import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
import { requiredValidator } from '../../form-field/validators/validators';
import { Observable } from 'rxjs';
import { GrpcAuthService } from '../../../services/grpc-auth.service';
import { take } from 'rxjs/operators';
@Component({
selector: 'cnsl-password-age-policy',
templateUrl: './password-age-policy.component.html',
styleUrls: ['./password-age-policy.component.scss'],
})
export class PasswordAgePolicyComponent implements OnInit {
@Input() public service!: ManagementService | AdminService;
@Input() public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
public passwordAgeForm!: UntypedFormGroup;
public passwordAgeData?: PasswordAgePolicy.AsObject;
public PolicyComponentServiceType: any = PolicyComponentServiceType;
public InfoSectionType: any = InfoSectionType;
public loading: boolean = false;
public canWrite$: Observable<boolean> = this.authService.isAllowed([
this.serviceType === PolicyComponentServiceType.ADMIN
? 'iam.policy.write'
: this.serviceType === PolicyComponentServiceType.MGMT
? 'policy.write'
: '',
]);
constructor(
private authService: GrpcAuthService,
private toast: ToastService,
private injector: Injector,
private dialog: MatDialog,
private fb: UntypedFormBuilder,
) {
this.passwordAgeForm = this.fb.group({
maxAgeDays: ['', []],
expireWarnDays: ['', []],
});
this.canWrite$.pipe(take(1)).subscribe((canWrite) => {
if (canWrite) {
this.passwordAgeForm.enable();
} else {
this.passwordAgeForm.disable();
}
});
}
public ngOnInit(): void {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
break;
}
this.fetchData();
}
private fetchData(): void {
this.loading = true;
this.getData().then((resp) => {
if (resp.policy) {
this.passwordAgeData = resp.policy;
this.passwordAgeForm.patchValue(this.passwordAgeData);
this.loading = false;
}
});
}
private getData(): Promise<AdminGetPasswordAgePolicyResponse.AsObject | MgmtGetPasswordAgePolicyResponse.AsObject> {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
return (this.service as ManagementService).getPasswordAgePolicy();
case PolicyComponentServiceType.ADMIN:
return (this.service as AdminService).getPasswordAgePolicy();
}
}
public resetPolicy(): void {
if (this.service instanceof ManagementService) {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.RESET',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'SETTING.DIALOG.RESET.DEFAULTTITLE',
descriptionKey: 'SETTING.DIALOG.RESET.DEFAULTDESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
(this.service as ManagementService)
.resetPasswordAgePolicyToDefault()
.then(() => {
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
}
public savePolicy(): void {
let promise: Promise<any>;
if (this.passwordAgeData) {
if (this.service instanceof AdminService) {
promise = this.service
.updatePasswordAgePolicy(this.maxAgeDays?.value ?? 0, this.expireWarnDays?.value ?? 0)
.then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
} else {
if ((this.passwordAgeData as PasswordAgePolicy.AsObject).isDefault) {
promise = (this.service as ManagementService)
.addCustomPasswordAgePolicy(this.maxAgeDays?.value ?? 0, this.expireWarnDays?.value ?? 0)
.then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
} else {
promise = (this.service as ManagementService)
.updateCustomPasswordAgePolicy(this.maxAgeDays?.value ?? 0, this.expireWarnDays?.value ?? 0)
.then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
}
}
}
}
public get isDefault(): boolean {
if (this.passwordAgeData && this.serviceType === PolicyComponentServiceType.MGMT) {
return (this.passwordAgeData as PasswordAgePolicy.AsObject).isDefault;
} else {
return false;
}
}
public get maxAgeDays(): AbstractControl | null {
return this.passwordAgeForm.get('maxAgeDays');
}
public get expireWarnDays(): AbstractControl | null {
return this.passwordAgeForm.get('expireWarnDays');
}
}

View File

@ -0,0 +1,46 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { CardModule } from '../../card/card.module';
import { InfoSectionModule } from '../../info-section/info-section.module';
import { WarnDialogModule } from '../../warn-dialog/warn-dialog.module';
import { PasswordAgePolicyRoutingModule } from './password-age-policy-routing.module';
import { PasswordAgePolicyComponent } from './password-age-policy.component';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@NgModule({
declarations: [PasswordAgePolicyComponent],
imports: [
PasswordAgePolicyRoutingModule,
CommonModule,
FormsModule,
InputModule,
MatButtonModule,
MatSlideToggleModule,
HasRolePipeModule,
MatDialogModule,
WarnDialogModule,
MatIconModule,
HasRoleModule,
MatTooltipModule,
CardModule,
TranslateModule,
DetailLayoutModule,
InfoSectionModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
],
exports: [PasswordAgePolicyComponent],
})
export class PasswordAgePolicyModule {}

View File

@ -8,7 +8,7 @@ import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { TranslateModule } from '@ngx-translate/core';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { MatDialogModule } from '@angular/material/dialog';
import { CardModule } from '../../card/card.module';
import { FormFieldModule } from '../../form-field/form-field.module';
import { InputModule } from '../../input/input.module';
@ -31,6 +31,7 @@ import { SecretGeneratorComponent } from './secret-generator.component';
MatProgressSpinnerModule,
MatSelectModule,
TranslateModule,
MatDialogModule,
],
exports: [SecretGeneratorComponent, DialogAddSecretGeneratorComponent],
})

View File

@ -18,6 +18,9 @@
<ng-container *ngIf="currentSetting === 'complexity'">
<cnsl-password-complexity-policy [serviceType]="serviceType"></cnsl-password-complexity-policy>
</ng-container>
<ng-container *ngIf="currentSetting === 'age'">
<cnsl-password-age-policy [serviceType]="serviceType"></cnsl-password-age-policy>
</ng-container>
<ng-container *ngIf="currentSetting === 'lockout'">
<cnsl-password-lockout-policy [serviceType]="serviceType"></cnsl-password-lockout-policy>
</ng-container>

View File

@ -16,6 +16,7 @@ import { NotificationPolicyModule } from '../policies/notification-policy/notifi
import { NotificationSMSProviderModule } from '../policies/notification-sms-provider/notification-sms-provider.module';
import { OIDCConfigurationModule } from '../policies/oidc-configuration/oidc-configuration.module';
import { PasswordComplexityPolicyModule } from '../policies/password-complexity-policy/password-complexity-policy.module';
import { PasswordAgePolicyModule } from '../policies/password-age-policy/password-age-policy.module';
import { PasswordLockoutPolicyModule } from '../policies/password-lockout-policy/password-lockout-policy.module';
import { PrivacyPolicyModule } from '../policies/privacy-policy/privacy-policy.module';
import { PrivateLabelingPolicyModule } from '../policies/private-labeling-policy/private-labeling-policy.module';
@ -40,6 +41,7 @@ import OrgListModule from 'src/app/pages/org-list/org-list.module';
LoginPolicyModule,
CardModule,
PasswordComplexityPolicyModule,
PasswordAgePolicyModule,
PasswordLockoutPolicyModule,
PrivateLabelingPolicyModule,
LanguageSettingsModule,

View File

@ -117,6 +117,16 @@ export const LOCKOUT: SidenavSetting = {
},
};
export const AGE: SidenavSetting = {
id: 'age',
i18nKey: 'SETTINGS.LIST.AGE',
groupI18nKey: 'SETTINGS.GROUPS.LOGIN',
requiredRoles: {
[PolicyComponentServiceType.MGMT]: ['policy.read'],
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
},
};
export const COMPLEXITY: SidenavSetting = {
id: 'complexity',
i18nKey: 'SETTINGS.LIST.COMPLEXITY',

View File

@ -25,7 +25,7 @@
<i *ngIf="copied !== tokenResponse.token" class="las la-clipboard"></i>
<i *ngIf="copied === tokenResponse.token" class="las la-clipboard-check"></i>
</button>
<span>{{ tokenResponse.token }}</span>
<span class="token">{{ tokenResponse.token }}</span>
</div>
</div>
</ng-container>

View File

@ -40,6 +40,11 @@
display: flex;
align-items: center;
.token {
overflow-wrap: break-word;
word-break: break-all;
}
.ctc {
margin-right: 1rem;
}

View File

@ -18,6 +18,7 @@ import {
LANGUAGES,
IDP,
LOCKOUT,
AGE,
LOGIN,
LOGINTEXTS,
MESSAGETEXTS,
@ -64,6 +65,7 @@ export class InstanceComponent implements OnInit, OnDestroy {
LOGIN,
IDP,
COMPLEXITY,
AGE,
LOCKOUT,
DOMAIN,

View File

@ -7,6 +7,7 @@ import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import {
AGE,
BRANDING,
COMPLEXITY,
DOMAIN,
@ -33,6 +34,7 @@ export class OrgSettingsComponent implements OnInit {
LOGIN,
IDP,
COMPLEXITY,
AGE,
LOCKOUT,
NOTIFICATIONS,
VERIFIED_DOMAINS,

View File

@ -1,3 +1,3 @@
export const supportedLanguages = ['de', 'en', 'es', 'fr', 'it', 'ja', 'pl', 'zh', 'bg', 'pt', 'mk', 'cs', 'ru', 'nl'];
export const supportedLanguagesRegexp: RegExp = /de|en|es|fr|it|ja|pl|zh|bg|pt|mk|cs|ru|nl/;
export const supportedLanguages = ['de', 'en', 'es', 'fr', 'it', 'ja', 'pl', 'zh', 'bg', 'pt', 'mk', 'cs', 'ru', 'nl', 'sv'];
export const supportedLanguagesRegexp: RegExp = /de|en|es|fr|it|ja|pl|zh|bg|pt|mk|cs|ru|nl|sv/;
export const fallbackLanguage: string = 'en';

View File

@ -1319,6 +1319,7 @@
"LANGUAGES": "Езици",
"LOGIN": "Поведение при влизане и сигурност",
"LOCKOUT": "Блокиране",
"AGE": "Изтичане на паролата",
"COMPLEXITY": "Сложност на паролата",
"NOTIFICATIONS": "Настройки за известията",
"SMTP_PROVIDER": "SMTP доставчик",
@ -1373,7 +1374,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
}
},
"SMTP": {
@ -1538,8 +1540,8 @@
}
},
"PWD_AGE": {
"TITLE": "Остаряване на паролата",
"DESCRIPTION": "Можете да зададете политика за остаряването на паролите. "
"TITLE": "Изтичане на паролата",
"DESCRIPTION": "Можете да зададете политика за изтичане на паролите. Тази политика ще принуди потребителя да смени паролата при следващото влизане след изтичането. Няма автоматични предупреждения и известия."
},
"PWD_LOCKOUT": {
"TITLE": "Политика за блокиране",
@ -1602,7 +1604,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"KEYS": {
"emailVerificationDoneText": "Проверката на имейл е извършена",
@ -1691,8 +1694,8 @@
"SHOWLOCKOUTFAILURES": "показва грешки при блокиране",
"MAXPASSWORDATTEMPTS": "Максимален брой опити за парола",
"MAXOTPATTEMPTS": "Максимален брой опити за OTP",
"EXPIREWARNDAYS": "Предупреждение за изтичане след ден",
"MAXAGEDAYS": "Максимална възраст в дни",
"EXPIREWARNDAYS": "Предупреждение за изтичане след дни",
"MAXAGEDAYS": "Максимална валидност в дни",
"USERLOGINMUSTBEDOMAIN": "Добавяне на домейн на организация като суфикс към имената за вход",
"USERLOGINMUSTBEDOMAIN_DESCRIPTION": "Ако активирате тази настройка, всички имена за вход ще имат суфикс с домейна на организацията. ",
"VALIDATEORGDOMAINS": "Верификация на домейна на организацията е необходима (DNS или HTTP предизвикателство)",
@ -2531,7 +2534,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"MEMBER": {
"ADD": "Добавяне на мениджър",

View File

@ -1326,6 +1326,7 @@
"LANGUAGES": "Jazyky",
"LOGIN": "Chování při přihlášení a bezpečnost",
"LOCKOUT": "Blokování",
"AGE": "Expirace hesla",
"COMPLEXITY": "Složitost hesla",
"NOTIFICATIONS": "Oznámení",
"SMTP_PROVIDER": "Poskytovatel SMTP",
@ -1380,7 +1381,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
}
},
"SMTP": {
@ -1545,8 +1547,8 @@
}
},
"PWD_AGE": {
"TITLE": "Stáří hesla",
"DESCRIPTION": "Můžete nastavit politiku pro stáří hesel. Tato politika vydá varování po uplynutí konkrétního času stáří."
"TITLE": "Expirace hesla",
"DESCRIPTION": "Můžete nastavit pravidlo pro vypršení platnosti hesel. Toto pravidlo donutí uživatele změnit heslo při dalším přihlášení po uplynutí platnosti. Neexistují žádná automatická varování a upozornění."
},
"PWD_LOCKOUT": {
"TITLE": "Politika uzamčení",
@ -1609,7 +1611,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"KEYS": {
"emailVerificationDoneText": "Ověření e-mailu dokončeno",
@ -1698,8 +1701,8 @@
"SHOWLOCKOUTFAILURES": "zobrazit neúspěšné pokusy o uzamčení",
"MAXPASSWORDATTEMPTS": "Maximální počet pokusů o heslo",
"MAXOTPATTEMPTS": "Maximální počet pokusů o OTP",
"EXPIREWARNDAYS": "Upozornění na expiraci po dni",
"MAXAGEDAYS": "Maximální stáří v dnech",
"EXPIREWARNDAYS": "Upozornění na uplynutí po dnech",
"MAXAGEDAYS": "Maximální platnost v dnech",
"USERLOGINMUSTBEDOMAIN": "Přidat doménu organizace jako příponu k přihlašovacím jménům",
"USERLOGINMUSTBEDOMAIN_DESCRIPTION": "Pokud povolíte toto nastavení, všechna přihlašovací jména budou doplněna o doménu organizace. Pokud je toto nastavení zakázáno, musíte zajistit, aby byla uživatelská jména jedinečná napříč všemi organizacemi.",
"VALIDATEORGDOMAINS": "Požadováno ověření domény organizace (DNS nebo HTTP výzva)",
@ -2550,7 +2553,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"MEMBER": {
"ADD": "Přidat manažera",

View File

@ -1325,6 +1325,7 @@
"LANGUAGES": "Sprachen",
"LOGIN": "Loginverhalten und Sicherheit",
"LOCKOUT": "Sperrmechanismen",
"AGE": "Passwortgültigkeitsdauer",
"COMPLEXITY": "Passwordkomplexität",
"NOTIFICATIONS": "Benachrichtigungseinstellungen",
"SMTP_PROVIDER": "SMTP-Anbieter",
@ -1379,7 +1380,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
}
},
"SMTP": {
@ -1545,7 +1547,7 @@
},
"PWD_AGE": {
"TITLE": "Gültigkeitsdauer für Passwörter",
"DESCRIPTION": "Du kannst eine Richtlinie für die maximale Gültigkeitsdauer von Passwörtern festlegen. Diese Richtlinie löst eine Warnung nach Ablauf einer festgelegten Gültigkeitsdauer aus."
"DESCRIPTION": "Du kannst eine Richtlinie für die maximale Gültigkeitsdauer von Passwörtern festlegen. Diese Richtlinie zwingt den Benutzer dazu, das Passwort bei der nächsten Anmeldung nach dem Ablauf zu ändern. Es gibt keine automatischen Warnungen und Benachrichtigungen."
},
"PWD_LOCKOUT": {
"TITLE": "Passwortsperre",
@ -1608,7 +1610,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"KEYS": {
"emailVerificationDoneText": "Email Verification erfolgreich",
@ -2540,7 +2543,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"MEMBER": {
"ADD": "Manager hinzufügen",

View File

@ -1326,6 +1326,7 @@
"LANGUAGES": "Languages",
"LOGIN": "Login Behavior and Security",
"LOCKOUT": "Lockout",
"AGE": "Password expiry",
"COMPLEXITY": "Password complexity",
"NOTIFICATIONS": "Notifications",
"SMTP_PROVIDER": "SMTP Provider",
@ -1380,7 +1381,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
}
},
"SMTP": {
@ -1545,8 +1547,8 @@
}
},
"PWD_AGE": {
"TITLE": "Password Aging",
"DESCRIPTION": "You can set a policy for the aging of passwords. This policy emits a warning after the specific aging time has elapsed."
"TITLE": "Password Expiry",
"DESCRIPTION": "You can set a policy for the expiry of passwords. This policy will force the user to change the password on the next login after the expiration. There are no automatic warnings and notifications."
},
"PWD_LOCKOUT": {
"TITLE": "Lockout Policy",
@ -1609,7 +1611,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"KEYS": {
"emailVerificationDoneText": "Email verification done",
@ -1698,8 +1701,8 @@
"SHOWLOCKOUTFAILURES": "show lockout failures",
"MAXPASSWORDATTEMPTS": "Password maximum attempts",
"MAXOTPATTEMPTS": "OTP maximum attempts",
"EXPIREWARNDAYS": "Expiration Warning after day",
"MAXAGEDAYS": "Max Age in days",
"EXPIREWARNDAYS": "Expiration Warning after days",
"MAXAGEDAYS": "Maximum validity in days",
"USERLOGINMUSTBEDOMAIN": "Add organization domain as suffix to loginnames",
"USERLOGINMUSTBEDOMAIN_DESCRIPTION": "If you enable this setting, all loginnames will be suffixed with the organization domain. If this settings is disabled, you have to ensure that usernames are unique over all organizations.",
"VALIDATEORGDOMAINS": "Organization domain verification required (DNS or HTTP challenge)",
@ -2562,7 +2565,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"MEMBER": {
"ADD": "Add a Manager",

View File

@ -1327,6 +1327,7 @@
"LANGUAGES": "Idiomas",
"LOGIN": "Comportamiento del inicio de sesión y de la seguridad",
"LOCKOUT": "Bloqueo",
"AGE": "Caducidad de la contraseña",
"COMPLEXITY": "Complejidad de contraseña",
"NOTIFICATIONS": "Ajustes de notificación",
"SMTP_PROVIDER": "Proveedor SMTP",
@ -1381,7 +1382,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
}
},
"SMTP": {
@ -1546,8 +1548,8 @@
}
},
"PWD_AGE": {
"TITLE": "Antigüedad de la contraseña",
"DESCRIPTION": "Puedes establecer una política para la antigüedad de las contraseñas. Esta política emite un aviso después de que la antigüedad máxima se haya superado."
"TITLE": "Caducidad de la contraseña",
"DESCRIPTION": "Puedes establecer una política para la caducidad de las contraseñas. Esta política obligará al usuario a cambiar la contraseña en el próximo inicio de sesión después de la caducidad. No hay avisos ni notificaciones automáticos."
},
"PWD_LOCKOUT": {
"TITLE": "Política de bloqueo",
@ -1610,7 +1612,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"KEYS": {
"emailVerificationDoneText": "Verificación de email realizada",
@ -1699,8 +1702,8 @@
"SHOWLOCKOUTFAILURES": "mostrar fallos de bloqueo",
"MAXPASSWORDATTEMPTS": "Intentos máximos de contraseña",
"MAXOTPATTEMPTS": "Intentos máximos de OTP",
"EXPIREWARNDAYS": "Aviso de expiración después de estos días: ",
"MAXAGEDAYS": "Antigüedad máxima en días",
"EXPIREWARNDAYS": "Aviso de caducidad después de días",
"MAXAGEDAYS": "Validez máxima en días",
"USERLOGINMUSTBEDOMAIN": "Añadir el dominio de la organización como sufijo de los nombres de inicio de sesión",
"USERLOGINMUSTBEDOMAIN_DESCRIPTION": "Si activas esta opción, todos los nombres de inicio de sesión tendrán como sufijo el dominio de esta organización. Si esta opción está desactivada, tendrás que asegurarte de que los nombres de usuario son únicos para todas las organizaciones.",
"VALIDATEORGDOMAINS": "Verificación de dominio de la organización requerida (desafío DNS o HTTP)",
@ -2538,7 +2541,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"MEMBER": {
"ADD": "Añadir un Mánager",

View File

@ -1325,6 +1325,7 @@
"LANGUAGES": "Langues",
"LOGIN": "Comportement de connexion et sécurité",
"LOCKOUT": "Verrouillage",
"AGE": "Expiration du mot de passe",
"COMPLEXITY": "Complexité du mot de passe",
"NOTIFICATIONS": "Paramètres de notification",
"SMTP_PROVIDER": "Fournisseur SMTP",
@ -1379,7 +1380,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
}
},
"SMTP": {
@ -1544,8 +1546,8 @@
}
},
"PWD_AGE": {
"TITLE": "Vieillissement des mots de passe",
"DESCRIPTION": "Vous pouvez définir une politique pour le vieillissement des mots de passe. Cette politique émet un avertissement après que le temps de vieillissement spécifique se soit écoulé."
"TITLE": "Expiration du mot de passe",
"DESCRIPTION": "Vous pouvez définir une politique d'expiration des mots de passe. Cette politique obligera l'utilisateur à changer son mot de passe lors de la prochaine connexion après l'expiration. Il n'y a pas d'avertissements ni de notifications automatiques."
},
"PWD_LOCKOUT": {
"TITLE": "Politique de verrouillage",
@ -1608,7 +1610,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"KEYS": {
"emailVerificationDoneText": "Vérification de l'e-mail effectuée",
@ -1697,8 +1700,8 @@
"SHOWLOCKOUTFAILURES": "montrer les échecs de verrouillage",
"MAXPASSWORDATTEMPTS": "Tentatives maximales du mot de passe",
"MAXOTPATTEMPTS": "Tentatives maximales de l'OTP",
"EXPIREWARNDAYS": "Avertissement d'expiration après le jour",
"MAXAGEDAYS": "Âge maximum en jours",
"EXPIREWARNDAYS": "Avertissement d'expiration après jours",
"MAXAGEDAYS": "Validité maximale en jours",
"USERLOGINMUSTBEDOMAIN": "Le nom de connexion de l'utilisateur doit contenir le nom de domaine de l'organisation",
"USERLOGINMUSTBEDOMAIN_DESCRIPTION": "Si vous activez ce paramètre, tous les noms de connexion seront suffixés avec le domaine de l'organisation. Si ce paramètre est désactivé, vous devez vous assurer que les noms d'utilisateur sont uniques pour toutes les organisations.",
"VALIDATEORGDOMAINS": "Vérification du domaine de l'organisation requise (challenge DNS ou HTTP)",
@ -2541,7 +2544,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"MEMBER": {
"ADD": "Ajouter un responsable",

View File

@ -1325,6 +1325,7 @@
"LANGUAGES": "Lingue",
"LOGIN": "Comportamento login e sicurezza",
"LOCKOUT": "Meccanismi di bloccaggio",
"AGE": "Scadenza password",
"COMPLEXITY": "Complessità della password",
"NOTIFICATIONS": "Impostazioni di notifica",
"SMTP_PROVIDER": "Fornitore SMTP",
@ -1379,7 +1380,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
}
},
"SMTP": {
@ -1544,8 +1546,8 @@
}
},
"PWD_AGE": {
"TITLE": "Impostazioni di validità della password",
"DESCRIPTION": "È possibile impostare una impostazone per la validità delle password. Questa emette un avviso dopo che il tempo di invecchiamento specifico è trascorso."
"TITLE": "Scadenza password",
"DESCRIPTION": "Puoi impostare una policy per la scadenza delle password. Questa policy obbligherà l'utente a cambiare la password al prossimo accesso dopo la scadenza. Non ci sono avvisi e notifiche automatiche."
},
"PWD_LOCKOUT": {
"TITLE": "Impostazioni di blocco",
@ -1608,7 +1610,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"KEYS": {
"emailVerificationDoneText": "Verifica dell'e-mail terminata con successo.",
@ -1697,8 +1700,8 @@
"SHOWLOCKOUTFAILURES": "mostra i fallimenti del blocco",
"MAXPASSWORDATTEMPTS": "Massimo numero di tentativi di password",
"MAXOTPATTEMPTS": "Massimo numero di tentativi di OTP",
"EXPIREWARNDAYS": "Avviso scadenza dopo il giorno",
"MAXAGEDAYS": "Lunghezza massima in giorni",
"EXPIREWARNDAYS": "Avviso di scadenza dopo giorni",
"MAXAGEDAYS": "Validità massima in giorni",
"USERLOGINMUSTBEDOMAIN": "Nome utente deve contenere il dominio dell' organizzazione",
"USERLOGINMUSTBEDOMAIN_DESCRIPTION": "Se abiliti questa impostazione, a tutti i nomi di accesso verrà aggiunto il suffisso del dominio dell'organizzazione. Se questa impostazione è disabilitata, devi assicurarti che i nomi utente siano univoci per tutte le organizzazioni.",
"VALIDATEORGDOMAINS": "Verifica del dominio dell'organizzazione richiesta (challenge DNS o HTTP)",
@ -2541,7 +2544,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"MEMBER": {
"ADD": "Aggiungi un manager",

View File

@ -1326,6 +1326,7 @@
"LANGUAGES": "一般設定",
"LOGIN": "ログイン動作とセキュリティ",
"LOCKOUT": "ロックアウト",
"AGE": "パスワードの有効期限",
"COMPLEXITY": "パスワードの複雑さ",
"NOTIFICATIONS": "通知設定",
"SMTP_PROVIDER": "SMTPプロバイダー",
@ -1380,7 +1381,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
}
},
"SMTP": {
@ -1541,8 +1543,8 @@
}
},
"PWD_AGE": {
"TITLE": "パスワードエージング",
"DESCRIPTION": "パスワードエージングに関するポリシーを設定できます。このポリシーは、特定のエージング時間が経過した後に警告を発します。"
"TITLE": "パスワードの有効期限",
"DESCRIPTION": "パスワードの有効期限ポリシーを設定できます。 このポリシーにより、有効期限が切れた後にユーザーは次回のログイン時にパスワードを変更することを求められます。 自動的な警告や通知はない。"
},
"PWD_LOCKOUT": {
"TITLE": "ロックアウトポリシー",
@ -1605,7 +1607,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"KEYS": {
"emailVerificationDoneText": "メール認証が完了しました",
@ -1694,8 +1697,8 @@
"SHOWLOCKOUTFAILURES": "ロックアウトの失敗を表示する",
"MAXPASSWORDATTEMPTS": "パスワードの最大試行",
"MAXOTPATTEMPTS": "最大OTP試行回数",
"EXPIREWARNDAYS": "有効期限の翌日以降の警告",
"MAXAGEDAYS": "最大有効期限",
"EXPIREWARNDAYS": "数日後に有効期限が切れます",
"MAXAGEDAYS": "最大有効期限 (日数)",
"USERLOGINMUSTBEDOMAIN": "ログイン名の接尾辞として組織ドメインを追加する",
"USERLOGINMUSTBEDOMAIN_DESCRIPTION": "この設定を有効にすると、すべてのログイン名が組織ドメインで接尾辞が付けられます。この設定が無効になっている場合、ユーザー名がすべての組織で一意であることを確認する必要があります。",
"VALIDATEORGDOMAINS": "組織のドメイン検証が必要です (DNSまたはHTTPチャレンジ)",
@ -2533,7 +2536,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"MEMBER": {
"ADD": "マネージャーを追加する",

View File

@ -1327,6 +1327,7 @@
"LANGUAGES": "Општо",
"LOGIN": "Правила и безбедност при најава",
"LOCKOUT": "Забрана на пристап",
"AGE": "Истекување на лозинката",
"COMPLEXITY": "Сложеност на лозинката",
"NOTIFICATIONS": "Подесувања за известувања",
"SMTP_PROVIDER": "SMTP провајдер",
@ -1381,7 +1382,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
}
},
"SMTP": {
@ -1546,8 +1548,8 @@
}
},
"PWD_AGE": {
"TITLE": "Важност на лозинка",
"DESCRIPTION": "Можете да поставите политика за истекување на лозинките. Оваа политика издава предупредување откако ќе помени одреденото време на истекување."
"TITLE": "Истекување на лозинката",
"DESCRIPTION": "Можете да поставите политика за истекување на лозинките. Оваа политика ќе го принуди корисникот да ја смени лозинката при следлогото влегување по истекувањето. Нема автоматски предупредувања и известувања."
},
"PWD_LOCKOUT": {
"TITLE": "Политика за забрана на пристап",
@ -1610,7 +1612,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"KEYS": {
"emailVerificationDoneText": "Е-поштата е верифицирана",
@ -1699,8 +1702,8 @@
"SHOWLOCKOUTFAILURES": "прикажи неуспешни заклучувања",
"MAXPASSWORDATTEMPTS": "Максимален број на обиди за лозинка",
"MAXOTPATTEMPTS": "Максимални обиди за OTP",
"EXPIREWARNDAYS": "Предупредување за истекување по ден",
"MAXAGEDAYS": "Максимална возраст во денови",
"EXPIREWARNDAYS": "Предупредување за истекување по денови",
"MAXAGEDAYS": "Максимална валидност во денови",
"USERLOGINMUSTBEDOMAIN": "Додади организациски домен како суфикс на корисничките имиња",
"USERLOGINMUSTBEDOMAIN_DESCRIPTION": "Ако го овозможите ова подесување, сите кориснички имиња ќе имаат суфикс на организацискиот домен. Доколку ова подесување е оневозможено, морате да се осигурате дека корисничките имиња се уникатни низ сите организации.",
"VALIDATEORGDOMAINS": "Потврда на доменот на организацијата е неопходна (DNS или HTTP предизвик)",
@ -2538,7 +2541,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"MEMBER": {
"ADD": "Додај Менаџер",

View File

@ -1326,6 +1326,7 @@
"LANGUAGES": "Talen",
"LOGIN": "Login Gedrag en Beveiliging",
"LOCKOUT": "Lockout",
"AGE": "Wachtwoord verloopt",
"COMPLEXITY": "Wachtwoord complexiteit",
"NOTIFICATIONS": "Notificaties",
"SMTP_PROVIDER": "SMTP Provider",
@ -1380,7 +1381,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
}
},
"SMTP": {
@ -1545,8 +1547,8 @@
}
},
"PWD_AGE": {
"TITLE": "Wachtwoord Veroudering",
"DESCRIPTION": "U kunt een beleid instellen voor de veroudering van wachtwoorden. Dit beleid geeft een waarschuwing nadat de specifieke verouderingstijd is verstreken."
"TITLE": "Wachtwoord verloopt",
"DESCRIPTION": "U kunt een beleid instellen voor het verlopen van wachtwoorden. Dit beleid dwingt de gebruiker om het wachtwoord te wijzigen bij de volgende aanmelding na het verlopen. Er zijn geen automatische waarschuwingen en meldingen."
},
"PWD_LOCKOUT": {
"TITLE": "Lockout Beleid",
@ -1609,7 +1611,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"KEYS": {
"emailVerificationDoneText": "E-mail verificatie voltooid",
@ -1698,8 +1701,8 @@
"SHOWLOCKOUTFAILURES": "toon lockout mislukkingen",
"MAXPASSWORDATTEMPTS": "Maximum pogingen voor wachtwoord",
"MAXOTPATTEMPTS": "Maximale OTP-pogingen",
"EXPIREWARNDAYS": "Vervaldatum Waarschuwing na dag",
"MAXAGEDAYS": "Maximale Leeftijd in dagen",
"EXPIREWARNDAYS": "Waarschuwing voor verlopen na dagen",
"MAXAGEDAYS": "Maximale geldigheid in dagen",
"USERLOGINMUSTBEDOMAIN": "Voeg organisatie domein toe als achtervoegsel aan inlognamen",
"USERLOGINMUSTBEDOMAIN_DESCRIPTION": "Als u deze instelling inschakelt, worden alle inlognamen voorzien van een achtervoegsel met het domein van de organisatie. Als deze instelling is uitgeschakeld, moet u ervoor zorgen dat gebruikersnamen uniek zijn over alle organisaties.",
"VALIDATEORGDOMAINS": "Verificatie van organisatiedomeinen vereist (DNS of HTTP-uitdaging)",
@ -2559,7 +2562,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"MEMBER": {
"ADD": "Voeg een Manager toe",

View File

@ -1325,6 +1325,7 @@
"LANGUAGES": "Języki",
"LOGIN": "Zachowanie logowania i bezpieczeństwo",
"LOCKOUT": "Blokada",
"AGE": "Wygaśnięcie hasła",
"COMPLEXITY": "Złożoność hasła",
"NOTIFICATIONS": "Ustawienia powiadomień",
"SMTP_PROVIDER": "Dostawca SMTP",
@ -1379,7 +1380,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
}
},
"SMTP": {
@ -1544,8 +1546,8 @@
}
},
"PWD_AGE": {
"TITLE": "Starzenie się hasła",
"DESCRIPTION": "Możesz ustawić politykę dotyczącą starzenia się haseł. Ta polityka emituje ostrzeżenie po upływie określonego czasu starzenia."
"TITLE": "Wygaśnięcie hasła",
"DESCRIPTION": "Możesz ustawić zasady wygasania haseł. Ta zasada zmusi użytkownika do zmiany hasła przy następnym logowaniu po jego wygaśnięciu. Nie ma automatycznych ostrzeżeń i powiadomień."
},
"PWD_LOCKOUT": {
"TITLE": "Polityka blokowania",
@ -1608,7 +1610,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"KEYS": {
"emailVerificationDoneText": "Weryfikacja adresu e-mail zakończona",
@ -1697,8 +1700,8 @@
"SHOWLOCKOUTFAILURES": "pokaż blokady nieudanych prób",
"MAXPASSWORDATTEMPTS": "Maksymalna liczba prób wprowadzenia hasła",
"MAXOTPATTEMPTS": "Maksymalna liczba prób OTP",
"EXPIREWARNDAYS": "Ostrzeżenie o wygaśnięciu po dniu",
"MAXAGEDAYS": "Maksymalny wiek w dniach",
"EXPIREWARNDAYS": "Ostrzeżenie o wygaśnięciu po dniach",
"MAXAGEDAYS": "Maksymalna ważność w dniach",
"USERLOGINMUSTBEDOMAIN": "Dodaj domenę organizacji jako przyrostek do nazw logowania",
"USERLOGINMUSTBEDOMAIN_DESCRIPTION": "Jeśli włączysz to ustawienie, wszystkie nazwy logowania będą miały przyrostek z domeną organizacji. Jeśli to ustawienie jest wyłączone, musisz zapewnić unikalność nazw użytkowników we wszystkich organizacjach.",
"VALIDATEORGDOMAINS": "Weryfikacja domeny organizacji jest wymagana (wyzwanie DNS lub HTTP)",
@ -2541,7 +2544,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"MEMBER": {
"ADD": "Dodaj managera",

View File

@ -1327,6 +1327,7 @@
"LANGUAGES": "Idiomas",
"LOGIN": "Comportamento de Login e Segurança",
"LOCKOUT": "Bloqueio",
"AGE": "Expiração da senha",
"COMPLEXITY": "Complexidade de Senha",
"NOTIFICATIONS": "Configurações de Notificação",
"SMTP_PROVIDER": "Provedor SMTP",
@ -1381,7 +1382,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
}
},
"SMTP": {
@ -1546,8 +1548,8 @@
}
},
"PWD_AGE": {
"TITLE": "Envelhecimento de senha",
"DESCRIPTION": "Você pode definir uma política para o envelhecimento de senhas. Essa política emite um aviso após o tempo de envelhecimento específico ter passado."
"TITLE": "Expiração da senha",
"DESCRIPTION": "Você pode definir uma política para a expiração de senhas. Esta política forçará o usuário a alterar a senha no próximo login após a expiração. Não existem avisos e notificações automáticas."
},
"PWD_LOCKOUT": {
"TITLE": "Política de bloqueio",
@ -1610,7 +1612,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"KEYS": {
"emailVerificationDoneText": "Verificação de email concluída",
@ -1700,7 +1703,7 @@
"MAXPASSWORDATTEMPTS": "Máximo de tentativas de senha",
"MAXOTPATTEMPTS": "Máximo de tentativas de OTP",
"EXPIREWARNDAYS": "Aviso de expiração após dias",
"MAXAGEDAYS": "Idade máxima em dias",
"MAXAGEDAYS": "Validade máxima em dias",
"USERLOGINMUSTBEDOMAIN": "Adicionar domínio da organização como sufixo aos nomes de login",
"USERLOGINMUSTBEDOMAIN_DESCRIPTION": "Se você habilitar essa configuração, todos os nomes de login serão sufixados com o domínio da organização. Se essa configuração estiver desabilitada, você deve garantir que os nomes de usuário sejam exclusivos em todas as organizações.",
"VALIDATEORGDOMAINS": "Verificação de domínio da organização necessária (desafio DNS ou HTTP)",
@ -2536,7 +2539,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"MEMBER": {
"ADD": "Adicionar um Gerente",

View File

@ -1373,6 +1373,7 @@
"LANGUAGES": "Языки",
"LOGIN": "Действия при входе и безопасность",
"LOCKOUT": "Блокировка",
"AGE": "Срок действия пароля",
"COMPLEXITY": "Сложность пароля",
"NOTIFICATIONS": "Настройки уведомлений",
"NOTIFICATIONS_DESC": "Настройки SMTP и SMS",
@ -1423,7 +1424,8 @@
"pt": "Portuguese",
"mk": "Македонски",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
}
},
"SMTP": {
@ -1595,7 +1597,7 @@
},
"PWD_AGE": {
"TITLE": "Срок действия пароля",
"DESCRIPTION": "Вы можете установить политику срока действия паролей. Данная политика предупреждает об истечении определённого времени срока действия."
"DESCRIPTION": "Вы можете установить политику истечения срока действия паролей. Эта политика вынудит пользователя изменить пароль при следующем входе в систему после истечения срока его действия. Нет никаких автоматических предупреждений и уведомлений."
},
"PWD_LOCKOUT": {
"TITLE": "Политика блокировки",
@ -1664,7 +1666,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"LOCALE": "Код языка",
"LOCALES": {
@ -1765,8 +1768,8 @@
"SHOWLOCKOUTFAILURES": "Показать ошибки блокировки",
"MAXPASSWORDATTEMPTS": "Максимальное количество попыток пароля",
"MAXOTPATTEMPTS": "Максимальное количество попыток OTP",
"EXPIREWARNDAYS": "Предупреждение об истечении срока действия после дня",
"MAXAGEDAYS": "Максимальный возраст в днях",
"EXPIREWARNDAYS": "Предупреждение об истечении срока действия через несколько дней",
"MAXAGEDAYS": "Максимальная продолжительность действия (дни)",
"USERLOGINMUSTBEDOMAIN": "Добавить домен организации в качестве суффикса к именам логина",
"USERLOGINMUSTBEDOMAIN_DESCRIPTION": "Если вы включите данный параметр, все имена входа будут иметь суффикс домена организации. Если данный параметр отключен, вы должны убедиться, что имена пользователей уникальны для всех организаций.",
"VALIDATEORGDOMAINS": "Проверка доменов организации",
@ -2649,7 +2652,8 @@
"pt": "Portuguese",
"mk": "Македонски",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"MEMBER": {
"ADD": "Добавить менеджера",

File diff suppressed because it is too large Load Diff

View File

@ -1325,6 +1325,7 @@
"LANGUAGES": "语言",
"LOGIN": "登录行为和安全",
"LOCKOUT": "安全锁策略",
"AGE": "密码过期",
"COMPLEXITY": "密码复杂性",
"NOTIFICATIONS": "通知设置",
"SMTP_PROVIDER": "SMTP 提供商",
@ -1379,7 +1380,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
}
},
"SMTP": {
@ -1544,7 +1546,7 @@
},
"PWD_AGE": {
"TITLE": "密码过期",
"DESCRIPTION": "您可以设置密码过期策略。此策略会在特定过期时间过后发出警告。"
"DESCRIPTION": "您可以设置密码过期策略。此策略将强制用户在密码过期后下次登录时更改密码。没有自动警告和通知。"
},
"PWD_LOCKOUT": {
"TITLE": "锁定策略",
@ -1607,7 +1609,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"KEYS": {
"emailVerificationDoneText": "电子邮件验证完成",
@ -1696,8 +1699,8 @@
"SHOWLOCKOUTFAILURES": "显示锁定失败",
"MAXPASSWORDATTEMPTS": "密码最大尝试次数",
"MAXOTPATTEMPTS": "最多尝试 OTP 次数",
"EXPIREWARNDAYS": "密码过期警告",
"MAXAGEDAYS": "Max Age in days",
"EXPIREWARNDAYS": "密码将在几天后过期",
"MAXAGEDAYS": "最大有效期 (天)",
"USERLOGINMUSTBEDOMAIN": "用户名必须包含组织域名",
"USERLOGINMUSTBEDOMAIN_DESCRIPTION": "如果启用此设置,所有登录名都将以组织域为后缀。如果禁用此设置,您必须确保用户名在所有组织中都是唯一的。",
"VALIDATEORGDOMAINS": "组织域名验证需要 (DNS 或 HTTP 挑战)",
@ -2540,7 +2543,8 @@
"mk": "Македонски",
"cs": "Čeština",
"ru": "Русский",
"nl": "Nederlands"
"nl": "Nederlands",
"sv": "Svenska"
},
"MEMBER": {
"ADD": "添加管理者",

1
docs/.gitignore vendored
View File

@ -26,3 +26,4 @@ package-lock.json
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.vercel

View File

@ -1,13 +0,0 @@
## Angular lint workspace and production build
FROM node:18 as builder
WORKDIR /docs
COPY docs/package.json docs/yarn.lock ./
RUN yarn install --frozen-lockfile
COPY docs .
COPY proto /proto
RUN yarn build
## Final image for serving
FROM nginx as final
COPY docs/nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /docs/build /usr/share/nginx/html

View File

@ -1,4 +1,4 @@
module.exports = {
presets: [require.resolve("@docusaurus/core/lib/babel/preset")],
compact: true
compact: auto
};

View File

@ -0,0 +1,123 @@
syntax = "proto3";
package zitadel.resources.action.v3alpha;
import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/struct.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/protoc_gen_zitadel/v2/options.proto";
import "zitadel/resources/object/v3alpha/object.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action";
message Execution {
Condition condition = 1;
// Target IDs which are called when the defined conditions trigger.
repeated string targets = 2;
// Included executions with the same condition-types.
repeated string includes = 3;
}
message GetExecution {
zitadel.resources.object.v3alpha.Details details = 1;
Execution execution = 2;
}
message Condition {
// Condition-types under which conditions the execution should trigger. Only one is possible.
oneof condition_type {
option (validate.required) = true;
// Condition-type to execute after a request on the defined API point is received.
RequestExecution request = 1;
// Condition-type to execute before a response on the defined API point is returned.
ResponseExecution response = 2;
// Condition-type to execute when a function is used, replaces actions v1.
string function = 3;
// Condition-type to execute after an event is created in the system.
EventExecution event = 4;
}
}
message RequestExecution {
// Condition for the request execution. Only one is possible.
oneof condition{
// GRPC-method as condition.
string method = 1 [
(validate.rules).string = {min_len: 1, max_len: 1000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 1000,
example: "\"/zitadel.session.v2beta.SessionService/ListSessions\"";
}
];
// GRPC-service as condition.
string service = 2 [
(validate.rules).string = {min_len: 1, max_len: 1000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 1000,
example: "\"zitadel.session.v2beta.SessionService\"";
}
];
// All calls to any available services and methods as condition.
bool all = 3;
}
}
message ResponseExecution {
// Condition for the response execution. Only one is possible.
oneof condition{
// GRPC-method as condition.
string method = 1 [
(validate.rules).string = {min_len: 1, max_len: 1000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 1000,
example: "\"/zitadel.session.v2beta.SessionService/ListSessions\"";
}
];
// GRPC-service as condition.
string service = 2 [
(validate.rules).string = {min_len: 1, max_len: 1000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 1000,
example: "\"zitadel.session.v2beta.SessionService\"";
}
];
// All calls to any available services and methods as condition.
bool all = 3;
}
}
message EventExecution{
// Condition for the event execution. Only one is possible.
oneof condition{
// Event name as condition.
string event = 1 [
(validate.rules).string = {min_len: 1, max_len: 1000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 1000,
example: "\"user.human.added\"";
}
];
// Event group as condition, all events under this group.
string group = 2 [
(validate.rules).string = {min_len: 1, max_len: 1000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 1000,
example: "\"user.human\"";
}
];
// all events as condition.
bool all = 3;
}
}

View File

@ -0,0 +1,111 @@
syntax = "proto3";
package zitadel.resources.action.v3alpha;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action";
import "google/api/field_behavior.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/resources/object/v3alpha/object.proto";
import "zitadel/resources/action/v3alpha/execution.proto";
message ExecutionSearchFilter {
oneof filter {
option (validate.required) = true;
InConditionsFilter in_conditions = 1;
ExecutionTypeFilter execution_type = 2;
TargetFilter target = 3;
IncludeFilter include = 4;
}
}
message InConditionsFilter {
// Defines the conditions to query for.
repeated Condition conditions = 1;
}
message ExecutionTypeFilter {
// Defines the type to query for.
ExecutionType type = 1;
}
message TargetFilter {
// Defines the id to query for.
string id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "the id of the targets to include"
example: "\"69629023906488334\"";
}
];
}
message IncludeFilter {
// Defines the include to query for.
string include = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "the id of the include"
example: "\"request.zitadel.session.v2beta.SessionService\"";
}
];
}
message TargetSearchFilter {
oneof query {
option (validate.required) = true;
TargetNameFilter name = 1;
InTargetIDsFilter in_ids = 2;
}
}
message TargetNameFilter {
// Defines the name of the target to query for.
string name = 1 [
(validate.rules).string = {max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
max_length: 200;
example: "\"ip_allow_list\"";
}
];
// Defines which text comparison method used for the name query.
zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [
(validate.rules).enum.defined_only = true,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines which text equality method is used";
}
];
}
message InTargetIDsFilter {
// Defines the ids to query for.
repeated string ids = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "the ids of the targets to include"
example: "[\"69629023906488334\",\"69622366012355662\"]";
}
];
}
enum ExecutionType {
EXECUTION_TYPE_UNSPECIFIED = 0;
EXECUTION_TYPE_REQUEST = 1;
EXECUTION_TYPE_RESPONSE = 2;
EXECUTION_TYPE_EVENT = 3;
EXECUTION_TYPE_FUNCTION = 4;
}
enum TargetFieldName {
TARGET_FIELD_NAME_UNSPECIFIED = 0;
TARGET_FIELD_NAME_ID = 1;
TARGET_FIELD_NAME_CREATION_DATE = 2;
TARGET_FIELD_NAME_CHANGE_DATE = 3;
TARGET_FIELD_NAME_NAME = 4;
TARGET_FIELD_NAME_TARGET_TYPE = 5;
TARGET_FIELD_NAME_URL = 6;
TARGET_FIELD_NAME_TIMEOUT = 7;
TARGET_FIELD_NAME_ASYNC = 8;
TARGET_FIELD_NAME_INTERRUPT_ON_ERROR = 9;
}

View File

@ -0,0 +1,554 @@
syntax = "proto3";
package zitadel.resources.action.v3alpha;
import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/struct.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/protoc_gen_zitadel/v2/options.proto";
import "zitadel/resources/action/v3alpha/target.proto";
import "zitadel/resources/action/v3alpha/execution.proto";
import "zitadel/resources/action/v3alpha/search.proto";
import "zitadel/resources/object/v3alpha/object.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action";
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
info: {
title: "Action Service";
version: "3.0-alpha";
description: "This API is intended to manage custom executions (previously known as actions) in a ZITADEL instance. It is behind the feature flag \"multitenancy_resources_api\". It will continue breaking as long as it is in alpha state.";
contact:{
name: "ZITADEL"
url: "https://zitadel.com"
email: "hi@zitadel.com"
}
license: {
name: "Apache 2.0",
url: "https://github.com/zitadel/zitadel/blob/main/LICENSE";
};
};
schemes: HTTPS;
schemes: HTTP;
consumes: "application/json";
consumes: "application/grpc";
produces: "application/json";
produces: "application/grpc";
consumes: "application/grpc-web+proto";
produces: "application/grpc-web+proto";
host: "$ZITADEL_DOMAIN";
base_path: "/resources/v3alpha";
external_docs: {
description: "Detailed information about ZITADEL",
url: "https://zitadel.com/docs"
}
security_definitions: {
security: {
key: "OAuth2";
value: {
type: TYPE_OAUTH2;
flow: FLOW_ACCESS_CODE;
authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize";
token_url: "$CUSTOM-DOMAIN/oauth/v2/token";
scopes: {
scope: {
key: "openid";
value: "openid";
}
scope: {
key: "urn:zitadel:iam:org:project:id:zitadel:aud";
value: "urn:zitadel:iam:org:project:id:zitadel:aud";
}
}
}
}
}
security: {
security_requirement: {
key: "OAuth2";
value: {
scope: "openid";
scope: "urn:zitadel:iam:org:project:id:zitadel:aud";
}
}
}
responses: {
key: "403";
value: {
description: "Returned when the user does not have permission to access the resource.";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
}
}
}
}
responses: {
key: "404";
value: {
description: "Returned when the resource does not exist.";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
}
}
}
}
};
service ZITADELActions {
// Create a target
//
// Create a new target, which can be used in executions.
rpc CreateTarget (CreateTargetRequest) returns (CreateTargetResponse) {
option (google.api.http) = {
post: "/targets"
body: "target"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "action.target.write"
}
http_response: {
success_code: 201
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "201";
value: {
description: "Target successfully created";
schema: {
json_schema: {
ref: "#/definitions/v2CreateTargetResponse";
}
}
};
};
};
}
// Patch a target
//
// Patch an existing target.
rpc PatchTarget (PatchTargetRequest) returns (PatchTargetResponse) {
option (google.api.http) = {
patch: "/targets/{id}"
body: "target"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "action.target.write"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "Target successfully updated";
};
};
};
}
// Delete a target
//
// Delete an existing target. This will remove it from any configured execution as well.
rpc DeleteTarget (DeleteTargetRequest) returns (DeleteTargetResponse) {
option (google.api.http) = {
delete: "/targets/{id}"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "action.target.delete"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "Target successfully deleted";
};
};
};
}
// Target by ID
//
// Returns the target identified by the requested ID.
rpc GetTarget (GetTargetRequest) returns (GetTargetResponse) {
option (google.api.http) = {
get: "/targets/{id}"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "action.target.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200"
value: {
description: "Target successfully retrieved";
}
};
};
}
// Search targets
//
// Search all matching targets. By default, we will return all targets of your instance.
// Make sure to include a limit and sorting for pagination.
rpc SearchTargets (SearchTargetsRequest) returns (SearchTargetsResponse) {
option (google.api.http) = {
post: "/targets/_search",
body: "filters"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "action.target.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "A list of all targets matching the query";
};
};
responses: {
key: "400";
value: {
description: "invalid list query";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
};
};
};
};
};
}
// Put an execution to call a target or include the targets of another execution.
//
// Creates an execution for the given condition if it doesn't exists.
// Otherwise, the existing execution is updated.
rpc PutExecution (PutExecutionRequest) returns (PutExecutionResponse) {
option (google.api.http) = {
post: "/executions"
body: "execution"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "action.execution.write"
}
http_response: {
success_code: 201
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "201";
value: {
description: "Execution successfully created";
schema: {
json_schema: {
ref: "#/definitions/v2CreateExecutionResponse";
}
}
};
};
responses: {
key: "200";
value: {
description: "Execution successfully updated";
};
};
};
}
// Delete an execution
//
// Delete an existing execution.
rpc DeleteExecution (DeleteExecutionRequest) returns (DeleteExecutionResponse) {
option (google.api.http) = {
delete: "/executions/{id}"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "action.execution.delete"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "Execution successfully deleted";
};
};
};
}
// Search executions
//
// Search all matching executions. By default, we will return all executions of your instance.
// Depending on the ZITADEL configuration, the number of returned resources is most probably limited.
// To make sure you get deterministic results, sort and paginate by the resources creation dates.
rpc SearchExecutions (SearchExecutionsRequest) returns (SearchExecutionsResponse) {
option (google.api.http) = {
post: "/executions/_search"
body: "filters"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "execution.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "A list of all executions matching the query";
};
};
responses: {
key: "400";
value: {
description: "invalid list query";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
};
};
};
};
};
}
// List all available functions
//
// List all available functions which can be used as condition for executions.
rpc ListAvailableExecutionFunctions (ListAvailableExecutionFunctionsRequest) returns (ListAvailableExecutionFunctionsResponse) {
option (google.api.http) = {
get: "/executions/functions"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "execution.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "List all functions successfully";
};
};
};
}
// List all available methods
//
// List all available methods which can be used as condition for executions.
rpc ListAvailableExecutionMethods (ListAvailableExecutionMethodsRequest) returns (ListAvailableExecutionMethodsResponse) {
option (google.api.http) = {
get: "/executions/methods"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "execution.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "List all methods successfully";
};
};
};
}
// List all available service
//
// List all available services which can be used as condition for executions.
rpc ListAvailableExecutionServices (ListAvailableExecutionServicesRequest) returns (ListAvailableExecutionServicesResponse) {
option (google.api.http) = {
get: "/executions/services"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "execution.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "List all services successfully";
};
};
};
}
}
message CreateTargetRequest {
Target target = 2;
}
message CreateTargetResponse {
zitadel.resources.object.v3alpha.Details details = 2;
}
message PatchTargetRequest {
string id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"69629026806489455\"";
}
];
PatchTarget target = 2;
}
message PatchTargetResponse {
zitadel.resources.object.v3alpha.Details details = 1;
}
message DeleteTargetRequest {
string id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"69629026806489455\"";
}
];
}
message DeleteTargetResponse {
zitadel.resources.object.v3alpha.Details details = 1;
}
message SearchTargetsRequest {
// list limitations and ordering.
zitadel.resources.object.v3alpha.ListQuery query = 2;
// the field the result is sorted.
zitadel.resources.action.v3alpha.TargetFieldName sorting_column = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"FIELD_NAME_SCHEMA_TYPE\""
}
];
// Define the criteria to query for.
repeated zitadel.resources.action.v3alpha.TargetSearchFilter filters = 4;
}
message SearchTargetsResponse {
zitadel.resources.object.v3alpha.ListDetails details = 1;
zitadel.resources.action.v3alpha.TargetFieldName sorting_column = 2;
repeated zitadel.resources.action.v3alpha.GetTarget result = 3;
}
message GetTargetRequest {
// unique identifier of the target.
string id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"69629026806489455\"";
}
];
}
message GetTargetResponse {
zitadel.resources.action.v3alpha.GetTarget target = 1;
}
message PutExecutionRequest {
Execution execution = 2;
}
message PutExecutionResponse {
zitadel.resources.object.v3alpha.Details details = 2;
}
message DeleteExecutionRequest {
string id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"69629026806489455\"";
}
];
}
message DeleteExecutionResponse {
zitadel.resources.object.v3alpha.Details details = 1;
}
message SearchExecutionsRequest {
// list limitations and ordering.
zitadel.resources.object.v3alpha.ListQuery query = 1;
// Define the criteria to query for.
repeated zitadel.resources.action.v3alpha.ExecutionSearchFilter filters = 2;
}
message SearchExecutionsResponse {
zitadel.resources.object.v3alpha.ListDetails details = 1;
repeated zitadel.resources.action.v3alpha.GetExecution result = 2;
}
message ListAvailableExecutionFunctionsRequest{}
message ListAvailableExecutionFunctionsResponse{
// All available functions
repeated string functions = 1;
}
message ListAvailableExecutionMethodsRequest{}
message ListAvailableExecutionMethodsResponse{
// All available methods
repeated string methods = 1;
}
message ListAvailableExecutionServicesRequest{}
message ListAvailableExecutionServicesResponse{
// All available services
repeated string services = 1;
}

View File

@ -0,0 +1,94 @@
syntax = "proto3";
package zitadel.resources.action.v3alpha;
import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/struct.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/protoc_gen_zitadel/v2/options.proto";
import "zitadel/resources/object/v3alpha/object.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action";
message Target {
string name = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"ip_allow_list\"";
}
];
// Defines the target type and how the response of the target is treated.
oneof target_type {
SetRESTWebhook rest_webhook = 4;
SetRESTRequestResponse rest_request_response = 5;
}
// Timeout defines the duration until ZITADEL cancels the execution.
google.protobuf.Duration timeout = 6 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"10s\"";
}
];
oneof execution_type {
// Set the execution to run asynchronously.
bool is_async = 7;
// Define if any error stops the whole execution. By default the process continues as normal.
bool interrupt_on_error = 8;
}
}
message GetTarget {
zitadel.resources.object.v3alpha.Details details = 1;
Target target = 2;
}
message PatchTarget {
optional string name = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"ip_allow_list\"";
}
];
// Defines the target type and how the response of the target is treated.
oneof target_type {
SetRESTWebhook rest_webhook = 3;
SetRESTRequestResponse rest_request_response = 4;
}
// Timeout defines the duration until ZITADEL cancels the execution.
optional google.protobuf.Duration timeout = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"10s\"";
}
];
oneof execution_type {
// Set the execution to run asynchronously.
bool is_async = 6;
// Define if any error stops the whole execution. By default the process continues as normal.
bool interrupt_on_error = 7;
}
}
message SetRESTWebhook {
string url = 1 [
(validate.rules).string = {min_len: 1, max_len: 1000, uri: true},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 1000,
example: "\"https://example.com/hooks/ip_check\"";
}
];
}
message SetRESTRequestResponse {
string url = 1 [
(validate.rules).string = {min_len: 1, max_len: 1000, uri: true},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 1000,
example: "\"https://example.com/hooks/ip_check\"";
}
];
}

View File

@ -0,0 +1,94 @@
syntax = "proto3";
package zitadel.resources.idp.v3alpha;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/idp/v3alpha;idp";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/resources/object/v3alpha/object.proto";
message IDP {
string name = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"GitLab\"";
}
];
zitadel.resources.object.v3alpha.StatePolicy state_policy = 2;
Options options = 3;
}
message PatchIDP {
optional string name = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"GitLab\"";
}
];
optional zitadel.resources.object.v3alpha.StatePolicy state_policy = 2;
optional Options options = 3;
}
message GetIDP {
zitadel.resources.object.v3alpha.Details details = 1;
optional zitadel.resources.object.v3alpha.Parent parent = 2;
zitadel.resources.object.v3alpha.State state = 3;
ProviderType type = 4;
IDP idp = 5;
}
enum ProviderType {
PROVIDER_TYPE_UNSPECIFIED = 0;
PROVIDER_TYPE_OIDC = 1;
PROVIDER_TYPE_JWT = 2;
PROVIDER_TYPE_LDAP = 3;
PROVIDER_TYPE_OAUTH = 4;
PROVIDER_TYPE_AZURE_AD = 5;
PROVIDER_TYPE_GITHUB = 6;
PROVIDER_TYPE_GITHUB_ES = 7;
PROVIDER_TYPE_GITLAB = 8;
PROVIDER_TYPE_GITLAB_SELF_HOSTED = 9;
PROVIDER_TYPE_GOOGLE = 10;
PROVIDER_TYPE_APPLE = 11;
PROVIDER_TYPE_SAML = 12;
}
enum AutoLinkingOption {
// AUTO_LINKING_OPTION_UNSPECIFIED disables the auto linking prompt.
AUTO_LINKING_OPTION_UNSPECIFIED = 0;
// AUTO_LINKING_OPTION_USERNAME will use the username of the external user to check for a corresponding ZITADEL user.
AUTO_LINKING_OPTION_USERNAME = 1;
// AUTO_LINKING_OPTION_EMAIL will use the email of the external user to check for a corresponding ZITADEL user with the same verified email
// Note that in case multiple users match, no prompt will be shown.
AUTO_LINKING_OPTION_EMAIL = 2;
}
message Options {
bool is_linking_allowed = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Enable if users should be able to link an existing ZITADEL user with an external account.";
}
];
bool is_creation_allowed = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Enable if users should be able to create a new account in ZITADEL when using an external account.";
}
];
bool is_auto_creation = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Enable if a new account in ZITADEL should be created automatically when login with an external account.";
}
];
bool is_auto_update = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Enable if a the ZITADEL account fields should be updated automatically on each login.";
}
];
AutoLinkingOption auto_linking = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Enable if users should get prompted to link an existing ZITADEL user to an external account if the selected attribute matches.";
}
];
}

View File

@ -0,0 +1,59 @@
syntax = "proto3";
package zitadel.resources.idp.v3alpha;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/idp/v3alpha;idp";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/resources/object/v3alpha/object.proto";
import "zitadel/resources/idp/v3alpha/idp.proto";
message GetGitLabIDP {
zitadel.resources.object.v3alpha.Details details = 1;
optional zitadel.resources.object.v3alpha.Parent parent = 2;
zitadel.resources.object.v3alpha.State state = 3;
ProviderType type = 4;
GitLabIDP idp = 5;
}
message GitLabIDP {
IDP idp = 1;
GitLabConfig config = 2;
}
message PatchGitLabIDP {
optional PatchIDP idp = 1;
optional PatchGitLabConfig config = 2;
}
message GitLabConfig {
string client_id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"client-id\"";
description: "client id of the GitLab application";
}
];
repeated string scopes = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"openid\", \"profile\", \"email\"]";
description: "the scopes requested by ZITADEL during the request to GitLab";
}
];
}
message PatchGitLabConfig {
optional string client_id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"client-id\"";
description: "client id of the GitLab application";
}
];
repeated string scopes = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"openid\", \"profile\", \"email\"]";
description: "the scopes requested by ZITADEL during the request to GitLab";
}
];
}

View File

@ -0,0 +1,47 @@
syntax = "proto3";
package zitadel.resources.idp.v3alpha;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/idp/v3alpha;idp";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/resources/object/v3alpha/object.proto";
enum IDPFieldName {
IDP_FIELD_NAME_UNSPECIFIED = 0;
IDP_FIELD_NAME_NAME = 1;
}
message IDPSearchFilter {
oneof filter {
IDPIDFilter id = 1;
IDPNameFilter name = 2;
resources.object.v3alpha.StateFilter state = 3;
}
}
message IDPIDFilter {
string id = 1 [
(validate.rules).string = {max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629023906488334\"";
}
];
}
message IDPNameFilter {
string name = 1 [
(validate.rules).string = {max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"google\"";
}
];
zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [
(validate.rules).enum.defined_only = true,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines which text equality method is used";
}
];
}

View File

@ -0,0 +1,325 @@
syntax = "proto3";
package zitadel.resources.idp.v3alpha;
import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/struct.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/protoc_gen_zitadel/v2/options.proto";
import "zitadel/resources/object/v3alpha/object.proto";
import "zitadel/resources/idp/v3alpha/search.proto";
import "zitadel/resources/idp/v3alpha/idp.proto";
import "zitadel/resources/idp/v3alpha/gitlab.proto";
import "zitadel/object/v3alpha/object.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/idp/v3alpha;idp";
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
info: {
title: "Identity Provider Service";
version: "3.0-alpha";
description: "This API is intended to manage identity providers (IDPs). IDPs can be created for specific organizations or for an instance. IDPs created on an instance can be activated (reused) or deactivated in organizations. It is behind the feature flag \"multitenancy_resources_api\". It will continue breaking as long as it is in alpha state.";
contact:{
name: "ZITADEL"
url: "https://zitadel.com"
email: "hi@zitadel.com"
}
license: {
name: "Apache 2.0",
url: "https://github.com/zitadel/zitadel/blob/main/LICENSE";
};
};
schemes: HTTPS;
schemes: HTTP;
consumes: "application/json";
consumes: "application/grpc";
produces: "application/json";
produces: "application/grpc";
consumes: "application/grpc-web+proto";
produces: "application/grpc-web+proto";
host: "$ZITADEL_DOMAIN";
base_path: "/resources/v3alpha";
external_docs: {
description: "Detailed information about ZITADEL",
url: "https://zitadel.com/docs"
}
security_definitions: {
security: {
key: "OAuth2";
value: {
type: TYPE_OAUTH2;
flow: FLOW_ACCESS_CODE;
authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize";
token_url: "$CUSTOM-DOMAIN/oauth/v2/token";
scopes: {
scope: {
key: "openid";
value: "openid";
}
scope: {
key: "urn:zitadel:iam:org:project:id:zitadel:aud";
value: "urn:zitadel:iam:org:project:id:zitadel:aud";
}
}
}
}
}
security: {
security_requirement: {
key: "OAuth2";
value: {
scope: "openid";
scope: "urn:zitadel:iam:org:project:id:zitadel:aud";
}
}
}
responses: {
key: "403";
value: {
description: "Returned when the user does not have permission to access the resource.";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
}
}
}
}
responses: {
key: "404";
value: {
description: "Returned when the resource does not exist.";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
}
}
}
}
};
service ZITADELIdentityProviders {
// Create a GitLab IDP
rpc CreateGitLabIDP (CreateGitLabIDPRequest) returns (CreateGitLabIDPResponse) {
option (google.api.http) = {
post: "/idps/gitlab"
body: "idp"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "idp.write"
}
http_response: {
success_code: 201
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "201";
value: {
description: "GitLabIDP successfully created";
schema: {
json_schema: {
ref: "#/definitions/v2CreateGitLabIDPResponse";
}
}
};
};
};
}
// Patch a GitLab IDP
rpc PatchGitLabIDP (PatchGitLabIDPRequest) returns (PatchGitLabIDPResponse) {
option (google.api.http) = {
patch: "/idps/gitlab/{id}"
body: "idp"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "idp.write"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "GitLabIDP successfully updated";
};
};
};
}
// Find a GitLab IDP by ID
rpc GetGitLabIDP (GetGitLabIDPRequest) returns (GetGitLabIDPResponse) {
option (google.api.http) = {
get: "/idps/gitlab/{id}"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "idp.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200"
value: {
description: "GitLabIDP successfully retrieved";
}
};
};
}
// Delete an IDP of any type
rpc DeleteIDP (DeleteIDPRequest) returns (DeleteIDPResponse) {
option (google.api.http) = {
delete: "/idps/{id}"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "idp.delete"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "Identity provider successfully deleted";
};
};
};
}
// Search IDPs
//
// Search all matching IDPs. By default, all instance-level and organization-level providers of all types are returned.
// Only type-agnostic properties are returned in the response.
// To get the full details of a specific IDP, use the specific types Get method.
// If you search by passing an organization context, the state and the state policy might be different than if you search within the default instance-level context.
// Make sure to include a limit and sorting for pagination.
rpc SearchIDPs (SearchIDPsRequest) returns (SearchIDPsResponse) {
option (google.api.http) = {
post: "/idps/_search",
body: "filters"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "idp.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "A list of all IDPs matching the query";
};
};
responses: {
key: "400";
value: {
description: "invalid list query";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
};
};
};
};
};
}
}
message CreateGitLabIDPRequest {
optional zitadel.object.v3alpha.RequestContext ctx = 1;
GitLabIDP idp = 2;
}
message CreateGitLabIDPResponse {
zitadel.resources.object.v3alpha.Details details = 2;
}
message PatchGitLabIDPRequest {
string id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"69629026806489455\"";
}
];
PatchGitLabIDP idp = 2;
}
message PatchGitLabIDPResponse {
zitadel.resources.object.v3alpha.Details details = 1;
}
message DeleteIDPRequest {
string id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"69629026806489455\"";
}
];
}
message DeleteIDPResponse {
zitadel.resources.object.v3alpha.Details details = 1;
}
message SearchIDPsRequest {
optional zitadel.object.v3alpha.RequestContext ctx = 1;
// list limitations and ordering.
zitadel.resources.object.v3alpha.ListQuery query = 2;
// the field the result is sorted.
zitadel.resources.idp.v3alpha.IDPFieldName sorting_column = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"FIELD_NAME_SCHEMA_TYPE\""
}
];
repeated zitadel.resources.idp.v3alpha.IDPSearchFilter filters = 4;
}
message SearchIDPsResponse {
zitadel.resources.object.v3alpha.ListDetails details = 1;
zitadel.resources.idp.v3alpha.IDPFieldName sorting_column = 2;
repeated zitadel.resources.idp.v3alpha.GetIDP result = 3;
}
message GetGitLabIDPRequest {
string id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"69629026806489455\"";
}
];
}
message GetGitLabIDPResponse {
zitadel.resources.idp.v3alpha.GetGitLabIDP idp = 1;
}

View File

@ -0,0 +1,69 @@
syntax = "proto3";
package zitadel.settings.language.v3alpha;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/language/v3alpha;language";
import "validate/validate.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "zitadel/settings/object/v3alpha/object.proto";
import "zitadel/object/v3alpha/object.proto";
message SetLanguageSettings {
optional zitadel.settings.object.v3alpha.Language default_language = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "default language for the current context"
example: "\"en\""
}
];
optional SetLanguages restricted_languages = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "To these languages, message texts and default login UI labels are translated to. Also, the discovery endpoint only lists these languages."
example: "[\"en\", \"de\"]"
}
];
}
message ResolvedLanguageSettings {
zitadel.settings.object.v3alpha.ResolvedString default_language = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "default language for the current context"
example: "\"en\""
}
];
ResolvedLanguages restricted_languages = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "To these languages, message texts and default login UI labels are translated to. Also, the discovery endpoint only lists these languages."
example: "[\"en\", \"de\"]"
}
];
ResolvedLanguages supported_languages = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "These languages are supported by the system. For simplicity, the field is of type ResolvedLanguages, even though the list is immutable and the owner is always of OWNER_TYPE_SYSTEM."
example: "[\"en\", \"de\", \"it\"]"
}
];
}
message SetLanguages {
repeated zitadel.settings.object.v3alpha.Language languages = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "List of languages to set"
example: "[\"en\", \"de\"]"
}
];
}
message ResolvedLanguages {
repeated zitadel.settings.object.v3alpha.Language value = 1[
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "List of languages"
example: "[\"en\", \"de\"]"
}
];
optional zitadel.object.v3alpha.Owner owner = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "If the value is inherited, the value is inherited from this owner.";
}
];
}

View File

@ -0,0 +1,158 @@
syntax = "proto3";
package zitadel.settings.language.v3alpha;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/language/v3alpha;language";
import "google/api/annotations.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "zitadel/object/v3alpha/object.proto";
import "zitadel/settings/object/v3alpha/object.proto";
import "zitadel/settings/language/v3alpha/language.proto";
import "zitadel/protoc_gen_zitadel/v2/options.proto";
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
info: {
title: "Language Settings Service";
version: "3.0-alpha";
description: "Language Service is intended to manage languages for ZITADEL. Enable the feature flag \"multitenancy_settings\" in order to activate it. Languages are settings, and are therefore inherited through the context hierarchy system -> instance -> org. It will continue breaking as long as it is in alpha state.";
contact:{
name: "ZITADEL"
url: "https://zitadel.com"
email: "hi@zitadel.com"
}
license: {
name: "Apache 2.0",
url: "https://github.com/zitadel/zitadel/blob/main/LICENSE";
};
};
schemes: HTTPS;
schemes: HTTP;
consumes: "application/json";
consumes: "application/grpc";
consumes: "application/grpc-web+proto";
produces: "application/json";
produces: "application/grpc";
produces: "application/grpc-web+proto";
host: "$ZITADEL_DOMAIN";
base_path: "/settings/v3alpha";
external_docs: {
description: "Detailed information about ZITADEL",
url: "https://zitadel.com/docs"
}
security_definitions: {
security: {
key: "OAuth2";
value: {
type: TYPE_OAUTH2;
flow: FLOW_ACCESS_CODE;
authorization_url: "$ZITADEL_DOMAIN/oauth/v2/authorize";
token_url: "$ZITADEL_DOMAIN/oauth/v2/token";
scopes: {
scope: {
key: "openid";
value: "openid";
}
scope: {
key: "urn:zitadel:iam:org:project:id:zitadel:aud";
value: "urn:zitadel:iam:org:project:id:zitadel:aud";
}
}
}
}
}
security: {
security_requirement: {
key: "OAuth2";
value: {
scope: "openid";
scope: "urn:zitadel:iam:org:project:id:zitadel:aud";
}
}
}
responses: {
key: "403";
value: {
description: "Returned when the user does not have permission to access the settings in the given context.";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
}
}
}
}
};
// ZITADELLanguageSettings is intended to manage languages for ZITADEL.
// Enable the feature flag \"multitenancy_settings\" in order to activate it.
// Languages are settings, and are therefore inherited through the context hierarchy system -> instance -> org.
service ZITADELLanguageSettings {
rpc SetLanguages (SetLanguageSettingsRequest) returns (SetLanguageSettingsResponse) {
option (google.api.http) = {
patch: "/languages"
body: "settings"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "authenticated"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Set languages for a given context";
description: "Configure and set languages for a given context. Only fields present in the request are set or unset."
responses: {
key: "200"
value: {
description: "OK";
}
};
};
};
rpc ResolveLanguages (ResolveLanguageSettingsRequest) returns (ResolveLanguageSettingsResponse) {
option (google.api.http) = {
get: "/languages"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "authenticated"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Get the languages in the given context";
description: "Returns all configured and inherited languages for the given context."
responses: {
key: "200"
value: {
description: "OK";
}
};
};
};
}
message SetLanguageSettingsRequest{
optional zitadel.object.v3alpha.RequestContext ctx = 1;
SetLanguageSettings settings = 2;
}
message SetLanguageSettingsResponse{
zitadel.settings.object.v3alpha.Details details = 1;
}
message ResolveLanguageSettingsRequest{
optional zitadel.object.v3alpha.RequestContext ctx = 1;
}
message ResolveLanguageSettingsResponse{
zitadel.settings.object.v3alpha.Details details = 1;
ResolvedLanguageSettings settings = 2;
}

View File

@ -0,0 +1,33 @@
syntax = "proto3";
package zitadel.object.v3alpha;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha;object";
import "google/protobuf/timestamp.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
message RequestContext {
// By default, the request context is set to the instance discovered by the domain from the requests host header.
oneof owner {
bool system = 1 [(validate.rules).bool = {const: true}]; // TODO: move the source of truth from the defaults.yaml into the database
string instance_id = 2;
string instance_domain = 3;
string org_id = 4;
string org_domain = 5;
}
}
enum OwnerType {
OWNER_TYPE_UNSPECIFIED = 0;
OWNER_TYPE_SYSTEM = 1; // TODO: move the source of truth from the defaults.yaml into the database
OWNER_TYPE_INSTANCE = 2;
OWNER_TYPE_ORG = 3;
}
message Owner {
OwnerType type = 1;
string id = 2;
}

View File

@ -0,0 +1,155 @@
syntax = "proto3";
package zitadel.resources.object.v3alpha;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha;object";
import "google/api/field_behavior.proto";
import "google/protobuf/timestamp.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/object/v3alpha/object.proto";
message Organization {
oneof org {
string org_id = 1;
string org_domain = 2;
}
}
message Details {
string id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629012906488334\"";
}
];
//sequence represents the order of events. It's always counting
//
// on read: the sequence of the last event reduced by the projection
//
// on manipulation: the timestamp of the event(s) added by the manipulation
uint64 sequence = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2\"";
}
];
//change_date is the timestamp when the object was changed
//
// on read: the timestamp of the last event reduced by the projection
//
// on manipulation: the timestamp of the event(s) added by the manipulation
google.protobuf.Timestamp change_date = 3;
//resource_owner represents the context an object belongs to
zitadel.object.v3alpha.Owner resource_owner = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629023906488334\"";
}
];
}
enum State {
EFFECTIVE_STATE_UNSPECIFIED = 0;
EFFECTIVE_STATE_ACTIVE = 1;
EFFECTIVE_STATE_INACTIVE = 2;
}
enum StatePolicy {
STATE_POLICY_UNSPECIFIED = 0;
STATE_POLICY_ACTIVATE = 1;
STATE_POLICY_DEACTIVATE = 2;
STATE_POLICY_INHERIT = 3;
}
message StateFilter {
// Defines the state to query for.
resources.object.v3alpha.State state = 1 [
(validate.rules).enum.defined_only = true,
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"STATE_ACTIVE\""
}
];
}
message Parent {
zitadel.object.v3alpha.Owner parent = 1;
State state = 2;
}
message ListQuery {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
json_schema: {
title: "General List Query"
description: "Object unspecific list filters like offset, limit and asc/desc."
}
};
uint64 offset = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"0\"";
}
];
uint32 limit = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "100";
description: "Maximum amount of events returned. The default is 100, the maximum is 1000. If the limit exceeds the maximum, ZITADEL throws an error.";
}
];
bool asc = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "default is descending"
}
];
}
message ListDetails {
uint32 applied_limit = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "100";
}
];
bool end_of_list = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
}
];
uint64 total_result = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2\"";
}
];
uint64 processed_sequence = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"267831\"";
}
];
google.protobuf.Timestamp timestamp = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "the last time the projection got updated"
}
];
}
enum TextFilterMethod {
TEXT_FILTER_METHOD_EQUALS = 0;
TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE = 1;
TEXT_FILTER_METHOD_STARTS_WITH = 2;
TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE = 3;
TEXT_FILTER_METHOD_CONTAINS = 4;
TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE = 5;
TEXT_FILTER_METHOD_ENDS_WITH = 6;
TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE = 7;
}
enum ListFilterMethod {
LIST_FILTER_METHOD_IN = 0;
}
enum TimestampFilterMethod {
TIMESTAMP_Filter_METHOD_EQUALS = 0;
TIMESTAMP_Filter_METHOD_GREATER = 1;
TIMESTAMP_Filter_METHOD_GREATER_OR_EQUALS = 2;
TIMESTAMP_Filter_METHOD_LESS = 3;
TIMESTAMP_Filter_METHOD_LESS_OR_EQUALS = 4;
}

View File

@ -0,0 +1,193 @@
syntax = "proto3";
package zitadel.settings.object.v3alpha;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/object/v3alpha;object";
import "google/protobuf/timestamp.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "google/protobuf/duration.proto";
import "zitadel/object/v3alpha/object.proto";
message Details {
//sequence represents the order of events. It's always counting
//
// on read: the sequence of the last event reduced by the projection
//
// on manipulation: the timestamp of the event(s) added by the manipulation
uint64 sequence = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2\"";
}
];
//change_date is the timestamp when the object was changed
//
// on read: the timestamp of the last event reduced by the projection
//
// on manipulation: the timestamp of the event(s) added by the manipulation
google.protobuf.Timestamp change_date = 2;
//resource_owner represents the context an object belongs to
zitadel.object.v3alpha.Owner owner = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629023906488334\"";
}
];
}
message ResolvedBool {
bool value = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "false";
description: "The resolved value is valid for the given context. Either the value was explicitly set for the given context or it is inherited from a higher-level context.";
}
];
optional zitadel.object.v3alpha.Owner owner = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "If the value is inherited, the value is inherited from this owner.";
}
];
}
message SetBool {
oneof value {
bool set = 1;
bool reset = 2 [(validate.rules).bool = {
const: true
}];
}
}
message ResolvedString {
string value = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"a resolved string\"";
description: "The resolved value is valid for the given context. Either the value was explicitly set for the given context or it is inherited from a higher-level context.";
}
];
optional zitadel.object.v3alpha.Owner owner = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "If the value is inherited, the value is inherited from this owner.";
}
];
}
message SetString {
oneof value {
string set = 1 [
(validate.rules).string = {
max_len: 256
}
];
bool reset = 2 [(validate.rules).bool = {
const: true
}];
}
}
message SetStringLong {
oneof value {
string set = 1 [
(validate.rules).string = {
max_len: 2048
}
];
bool reset = 2 [(validate.rules).bool = {
const: true
}];
}
}
message SetStringShort {
oneof value {
string set = 1 [
(validate.rules).string = {
max_len: 64
}
];
bool reset = 2 [(validate.rules).bool = {
const: true
}];
}
}
message Language {
string key = 1 [(validate.rules).string = {pattern: "^[a-z]{2}$"}];
}
message ResolvedStrings {
repeated string value = 1[
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"a\", \"resolved\", \"list\", \"of\", \"strings\"]";
description: "The resolved value is valid for the given context. Either the value was explicitly set for the given context or it is inherited from a higher-level context.";
}
];
optional zitadel.object.v3alpha.Owner owner = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "If the value is inherited, the value is inherited from this owner.";
}
];
}
message SetStrings {
oneof value {
SetStringsValue set = 1;
bool reset = 2 [(validate.rules).bool = {
const: true
}];
}
}
message SetStringsValue {
repeated string value = 1;
}
message ResolvedUInt64 {
uint64 value = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "1000";
description: "The resolved value is valid for the given context. Either the value was explicitly set for the given context or it is inherited from a higher-level context.";
}
];
optional zitadel.object.v3alpha.Owner owner = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "If the value is inherited, the value is inherited from this owner.";
}
];
}
message SetUInt64 {
oneof value {
uint64 set = 1;
bool reset = 2 [(validate.rules).bool = {
const: true
}];
}
}
message ResolvedDuration {
google.protobuf.Duration value = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"5s\"";
description: "The resolved value is valid for the given context. Either the value was explicitly set for the given context or it is inherited from a higher-level context.";
}
];
optional zitadel.object.v3alpha.Owner owner = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "If the value is inherited, the value is inherited from this owner.";
}
];
}
message SetDuration {
oneof value {
google.protobuf.Duration set = 1;
bool reset = 2 [(validate.rules).bool = {
const: true
}];
}
}

View File

@ -104,6 +104,22 @@ no additional parameters required
| prompt | If the Auth Server prompts the user for (re)authentication. <br />no prompt: the user will have to choose a session if more than one session exists<br />`none`: user must be authenticated without interaction, an error is returned otherwise <br />`login`: user must reauthenticate / provide a user name <br />`select_account`: user is prompted to select one of the existing sessions or create a new one <br />`create`: the registration form will be displayed to the user directly |
| state | Opaque value used to maintain state between the request and the callback. Used for Cross-Site Request Forgery (CSRF) mitigation as well, therefore highly **recommended**. |
| ui_locales | Spaces delimited list of preferred locales for the login UI, e.g. `de-CH de en`. If none is provided or matches the possible locales provided by the login UI, the `accept-language` header of the browser will be taken into account. |
| response_mode | The mechanism to be used for returning parameters to the application. See [response modes](#response-modes) for valid values. Invalid values are ignored. |
#### Response modes
ZITADEL supports the following `response_mode` values. When no response mode is requested, the response mode is choosen based on the configured Response Type of the application.
As per [OpenID Connect Core 1.0, Section 3.1.2.1](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest):
> The use of this parameter is NOT RECOMMENDED when the Response Mode that would be requested is the default mode specified for the Response Type.
| Response Mode | Description |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| query | Encode the returned parameters in the URL query string. This is the default when the Response type is `code`, for example [Web applications](/docs/guides/manage/console/applications#web). |
| fragment | Encode the returned parameters in the URL fragment. This is the default when the Response Type is `id_token`, for example implicit [User Agent apps](/docs/guides/manage/console/applications#user-agent). This mode will not work for server-side applications, because fragments are never sent by the browser to the server. |
| form_post[^1] | ZITADEL serves a small JavaScript to the browser which will send the returned parameters to the `redirect_uri` using HTTP POST. This mode only works for server-side applications and user agents which support / allow JavaScript. |
[^1]: Implements [OAuth 2.0 Form Post Response Mode](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html)
### Successful code response

View File

@ -24,14 +24,17 @@ ZITADEL supports the usage of scopes as way of requesting information from the I
In addition to the standard compliant scopes we utilize the following scopes.
| Scopes | Example | Description |
|:--------------------------------------------------|:-------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| :------------------------------------------------ | :----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `urn:zitadel:iam:org:project:role:{rolekey}` | `urn:zitadel:iam:org:project:role:user` | By using this scope a client can request the claim `urn:zitadel:iam:org:project:roles` to be asserted when possible. As an alternative approach you can enable all roles to be asserted from the [project](/guides/manage/console/roles#authorizations) a client belongs to. |
| `urn:zitadel:iam:org:projects:roles` | `urn:zitadel:iam:org:projects:roles` | By using this scope a client can request the claim `urn:zitadel:iam:org:project:{projectid}:roles` to be asserted for each requested project. All projects of the token audience, requested by the `urn:zitadel:iam:org:project:id:{projectid}:aud` scopes will be used. |
| `urn:zitadel:iam:org:id:{id}` | `urn:zitadel:iam:org:id:178204173316174381` | When requesting this scope **ZITADEL** will enforce that the user is a member of the selected organization. If the organization does not exist a failure is displayed. It will assert the `urn:zitadel:iam:user:resourceowner` claims. |
| `urn:zitadel:iam:org:domain:primary:{domainname}` | `urn:zitadel:iam:org:domain:primary:acme.ch` | When requesting this scope **ZITADEL** will enforce that the user is a member of the selected organization and the username is suffixed by the provided domain. If the organization does not exist a failure is displayed |
| `urn:zitadel:iam:role:{rolename}` | | |
| `urn:zitadel:iam:org:roles:id:{orgID}` | `urn:zitadel:iam:org:roles:id:178204173316174381` | This scope can be used one or more times to limit the granted organization IDs in the returned roles. Unknown organization IDs are ignored. When this scope is not used, all granted organizations are returned inside the roles.[^1] |
| `urn:zitadel:iam:org:project:id:{projectid}:aud` | `urn:zitadel:iam:org:project:id:69234237810729019:aud` | By adding this scope, the requested projectid will be added to the audience of the access token |
| `urn:zitadel:iam:org:project:id:zitadel:aud` | `urn:zitadel:iam:org:project:id:zitadel:aud` | By adding this scope, the ZITADEL project ID will be added to the audience of the access token |
| `urn:zitadel:iam:user:metadata` | `urn:zitadel:iam:user:metadata` | By adding this scope, the metadata of the user will be included in the token. The values are base64 encoded. |
| `urn:zitadel:iam:user:resourceowner` | `urn:zitadel:iam:user:resourceowner` | By adding this scope, the resourceowner (id, name, primary_domain) of the user will be included in the token. |
| `urn:zitadel:iam:org:idp:id:{idp_id}` | `urn:zitadel:iam:org:idp:id:76625965177954913` | By adding this scope the user will directly be redirected to the identity provider to authenticate. Make sure you also send the primary domain scope if a custom login policy is configured. Otherwise the system will not be able to identify the identity provider. |
[^1]: `urn:zitadel:iam:org:roles:id:{orgID}` is not supported when the `oidcLegacyIntrospection` [feature flag](/docs/apis/resources/feature_service_v2/feature-service-set-instance-features) is enabled.

View File

@ -3,13 +3,338 @@ title: APIs V3 (Preview)
---
import DocCardList from '@theme/DocCardList';
import CodeBlock from '@theme/CodeBlock';
import ActionServiceProto from '!!raw-loader!./_v3_action_service.proto'
import ActionExecutionProto from '!!raw-loader!./_v3_action_execution.proto'
import ActionTargetProto from '!!raw-loader!./_v3_action_target.proto'
import ActionSearchProto from '!!raw-loader!./_v3_action_search.proto'
import IDPServiceProto from '!!raw-loader!./_v3_idp_service.proto'
import IDPProto from '!!raw-loader!./_v3_idp.proto'
import IDPSearchProto from '!!raw-loader!./_v3_idp_search.proto'
import IDPGitLabProto from '!!raw-loader!./_v3_idp_gitlab.proto'
import LanguageServiceProto from '!!raw-loader!./_v3_language_service.proto'
import LanguageProto from '!!raw-loader!./_v3_language.proto'
import ObjectProto from '!!raw-loader!./_v3_object.proto'
import ResourceObjectProto from '!!raw-loader!./_v3_resource_object.proto'
import SettingsObjectProto from '!!raw-loader!./_v3_settings_object.proto'
APIs (V3) organize access by resources (users, settings, etc.), unlike context-specific V1 APIs.
This simplifies finding the right API, especially for multi-organization resources.
V3 also offers more flexibility over the V2 API in these points:
- User schema definition for custom user management.
- Behavior customization (API call manipulation, webhooks).
The APIs described in this section are currently either in *Preview* stage or not implemented, yet.
Before using these APIs, pleases consider the [API release policy below](#api-release-policy)
**Note**: V3 is currently in [Preview](/support/software-release-cycles-support#preview) and not yet generally available (breaking changes possible). Check individual services for availability.
## We Appreciate your Help
We invite you to...
- ... [discuss the concept with the ZITADEL community on GitHub](https://github.com/zitadel/zitadel/discussions/8125).
- ... try the implementations and provide feedback [by filing issues on GitHub](https://github.com/zitadel/zitadel/issues/new/choose).
## The Ideas behind the New V3 APIs
The current ZITADEL *GA* APIs are structured around contexts like System, Admin, Management, and Auth.
This structure leads to duplicate methods and makes it hard to find the right API for the right task.
Especially interacting with resources from multiple organizations is cumbersome.
Also, the APIs evolved over time, which lead to inconsistencies and a lack of flexibility in development.
We address these issues with the following new API categories:
- [Standard Resources](#standard-resources)
- [Reusable Resources](#reusable-resources)
- [Settings](#settings)
The designs for the new API categories aim for the following improvements:
### Service Structure
Instead of structuring the API methods around contexts, new APIs are structured around resources and settings.
This means, eventually, we deprecate the old System-, Admin-, Management- and AuthAPIs in favor of User-, Action-, Language-, FeatureAPIs and so on.
This change makes it easier to find the right API for the right task, especially for multi-organization resources.
Also, it allows for faster development and independent versioning of the APIs.
### Multitenancy Management and Consistency
To improve managing and reusing resources and settings in multitenancy scenarios, we define some rules for the new APIs:
- Single properties from instance settings are overridable (patchable) in organizations.
- Some settings support user-defined custom properties that are also overridable in organizations.
- Improved experience with reusing resources in multiple organizations and instances.
- Resources are searchable over all organizations with a single call by default.
### HTTP and gRPC Consistency
To make the APIs more consistent and easier to use, we follow the same patterns in all Proto files.
- Patching is favored over updating resources and settings.
- HTTP calls are mapped so that query parameters can be used as much as possible. We avoid the annotation `body: "*"`.
- For search performance, we enforce query limits.
## Standard Resources
Standard resources exist in exactly one context.
For example, a user is always assigned to exactly one organization.
Or one SMS provider is always assigned to exactly one instance.
Standard resource methods behave like this:
- Search request results can be scoped to a RequestContext.
- Search request results only contain results for which the requesting user has the necessary read permissions.
- Search requests are limited to 100 by default. The limit can be increased by the caller up to 1000 by default.
- Resource configurations are partially updatable. With HTTP, this is done via PATCH requests. If no changes were made, the response is successful.
- Status changes or other actions on resources with side effects are done via POST requests. Their HTTP path ends with the underscore prefixed action name. For example `POST /resources/users/{id}/_unlock`.
For a full proto example, have a look at the [ZITADELActions service](#zitadelactions).
## Reusable Resources
Reusable resources are like standard resources but can be reused in multiple contexts.
For example, an external identity provider can be defined once on the instance.
Each organization within this instance can then choose to use this identity provider or not.
Additionally to the methods described for standard resources, reusable have the following capabilities:
Reusable resources have the same behavior as standard resources with the following additions:
- Reusable resources can be created in a given context level (system, instance, org).
- For requests, that require a resource ID, no request context is needed.
- Reusable resources are available in child contexts, even if their state is *inactive*.
- The child context can control if an inherited resource should be active or inactive for itself using a state policy.
- In child contexts, the state policy of a reused resource is *inherit* by default and can be changed to *activate*, *deactivate* or back to *inherit*.
- In child contexts, a reused resources configuration is read-only.
- Child contexts can read at least the following properties of reused resources:
- ID
- name
- description
- state
- the state policy in the child context
- sequence
- last changed date
- parent context
- state in the immediate parent context.
- By default, search queries for reused resources return all resources from the given contexts, all inherited resources and all resources defined in all children contexts.
Typically, a new resource is first designed and implemented as a non-reusable resource.
If the community sees a benefit in reusing the resource in multiple contexts, reusability is added to the resource.
For a full proto example, have a look at the [ZITADELIdentityProviders service](#zitadelidentityproviders).
## Resource Services
All resource services by default support the following CRUD operations [as described above](#standard-resources-behavior).
- Create
- Read (get, search)
- Patch (partially update, success on no changes)
- Delete
### ZITADELActions
- Standard CRUD methods for Targets
- Standard CRUD methods for Executions except the PutExecution method replaces the CreateExecution and PatchExecution methods
Additional to the standard CRUD methods:
- ListAvailableExecutionServices
- ListAvailableExecutionMethods
- ListAvailableExecutionFunctions
<details><summary>action_service.proto</summary>
<CodeBlock language="protobuf">{ActionServiceProto}</CodeBlock>
</details>
<details><summary>action_target.proto</summary>
<CodeBlock language="protobuf">{ActionTargetProto}</CodeBlock>
</details>
<details><summary>action_execution.proto</summary>
<CodeBlock language="protobuf">{ActionExecutionProto}</CodeBlock>
</details>
<details><summary>action_query.proto</summary>
<CodeBlock language="protobuf">{ActionSearchProto}</CodeBlock>
</details>
### ZITADELUsers
Standard CRUD methods
### ZITADELUserSchemas
Standard CRUD methods
### ZITADELIdentityProviders
- Standard CRUD and methods for all IDPs
- Resources have additional properties for reusability capabilities.
<details><summary>idp_service.proto</summary>
<CodeBlock language="protobuf">{IDPServiceProto}</CodeBlock>
</details>
<details><summary>idp.proto</summary>
<CodeBlock language="protobuf">{IDPProto}</CodeBlock>
</details>
<details><summary>idp_search.proto</summary>
<CodeBlock language="protobuf">{IDPSearchProto}</CodeBlock>
</details>
<details><summary>idp_gitlab.proto</summary>
<CodeBlock language="protobuf">{IDPGitLabProto}</CodeBlock>
</details>
<details><summary>object.proto</summary>
<CodeBlock language="protobuf">{ObjectProto}</CodeBlock>
</details>
<details><summary>resource_object.proto</summary>
<CodeBlock language="protobuf">{ResourceObjectProto}</CodeBlock>
</details>
### ZITADELInstances
Additional to the standard CRUD methods:
- Limit (partial update of block and audit log retention)
- BulkLimit (partial update of block and audit log retention for multiple instances)
### ZITADELOrganizations
Additional to the standard CRUD methods:
- SetAsInstanceDefault
- GetInstanceDefault
### ZITADELDomains
Additional to the standard CRUD methods:
- SetAsPrimary
- Validate
### ZITADELSessions
Standard CRUD methods
### ZITADELProjects
Standard CRUD methods
### ZITADELApps
Standard CRUD methods
### ZITADELMemberships
The given context defines the organization, instance or system where the membership is created.
The context and the user ID together are unique.
Additional to the standard CRUD methods:
- ListAvailableRoles (context-aware)
### ZITADELGrants
- Standard CRUD methods for project grants
- Standard CRUD methods for user grants
- Standard CRUD methods for roles
### ZITADELSMTPProviders
Standard CRUD methods
### ZITADELSMSProviders
Standard CRUD methods
## Settings
Settings have no identity (ID) and are always context-aware.
They also don't have a state like active or inactive.
They only have properties that can be set and queried.
These properties are inherited to from parent-contexts (instance) to child-contexts (organization).
Settings behave like this:
- Setting and retrieving settings is always context-aware. By default, the context is the instance discovered by the requests *Host* header.
- All settings properties can be partially overridden in child-contexts.
- All settings properties can be partially reset in child-contexts, so their values default to the parent contexts property values.
- All settings properties returned by queries contain the value and if it is inherited, the context where it is inherited from.
For a full proto example, have a look at the [ZITADELLanguageSettings service](#zitadellanguagesettings).
## Settings Services
### ZITADELLanguageSettings
Default language, restricted languages, supported languages
<details><summary>language_service.proto</summary>
<CodeBlock language="protobuf">{LanguageServiceProto}</CodeBlock>
</details>
<details><summary>language.proto</summary>
<CodeBlock language="protobuf">{LanguageProto}</CodeBlock>
</details>
<details><summary>object.proto</summary>
<CodeBlock language="protobuf">{ObjectProto}</CodeBlock>
</details>
<details><summary>settings_object.proto</summary>
<CodeBlock language="protobuf">{SettingsObjectProto}</CodeBlock>
</details>
### ZITADELTextSettings
Key-value pairs for localized login texts, previously known as login texts
### ZITADELBrandingSettings
Predefined branding settings and custom key-value pairs, previously known as label policy or branding settings
### ZITADELLoginSettings
Previously known as login policy
### ZITADELLockoutSettings
Previously known as lockout policy
### ZITADELPasswordSettings
Previously known as password complexity policy
### ZITADELHelpSettings
Previously known as legal and support settings or privacy policy
### ZITADELDomainSettings
Previously known as domain policy
### ZITADELFeatureSettings
Feature toggles
Also contains disallow public org registrations on system and instance level.
### ZITADELTemplatesSettings
HTML and text templates for fully customizable emails and sms
### ZITADELSecretSettings
Replaces secret generators
## API Release Policy
- Defined but not yet implemented APIs are subject to change without further notice.
- Once an API definition is implemented, it is released as *Preview* and is available for testing.
- When a *Preview* API is tested enough so the concepts are proven to work, a new *Beta* API is released.
- When an API is feature-complete and stable enough, a new *GA* (General Availability) API is released.
- In all stages, changes to already implemented APIs are done in a backwards-compatible way, if possible.
- When we release a new stage for an API, we deprecate the previous stage and keep it available for a smooth transition.
## Preview APIs
These APIs are ready for testing and feedback.
Beware, they don't yet follow all the rules defined above.
<DocCardList />

View File

@ -32,7 +32,7 @@ Continue and Create the application.
After successful app creation a popup will appear showing you your clientID as well as a secret.
Copy your client ID and Secrets as it will be needed in the next step.
> Note: You will be able to regenerate the secret at a later time if you loose it.
> Note: You will be able to regenerate the secret at a later time if you lose it.
## OAuth 2.0 Proxy Setup

View File

@ -0,0 +1 @@
You have to install Pylon as described in [their documentation](https://pylon.cronit.io/docs/installation/).

View File

@ -0,0 +1,304 @@
---
title: ZITADEL with Pylon
sidebar_label: Pylon
---
import AppJWT from "../imports/_app_jwt.mdx";
import ServiceuserJWT from "../imports/_serviceuser_jwt.mdx";
import ServiceuserRole from "../imports/_serviceuser_role.mdx";
import SetupPylon from "../imports/_setup_pylon.mdx";
This integration guide demonstrates the recommended way to incorporate ZITADEL into your Pylon service.
It explains how to check the token validity in the API and how to check for permissions.
By the end of this guide, your application will have three different endpoint which are public, private(valid token) and private-scoped(valid token with specific role).
## ZITADEL setup
Before we can start building our application, we have to do a few configuration steps in ZITADEL Console.
### Create application
<AppJWT />
### Create Serviceuser
<ServiceuserJWT />
### Give Serviceuser an authorization
<ServiceuserRole />
### Prerequisites
At the end you should have the following for the API:
- Issuer, something like `https://example.zitadel.cloud` or `http://localhost:8080`
- `.json`-key-file for the API, from the application
- ID of the project
And the following from the Serviceuser:
- `.json`-key-file from the serviceuser
## Setup new Pylon service
### Setup Pylon
<SetupPylon />
### Creating a new project
To create a new Pylon project, run the following command:
```bash
pylon new my-pylon-project
```
This will create a new directory called `my-pylon-project` with a basic Pylon project structure.
### Project structure
Pylon projects are structured as follows:
```
my-pylon-project/
├── .pylon/
├── src/
│ ├── index.ts
├── package.json
├── tsconfig.json
```
- `.pylon/`: Contains the production build of your project.
- `src/`: Contains the source code of your project.
- `src/index.ts`: The entry point of your Pylon service.
- `package.json`: The npm package configuration file.
- `tsconfig.json`: The TypeScript configuration file.
### Basic example
Here's an example of a basic Pylon service:
```ts
import { defineService } from "@getcronit/pylon";
export default defineService({
Query: {
sum: (a: number, b: number) => a + b,
},
Mutation: {
divide: (a: number, b: number) => a / b,
},
});
```
## Secure the API
### Add ZITADEL info to the service
1. Create a `.env` file in the root folder of your project and add the following configuration:
```bash
AUTH_ISSUER='URL to the zitadel instance'
AUTH_PROJECT_ID='ID of the project'
```
It should look something like this:
```bash
AUTH_ISSUER='https://example.zitadel.cloud'
AUTH_PROJECT_ID='250719519163548112'
```
2. Copy the `.json`-key-file that you downloaded from the ZITADEL Console into the root folder of your project and rename it to `key.json`.
### Auth
Pylon provides a auth module and a decorator to check the validity of the token and the permissions.
- `auth.initialize()`: Initializes the authentication middleware.
- `auth.require()` : Middleware to check if the token is valid.
- `auth.require({roles: ['role']})`: Middleware to check if the token is valid and has the specified roles.
- `requireAuth()`: Decorator to check if the token is valid.
- `requireAuth({roles: ['role']})`: Decorator to check if the token is valid and has the specified roles.
### Build the Pylon service
Now we will create a new Pylon service with the following endpoints:
- `/api/public`: Public endpoint
- `/api/private`: Private endpoint
- `/api/private-scoped`: Private endpoint with specific role
- `/graphql`: GraphQL endpoint
- Query: `me`: Private endpoint that returns the current user and the messages if the role is `read:messages`
- Query: `info`: Public endpoint
### Create the service
The following code demonstrates how to create a Pylon service with the required endpoints, it must be added to the `src/index.ts` file of your project:
```ts
import {
defineService,
PylonAPI,
auth,
requireAuth,
getContext,
ServiceError,
} from "@getcronit/pylon";
class User {
id: string;
name: string;
#messages: string[];
constructor(id: string, name: string, messages: string[]) {
this.id = id;
this.name = name;
this.#messages = messages;
}
@requireAuth({ roles: ["read:messages"] })
async messages() {
return this.#messages;
}
static users: User[] = [];
@requireAuth()
static async me() {
const ctx = getContext();
const id = ctx.get("auth")!.sub;
const user = User.users.find((user) => user.id === id);
if (!user) {
throw new ServiceError("User not found", {
statusCode: 404,
code: "USER_NOT_FOUND",
});
}
return user;
}
@requireAuth()
static async create() {
const ctx = getContext();
const auth = ctx.get("auth")!;
// Check if the user already exists
if (User.users.find((user) => user.id === auth.sub)) {
throw new ServiceError("User already exists", {
statusCode: 400,
code: "USER_ALREADY_EXISTS",
});
}
const user = new User(auth.sub, auth.username || "unknown", [
"Welcome to Pylon with ZITADEL!",
]);
User.users.push(user);
return user;
}
}
export default defineService({
Query: {
me: User.me,
info: () => "Public Data",
},
Mutation: {
createUser: User.create,
},
});
export const configureApp: PylonAPI["configureApp"] = (app) => {
// Initialize the authentication middleware
app.use("*", auth.initialize());
// Automatically try to create a user for each request for demonstration purposes
app.use(async (_, next) => {
try {
await User.create();
} catch {
// Ignore errors
// Fail silently if the user already exists
}
await next();
});
app.get("/api/info", (c) => {
return new Response("Public Data");
});
// The `auth.require()` middleware is optional here, as the `User.me` method already checks for it.
app.get("/api/me", auth.require(), async (c) => {
const user = await User.me();
return c.json(user);
});
// A role check for `read:messages` is not required here, as the `user.messages` method already checks for it.
app.get("/api/me/messages", auth.require(), async (c) => {
const user = await User.me();
// This will throw an error if the user does not have the `read:messages` role
return c.json(await user.messages());
});
};
```
### Call the API
To call the API you need an access token, which is then verified by ZITADEL.
Please follow [this guide here](/docs/guides/integrate/token-introspection/private-key-jwt#get-an-access-token), ignoring the first step as we already have the `.json`-key-file from the serviceaccount.
:::info
You can also create a PAT for the serviceuser and use it to test the API. For this, follow [this guide](/docs/guides/integrate/service-users/personal-access-token#create-a-service-user-with-a-pat).
:::
Optionally set the token as an environment variable:
```
export TOKEN='MtjHodGy4zxKylDOhg6kW90WeEQs2q...'
```
Now you have to start the Pylon service:
```bash
bun run develop
```
With the access token, you can then do the following calls:
1. GraphQL:
```
curl -H "Authorization: Bearer $TOKEN" -G http://localhost:3000/graphql --data-urlencode 'query={ info }'
curl -H "Authorization: Bearer $TOKEN" -G http://localhost:3000/graphql --data-urlencode 'query={ me { id name } }'
curl -H "Authorization: Bearer $TOKEN" -G http://localhost:3000/graphql --data-urlencode 'query={ me { id name messages } }'
```
You can also visit the GraphQL playground at `http://localhost:3000/graphql` and execute the queries there.
2. Routes:
```
curl -H "Authorization: Bearer $TOKEN" -X GET http://localhost:3000/api/info
curl -H "Authorization: Bearer $TOKEN" -X GET http://localhost:3000/api/me
curl -H "Authorization: Bearer $TOKEN" -X GET http://localhost:3000/api/me/messages
```
## Completion
Congratulations! You have successfully integrated your Pylon with ZITADEL!
If you get stuck, consider checking out their [documentation](https://pylon.cronit.io/). If you face issues, contact Pylon or raise an issue on [GitHub](https://github.com/getcronit/pylon/issues).

View File

@ -12,6 +12,13 @@ sidebar_label: Username and Password
First, we create a new user with a username and password. In the example below we add a user with profile data, a verified email address, and a password.
[Create User Documentation](/apis/resources/user_service/user-service-add-human-user)
### Custom Fields
If you have custom fields you like to add to your users that are not provided by ZITADEL, you can add them to the metadata.
Metadata are key value pairs you can use for additional user data.
These fields can also be included in the token of the user, so you have access to it all the time.
Read more about the metadata [here](/docs/guides/manage/customize/user-metadata)
### Request
```bash

View File

@ -102,7 +102,7 @@ Please follow the configuration guides for the needed providers: [Let Users Logi
import OrgLoginDescription from "./_org_login_description.mdx";
<OrgLoginDescription name="OrgLodinDescription" />
<OrgLoginDescription name="OrgLoginDescription" />
### Build your own registration form
@ -117,8 +117,11 @@ We do have a guide series on how to build your own login ui, which also includes
You can find all the guides here: [Build your own login UI](/docs/guides/integrate/login-ui)
The create user request also allows you to add metadata (key, value) to the user.
#### Custom fields
The [create user request](/docs/apis/resources/user_service/user-service-add-human-user) also allows you to add [metadata](/docs/guides/manage/customize/user-metadata) (key, value) to the user.
This gives you the possibility to collect additional data from your users during the registration process and store it directly to the user in ZITADEL.
Those metadata can also directly be included in the [token](/docs/guides/manage/customize/user-metadata#use-tokens-to-get-user-metadata) of the user.
We recommend storing business relevant data in the database of your application, and only authentication and authorization relevant data in ZITADEL to follow the separation of concern pattern.
#### Registration with Organization External Identity Provider

View File

@ -48,6 +48,7 @@ ZITADEL is available in the following languages
- Czech (cs)
- Russian (ru)
- Dutch (nl)
- Swedish (sv)
A language is displayed based on your agent's language header.
If a users language header doesn't match any of the supported or [restricted](#restrict-languages) languages, the instances default language will be used.

View File

@ -219,6 +219,11 @@ An example response for your search looks like this:
}
```
## Register user with custom metadata
When you build your own registration UI you have the possibility to have custom fields and add them to the metadata of your user.
Learn everything about how to build your own registration UI [here](/docs/guides/integrate/onboarding/end-users#build-your-own-registration-form).
## Manage user metadata through the management API
The previous methods allowed you to retrieve metadata only for the `sub` in the access token.

View File

@ -0,0 +1,37 @@
---
title: ZITADEL Terraform Provider
sidebar_label: Terraform Provider
---
The [ZITADEL Terraform Provider](https://registry.terraform.io/providers/zitadel/zitadel/latest/docs) is a tool that allows you to manage ZITADEL resources through Terraform.
In other words, it lets you define and provision infrastructure for ZITADEL using Terraform configuration files.
This Terraform provider acts as a bridge, allowing you to manage various aspects of your ZITADEL instance directly through the [ZITADEL API](/docs/apis/introduction), using Terraform's declarative configuration language.
It can be used to create, update, and delete ZITADEL resources, as well as to manage the relationships between those resources.
## Before you start
Make sure you create the following resources in ZITADEL and have [Terraform installed](https://learn.hashicorp.com/tutorials/terraform/install-cli):
- [A ZITADEL Instance](../start/quickstart)
- [A service user](/docs/guides/integrate/service-users/authenticate-service-users) with [enough authorization](/docs/guides/manage/console/managers) to manage the desired resources
## Manage ZITADEL resources through terraform
The full documentation and examples are available on the [Terraform registry](https://registry.terraform.io/providers/zitadel/zitadel/latest/docs).
To provide a small guide to where to start:
1. Create a folder where all the terraform files reside.
2. Configure the provider to use the right domain, port and token, with for example a `main.tf`file [as shown in the example](https://registry.terraform.io/providers/zitadel/zitadel/latest/docs).
3. Add a `zitadel_org` resource to the `main.tf` file, to create and manage a new organization in the instance, [as shown in the example](https://registry.terraform.io/providers/zitadel/zitadel/latest/docs/resources/org).
4. Add any resources to the organization in the `main.tf` file, [as example a human user](https://registry.terraform.io/providers/zitadel/zitadel/latest/docs/resources/human_user).
5. (Optional) Use Terraform in the directory with the command `terraform plan`, to see which resources would be created and how.
6. Apply the changes and start managing your resources with terraform with `terraform apply`.
7. (Optional) Delete your created resources with `terraform destroy` to clean-up.
## References
- [Deploy ZITADEL in your infrastructure](/docs/self-hosting/deploy/overview)
- [ZITADEL CLI](/docs/self-hosting/manage/cli/overview)
- [Configuration Options in ZITADEL](/docs/self-hosting/manage/configure)

View File

@ -1,28 +0,0 @@
---
title: ZITADEL Terraform Provider
sidebar_label: Terraform Provider
---
It covers how to:
- Manage ZITADEL resources through the ZITADEL Terraform provider
Prerequisites:
- A ZITADEL Instance, if not present follow [this guide](../../start/quickstart)
- A user with enough authorization to manage the desired resources, if not present follow [this guide](/docs/guides/integrate/service-users/authenticate-service-users)
- Installed Terraform, if not present follow [this guide](https://learn.hashicorp.com/tutorials/terraform/install-cli)
## Manage ZITADEL resources through terraform
The full documentation and examples are available [here](https://registry.terraform.io/providers/zitadel/zitadel/latest/docs).
To provide a small guide to where to start:
1. Create a folder where all the terraform files reside.
2. Configure the provider to use the right domain, port and token, with for example a `main.tf`file [as shown in the example](https://registry.terraform.io/providers/zitadel/zitadel/latest/docs).
3. Add a `zitadel_org` resource to the `main.tf` file, to create and manage a new organization in the instance, [as shown in the example](https://registry.terraform.io/providers/zitadel/zitadel/latest/docs/resources/org).
4. Add any resources to the organization in the `main.tf` file, [as example a human user](https://registry.terraform.io/providers/zitadel/zitadel/latest/docs/resources/human_user).
5. (Optional) Use Terraform in the directory with the command `terraform plan`, to see which resources would be created and how.
6. Apply the changes and start managing your resources with terraform with `terraform apply`.
7. (Optional) Delete your created resources with `terraform destroy` to clean-up.

View File

@ -56,7 +56,7 @@ docker compose up --detach
mv ./machinekey/zitadel-admin-sa.json $HOME/zitadel-admin-sa.json
```
This key can be used to provision resources with for example [Terraform](/docs/guides/manage/terraform/basics.md).
This key can be used to provision resources with for example [Terraform](/docs/guides/manage/terraform-provider).
<Next components={props.components} />
<Disclaimer components={props.components} />

View File

@ -0,0 +1,33 @@
---
title: Developing ZITADEL with Dev Containers
sidebar_label: Dev Containers
---
Dev containers provide a convenient way to set up a development environment for ZITADEL with all the necessary dependencies pre-configured. This allows you to start contributing or working on ZITADEL locally with minimal setup.
## Prerequisites
- Docker installed on your machine. You can find installation instructions for Docker on their official website: https://docs.docker.com/engine/install/
- A code editor or IDE with remote container development capabilities (optional, but recommended). [Visual Studio Code](https://code.visualstudio.com) with the [Remote Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) is a popular option.
## Setting Up Dev Container
ZITADEL provides a `.devcontainer` folder that configures the development environment within a container. Here's how to get started:
1. Clone the ZITADEL repository from GitHub:
```bash
git clone https://github.com/zitadel/zitadel.git
```
2. Navigate to the project directory:
```bash
cd zitadel
```
3. Open the project in your code editor or IDE (if using one with remote container support).
4. Follow the instructions provided by your code editor/IDE to initiate the development container. This typically involves opening the "Command Palette" or similar functionality and searching for commands related to "Dev Containers" or "Remote Containers". The quick start guide for VS Code can found [here](https://code.visualstudio.com/docs/devcontainers/containers#_quick-start-open-an-existing-folder-in-a-container)
**Note**: The first time you run this command, it might take some time to download the container image.
## Using Dev Container
Once the container is running, you will have a development environment set up with all the necessary dependencies pre-installed. You can then follow the instructions in the ZITADEL [contribution guide](https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#developing-zitadel-with-dev-containers) to build and run ZITADEL, or develop the ZITADEL console application.

View File

@ -59,7 +59,7 @@ ZITADEL_DATABASE_POSTGRES_HOST=localhost ZITADEL_DATABASE_POSTGRES_PORT=5432 ZIT
mv /tmp/zitadel-admin-sa.json $HOME/zitadel-admin-sa.json
```
This key can be used to provision resources with for example [Terraform](/docs/guides/manage/terraform/basics.md).
This key can be used to provision resources with for example [Terraform](/docs/guides/manage/terraform-provider).
<Next components={props.components} />
<Disclaimer components={props.components} />

View File

@ -61,7 +61,7 @@ ZITADEL_DATABASE_POSTGRES_HOST=localhost ZITADEL_DATABASE_POSTGRES_PORT=5432 ZIT
mv /tmp/zitadel-admin-sa.json $HOME/zitadel-admin-sa.json
```
This key can be used to provision resources with for example [Terraform](/docs/guides/manage/terraform/basics.md).
This key can be used to provision resources with for example [Terraform](/docs/guides/manage/terraform-provider).
<Next components={props.components} />
<Disclaimer components={props.components} />

View File

@ -42,7 +42,7 @@ The following commands setup the database as described above. See [configuration
```bash
zitadel init --config /path/to/your/new/config.yaml
zitadel setup --for-init --config /path/to/your/new/config.yaml # make sure to set --tlsMode and masterkey analog to your current deployment
zitadel setup --for-mirror --config /path/to/your/new/config.yaml # make sure to set --tlsMode and masterkey analog to your current deployment
zitadel mirror --system --config /path/to/your/mirror/config.yaml # make sure to set --tlsMode and masterkey analog to your current deployment
```

View File

@ -207,7 +207,7 @@ DefaultInstance:
- If you don't want to use the DefaultInstance configuration for the first instance that ZITADEL automatically creates for you during the [setup phase](/self-hosting/manage/configure#database-initialization), you can provide a FirstInstance YAML section using the --steps argument.
- Learn how to configure ZITADEL via the [Console user interface](/guides/manage/console/overview).
- Probably, you also want to [apply your custom branding](/guides/manage/customize/branding), [hook into certain events](/guides/manage/customize/behavior), [customize texts](/guides/manage/customize/texts) or [add metadata to your users](/guides/manage/customize/user-metadata).
- If you want to automatically create ZITADEL resources, you can use the [ZITADEL Terraform Provider](/guides/manage/terraform/basics).
- If you want to automatically create ZITADEL resources, you can use the [ZITADEL Terraform Provider](/guides/manage/terraform-provider).
## Limits and Quotas

View File

@ -186,7 +186,7 @@ module.exports = {
selector: "div#",
},
prism: {
additionalLanguages: ["csharp", "dart", "groovy", "regex", "java", "php", "python"],
additionalLanguages: ["csharp", "dart", "groovy", "regex", "java", "php", "python", "protobuf"],
},
colorMode: {
defaultMode: "dark",

View File

@ -111,5 +111,11 @@
"imgSrcDark": "/docs/img/tech/rustlight.svg",
"docsLink": "https://github.com/smartive/zitadel-rust",
"external": true
},
{
"title": "Pylon",
"imgSrcDark": "/docs/img/tech/pylon.svg",
"docsLink": "https://github.com/getcronit/pylon",
"external": true
}
]

View File

@ -1,56 +0,0 @@
events {
worker_connections 1024; ## Default: 1024
}
http {
include /etc/nginx/mime.types;
server {
listen 8080;
location / {
return 301 /docs;
}
location /docs {
alias /usr/share/nginx/html;
index /docs/index.html;
try_files $uri $uri/ /docs/index.html?q=$query_string;
}
location = /docs/proxy/js/script.js {
proxy_pass https://plausible.io/js/script.js;
proxy_set_header Host plausible.io;
}
location = /docs/proxy/api/event {
proxy_pass https://plausible.io/api/event;
proxy_set_header Host plausible.io;
proxy_buffering on;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
}
## enable gzip compression
gzip on;
gzip_vary on;
gzip_min_length 256;
gzip_proxied any;
gzip_types
## text/html is always compressed : https://nginx.org/en/docs/http/ngx_http_gzip_module.html
text/plain
text/css
text/javascript
application/javascript
application/x-javascript
application/xml
application/json
application/ld+json;
}

View File

@ -38,6 +38,7 @@ module.exports = {
"examples/secure-api/python-django",
"examples/secure-api/python-flask",
"examples/secure-api/nodejs-nestjs",
"examples/secure-api/pylon",
{
type: "link",
label: ".Net",
@ -100,6 +101,11 @@ module.exports = {
label: "Rust",
href: "https://github.com/smartive/zitadel-rust",
},
{
type: "link",
label: "Pylon",
href: "https://github.com/getcronit/pylon",
},
],
},
{
@ -154,11 +160,6 @@ module.exports = {
"guides/manage/customize/restrictions",
],
},
{
type: "category",
label: "Terraform",
items: ["guides/manage/terraform/basics"],
},
{
type: "category",
label: "Users",
@ -168,6 +169,7 @@ module.exports = {
"guides/manage/customize/user-schema",
],
},
"guides/manage/terraform-provider",
],
},
{
@ -284,7 +286,7 @@ module.exports = {
label: "Service Users",
link: {
type: "doc",
id: "guides/integrate/service-users/authenticate-service-users"
id: "guides/integrate/service-users/authenticate-service-users",
},
collapsed: true,
items: [
@ -327,7 +329,10 @@ module.exports = {
{
type: "category",
label: "Login users with SSO",
link: { type: "doc", id: "guides/integrate/identity-providers/introduction" },
link: {
type: "doc",
id: "guides/integrate/identity-providers/introduction",
},
collapsed: true,
items: [
"guides/integrate/identity-providers/google",
@ -353,7 +358,7 @@ module.exports = {
label: "ZITADEL APIs",
link: {
type: "doc",
id: "guides/integrate/zitadel-apis/access-zitadel-apis"
id: "guides/integrate/zitadel-apis/access-zitadel-apis",
},
collapsed: true,
items: [
@ -482,9 +487,8 @@ module.exports = {
{
type: "autogenerated",
dirName: "concepts/structure",
}
]
},
],
},
{
type: "category",
@ -494,9 +498,8 @@ module.exports = {
{
type: "autogenerated",
dirName: "concepts/features",
}
]
},
],
},
{
type: "autogenerated",
@ -811,10 +814,7 @@ module.exports = {
type: "category",
label: "Actions V2",
collapsed: false,
items: [
"apis/actionsv2/introduction",
"apis/actionsv2/execution-local",
],
items: ["apis/actionsv2/introduction", "apis/actionsv2/execution-local"],
},
{
type: "doc",
@ -843,6 +843,7 @@ module.exports = {
"self-hosting/deploy/linux",
"self-hosting/deploy/macos",
"self-hosting/deploy/compose",
"self-hosting/deploy/devcontainer",
"self-hosting/deploy/knative",
"self-hosting/deploy/kubernetes",
"self-hosting/deploy/loadbalancing-example/loadbalancing-example",
@ -887,11 +888,9 @@ module.exports = {
collapsed: false,
link: {
type: "doc",
id: "self-hosting/manage/cli/overview"
id: "self-hosting/manage/cli/overview",
},
items: [
"self-hosting/manage/cli/mirror"
],
items: ["self-hosting/manage/cli/mirror"],
},
],
},

1
docs/static/img/tech/pylon.svg vendored Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="500" zoomAndPan="magnify" viewBox="0 0 375 374.999991" height="500" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><clipPath id="0b6ffeb5c8"><path d="M 187.5 0 C 83.945312 0 0 83.945312 0 187.5 C 0 291.054688 83.945312 375 187.5 375 C 291.054688 375 375 291.054688 375 187.5 C 375 83.945312 291.054688 0 187.5 0 Z M 187.5 0 " clip-rule="nonzero"/></clipPath><clipPath id="3768169a3c"><path d="M 137 94.300781 L 238 94.300781 L 238 120 L 137 120 Z M 137 94.300781 " clip-rule="nonzero"/></clipPath><clipPath id="e990912f0b"><path d="M 86.097656 275 L 289 275 L 289 297.503906 L 86.097656 297.503906 Z M 86.097656 275 " clip-rule="nonzero"/></clipPath></defs><g clip-path="url(#0b6ffeb5c8)"><rect x="-37.5" width="450" fill="#000000" y="-37.499999" height="449.999989" fill-opacity="1"/></g><path fill="#ffffff" d="M 267.359375 242.78125 L 107.640625 242.78125 L 101.558594 268.128906 L 273.4375 268.128906 Z M 267.359375 242.78125 " fill-opacity="1" fill-rule="nonzero"/><g clip-path="url(#3768169a3c)"><path fill="#ffffff" d="M 223.167969 94.300781 L 151.828125 94.300781 C 151.21875 94.304688 150.617188 94.355469 150.019531 94.457031 C 149.421875 94.558594 148.835938 94.707031 148.261719 94.910156 C 147.6875 95.109375 147.136719 95.355469 146.605469 95.644531 C 146.074219 95.9375 145.570312 96.273438 145.09375 96.648438 C 144.617188 97.023438 144.175781 97.4375 143.769531 97.886719 C 143.363281 98.339844 142.996094 98.820312 142.667969 99.332031 C 142.34375 99.84375 142.058594 100.378906 141.824219 100.9375 C 141.585938 101.496094 141.398438 102.070312 141.257812 102.660156 L 137.179688 119.652344 L 237.835938 119.652344 L 233.734375 102.632812 C 233.59375 102.042969 233.402344 101.46875 233.167969 100.914062 C 232.929688 100.355469 232.644531 99.820312 232.320312 99.3125 C 231.992188 98.800781 231.625 98.320312 231.21875 97.875 C 230.8125 97.425781 230.371094 97.011719 229.894531 96.636719 C 229.417969 96.261719 228.914062 95.929688 228.382812 95.640625 C 227.851562 95.347656 227.300781 95.101562 226.726562 94.90625 C 226.15625 94.707031 225.570312 94.554688 224.972656 94.453125 C 224.375 94.351562 223.773438 94.300781 223.167969 94.300781 Z M 223.167969 94.300781 " fill-opacity="1" fill-rule="nonzero"/></g><g clip-path="url(#e990912f0b)"><path fill="#ffffff" d="M 89.71875 275.371094 L 285.277344 275.371094 C 285.757812 275.371094 286.21875 275.464844 286.660156 275.648438 C 287.105469 275.832031 287.496094 276.09375 287.835938 276.433594 C 288.175781 276.773438 288.4375 277.164062 288.621094 277.609375 C 288.804688 278.050781 288.898438 278.511719 288.898438 278.992188 L 288.898438 293.480469 C 288.898438 293.960938 288.804688 294.421875 288.621094 294.867188 C 288.4375 295.308594 288.175781 295.699219 287.835938 296.039062 C 287.496094 296.378906 287.105469 296.640625 286.660156 296.824219 C 286.21875 297.007812 285.757812 297.101562 285.277344 297.101562 L 89.71875 297.101562 C 89.238281 297.101562 88.777344 297.007812 88.335938 296.824219 C 87.890625 296.640625 87.5 296.378906 87.160156 296.039062 C 86.820312 295.699219 86.558594 295.308594 86.375 294.867188 C 86.191406 294.421875 86.097656 293.960938 86.097656 293.480469 L 86.097656 278.992188 C 86.097656 278.511719 86.191406 278.050781 86.375 277.609375 C 86.558594 277.164062 86.820312 276.773438 87.160156 276.433594 C 87.5 276.09375 87.890625 275.832031 88.335938 275.648438 C 88.777344 275.464844 89.238281 275.371094 89.71875 275.371094 Z M 89.71875 275.371094 " fill-opacity="1" fill-rule="nonzero"/></g><path fill="#ffffff" d="M 128.5 155.867188 L 246.5 155.867188 L 239.546875 126.894531 L 135.453125 126.894531 Z M 128.5 155.867188 " fill-opacity="1" fill-rule="nonzero"/><path fill="#ffffff" d="M 109.378906 235.535156 L 265.617188 235.535156 L 256.925781 199.324219 L 118.070312 199.324219 Z M 109.378906 235.535156 " fill-opacity="1" fill-rule="nonzero"/><path fill="#ffffff" d="M 126.753906 163.109375 L 119.804688 192.078125 L 255.191406 192.078125 L 248.242188 163.109375 Z M 126.753906 163.109375 " fill-opacity="1" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -1,4 +1,7 @@
{
"github": {
"enabled": true
},
"cleanUrls": true,
"rewrites": [
{
@ -39,6 +42,7 @@
{ "source": "/docs/guides/integrate/event-api", "destination": "/docs/guides/integrate/zitadel-apis/event-api", "permanent": true },
{ "source": "/docs/examples/call-zitadel-api/go", "destination": "/docs/guides/integrate/zitadel-apis/example-zitadel-api-with-go", "permanent": true },
{ "source": "/docs/examples/call-zitadel-api/dot-net", "destination": "/docs/guides/integrate/zitadel-apis/example-zitadel-api-with-dot-net", "permanent": true },
{ "source": "/docs/guides/manage/terraform/basics", "destination": "/docs/guides/manage/terraform-provider", "permanent": true },
{ "source": "/docs/guides/integrate/identity-providers", "destination": "/docs/guides/integrate/identity-providers/introduction", "permanent": true }
]
}

View File

@ -2145,7 +2145,7 @@
"@docusaurus/theme-search-algolia" "2.2.0"
"@docusaurus/types" "2.2.0"
"@docusaurus/react-loadable@5.5.2", "react-loadable@npm:@docusaurus/react-loadable@5.5.2":
"@docusaurus/react-loadable@5.5.2":
version "5.5.2"
resolved "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz"
integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==
@ -3894,9 +3894,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001517:
version "1.0.30001538"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001538.tgz"
integrity sha512-HWJnhnID+0YMtGlzcp3T9drmBJUVDchPJ08tpUGFLs9CYlwWPH2uLgpHn8fND5pCgXVtnGS3H4QR9XLMHVNkHw==
version "1.0.30001632"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001632.tgz"
integrity sha512-udx3o7yHJfUxMLkGohMlVHCvFvWmirKh9JAH/d7WOLPetlH+LTL5cocMZ0t7oZx/mdlOWXti97xLZWc8uURRHg==
ccount@^1.0.0:
version "1.1.0"
@ -8546,6 +8546,14 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1:
dependencies:
"@babel/runtime" "^7.10.3"
"react-loadable@npm:@docusaurus/react-loadable@5.5.2":
version "5.5.2"
resolved "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz"
integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==
dependencies:
"@types/react" "*"
prop-types "^15.6.2"
react-magic-dropzone@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/react-magic-dropzone/-/react-magic-dropzone-1.0.1.tgz"

View File

@ -246,7 +246,10 @@ func (s *Server) transportDataFromFile(ctx context.Context, v1Transformation boo
return dataOrgs, nil
}
func getFileFromS3(ctx context.Context, input *admin_pb.ImportDataRequest_S3Input) ([]byte, error) {
func getFileFromS3(ctx context.Context, input *admin_pb.ImportDataRequest_S3Input) (_ []byte, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
minioClient, err := minio.New(input.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(input.AccessKeyId, input.SecretAccessKey, ""),
Secure: input.Ssl,
@ -272,7 +275,10 @@ func getFileFromS3(ctx context.Context, input *admin_pb.ImportDataRequest_S3Inpu
return ioutil.ReadAll(object)
}
func getFileFromGCS(ctx context.Context, input *admin_pb.ImportDataRequest_GCSInput) ([]byte, error) {
func getFileFromGCS(ctx context.Context, input *admin_pb.ImportDataRequest_GCSInput) (_ []byte, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
saJson, err := base64.StdEncoding.DecodeString(input.ServiceaccountJson)
if err != nil {
return nil, err
@ -292,8 +298,11 @@ func getFileFromGCS(ctx context.Context, input *admin_pb.ImportDataRequest_GCSIn
return ioutil.ReadAll(reader)
}
func importOrg1(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, ctxData authz.CtxData, org *admin_pb.DataOrg, success *admin_pb.ImportDataSuccess, count *counts, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode crypto.Generator) error {
_, err := s.command.AddOrgWithID(ctx, org.GetOrg().GetName(), ctxData.UserID, ctxData.ResourceOwner, org.GetOrgId(), []string{})
func importOrg1(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, ctxData authz.CtxData, org *admin_pb.DataOrg, success *admin_pb.ImportDataSuccess, count *counts, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode crypto.Generator) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
_, err = s.command.AddOrgWithID(ctx, org.GetOrg().GetName(), ctxData.UserID, ctxData.ResourceOwner, org.GetOrgId(), []string{})
if err != nil {
*errors = append(*errors, &admin_pb.ImportDataError{Type: "org", Id: org.GetOrgId(), Message: err.Error()})
if _, err := s.query.OrgByID(ctx, true, org.OrgId); err != nil {
@ -328,11 +337,14 @@ func importOrg1(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataEr
return importResources(ctx, s, errors, successOrg, org, count, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode)
}
func importLabelPolicy(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) error {
func importLabelPolicy(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if org.LabelPolicy == nil {
return nil
}
_, err := s.command.AddLabelPolicy(ctx, org.GetOrgId(), management.AddLabelPolicyToDomain(org.GetLabelPolicy()))
_, err = s.command.AddLabelPolicy(ctx, org.GetOrgId(), management.AddLabelPolicyToDomain(org.GetLabelPolicy()))
if err != nil {
*errors = append(*errors, &admin_pb.ImportDataError{Type: "label_policy", Id: org.GetOrgId(), Message: err.Error()})
if isCtxTimeout(ctx) {
@ -351,6 +363,9 @@ func importLabelPolicy(ctx context.Context, s *Server, errors *[]*admin_pb.Impor
}
func importLockoutPolicy(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
if org.LockoutPolicy == nil {
return
}
@ -360,7 +375,10 @@ func importLockoutPolicy(ctx context.Context, s *Server, errors *[]*admin_pb.Imp
}
}
func importOidcIdps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg) error {
func importOidcIdps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if org.OidcIdps == nil {
return nil
}
@ -380,7 +398,10 @@ func importOidcIdps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDa
return nil
}
func importJwtIdps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg) error {
func importJwtIdps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if org.JwtIdps == nil {
return nil
}
@ -401,6 +422,9 @@ func importJwtIdps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDat
}
func importLoginPolicy(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
if org.LoginPolicy == nil {
return
}
@ -411,6 +435,9 @@ func importLoginPolicy(ctx context.Context, s *Server, errors *[]*admin_pb.Impor
}
func importPwComlexityPolicy(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
if org.PasswordComplexityPolicy == nil {
return
}
@ -421,6 +448,9 @@ func importPwComlexityPolicy(ctx context.Context, s *Server, errors *[]*admin_pb
}
func importPrivacyPolicy(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
if org.PrivacyPolicy == nil {
return
}
@ -430,7 +460,10 @@ func importPrivacyPolicy(ctx context.Context, s *Server, errors *[]*admin_pb.Imp
}
}
func importHumanUsers(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode crypto.Generator) error {
func importHumanUsers(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode crypto.Generator) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if org.HumanUsers == nil {
return nil
}
@ -465,7 +498,10 @@ func importHumanUsers(ctx context.Context, s *Server, errors *[]*admin_pb.Import
return nil
}
func importMachineUsers(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error {
func importMachineUsers(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if org.MachineUsers == nil {
return nil
}
@ -486,7 +522,10 @@ func importMachineUsers(ctx context.Context, s *Server, errors *[]*admin_pb.Impo
return nil
}
func importUserMetadata(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error {
func importUserMetadata(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if org.UserMetadata == nil {
return nil
}
@ -507,7 +546,10 @@ func importUserMetadata(ctx context.Context, s *Server, errors *[]*admin_pb.Impo
return nil
}
func importMachineKeys(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error {
func importMachineKeys(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if org.MachineKeys == nil {
return nil
}
@ -537,7 +579,10 @@ func importMachineKeys(ctx context.Context, s *Server, errors *[]*admin_pb.Impor
return nil
}
func importUserLinks(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error {
func importUserLinks(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if org.UserLinks == nil {
return nil
}
@ -548,6 +593,7 @@ func importUserLinks(ctx context.Context, s *Server, errors *[]*admin_pb.ImportD
IDPExternalID: userLinks.ProvidedUserId,
DisplayName: userLinks.ProvidedUserName,
}
// TBD: why not command.BulkAddedUserIDPLinks?
if _, err := s.command.AddUserIDPLink(ctx, userLinks.UserId, org.GetOrgId(), externalIDP); err != nil {
*errors = append(*errors, &admin_pb.ImportDataError{Type: "user_link", Id: userLinks.UserId + "_" + userLinks.IdpId, Message: err.Error()})
if isCtxTimeout(ctx) {
@ -563,7 +609,10 @@ func importUserLinks(ctx context.Context, s *Server, errors *[]*admin_pb.ImportD
}
func importProjects(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error {
func importProjects(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if org.Projects == nil {
return nil
}
@ -584,7 +633,10 @@ func importProjects(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDa
return nil
}
func importOIDCApps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error {
func importOIDCApps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if org.OidcApps == nil {
return nil
}
@ -605,7 +657,10 @@ func importOIDCApps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDa
return nil
}
func importAPIApps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error {
func importAPIApps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if org.ApiApps == nil {
return nil
}
@ -626,7 +681,10 @@ func importAPIApps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDat
return nil
}
func importAppKeys(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error {
func importAppKeys(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if org.AppKeys == nil {
return nil
}
@ -658,7 +716,10 @@ func importAppKeys(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDat
return nil
}
func importActions(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error {
func importActions(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if org.Actions == nil {
return nil
}
@ -678,12 +739,17 @@ func importActions(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDat
}
return nil
}
func importProjectRoles(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error {
func importProjectRoles(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if org.ProjectRoles == nil {
return nil
}
for _, role := range org.GetProjectRoles() {
logging.Debugf("import projectroles: %s", role.ProjectId+"_"+role.RoleKey)
// TBD: why not command.BulkAddProjectRole?
_, err := s.command.AddProjectRole(ctx, management.AddProjectRoleRequestToDomain(role), org.GetOrgId())
if err != nil {
*errors = append(*errors, &admin_pb.ImportDataError{Type: "project_role", Id: role.ProjectId + "_" + role.RoleKey, Message: err.Error()})
@ -700,7 +766,10 @@ func importProjectRoles(ctx context.Context, s *Server, errors *[]*admin_pb.Impo
return nil
}
func importResources(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode crypto.Generator) error {
func importResources(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode crypto.Generator) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if err := importOrgDomains(ctx, s, errors, successOrg, org); err != nil {
return err
}
@ -760,7 +829,10 @@ func importResources(ctx context.Context, s *Server, errors *[]*admin_pb.ImportD
return nil
}
func importOrgDomains(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg) error {
func importOrgDomains(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if org.Domains == nil {
return nil
}
@ -799,6 +871,9 @@ func importOrgDomains(ctx context.Context, s *Server, errors *[]*admin_pb.Import
}
func importLoginTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
if org.LoginTexts == nil {
return
}
@ -811,6 +886,9 @@ func importLoginTexts(ctx context.Context, s *Server, errors *[]*admin_pb.Import
}
func importInitMessageTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
if org.InitMessages == nil {
return
}
@ -823,6 +901,9 @@ func importInitMessageTexts(ctx context.Context, s *Server, errors *[]*admin_pb.
}
func importPWResetMessageTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
if org.PasswordResetMessages == nil {
return
}
@ -835,6 +916,9 @@ func importPWResetMessageTexts(ctx context.Context, s *Server, errors *[]*admin_
}
func importVerifyEmailMessageTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
if org.VerifyEmailMessages == nil {
return
}
@ -847,6 +931,9 @@ func importVerifyEmailMessageTexts(ctx context.Context, s *Server, errors *[]*ad
}
func importVerifyPhoneMessageTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
if org.VerifyPhoneMessages != nil {
return
}
@ -859,6 +946,9 @@ func importVerifyPhoneMessageTexts(ctx context.Context, s *Server, errors *[]*ad
}
func importDomainClaimedMessageTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
if org.DomainClaimedMessages == nil {
return
}
@ -871,6 +961,9 @@ func importDomainClaimedMessageTexts(ctx context.Context, s *Server, errors *[]*
}
func importPasswordlessRegistrationMessageTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
if org.PasswordlessRegistrationMessages == nil {
return
}
@ -882,7 +975,10 @@ func importPasswordlessRegistrationMessageTexts(ctx context.Context, s *Server,
}
}
func importOrg2(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, success *admin_pb.ImportDataSuccess, count *counts, org *admin_pb.DataOrg) error {
func importOrg2(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, success *admin_pb.ImportDataSuccess, count *counts, org *admin_pb.DataOrg) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
successOrg := findOldOrg(success, org.OrgId)
if successOrg == nil {
return nil
@ -932,7 +1028,10 @@ func importOrg2(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataEr
return nil
}
func importOrg3(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, success *admin_pb.ImportDataSuccess, count *counts, org *admin_pb.DataOrg) error {
func importOrg3(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, success *admin_pb.ImportDataSuccess, count *counts, org *admin_pb.DataOrg) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
successOrg := findOldOrg(success, org.OrgId)
if successOrg == nil {
return nil
@ -946,7 +1045,10 @@ func importOrg3(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataEr
return importProjectMembers(ctx, s, errors, successOrg, count, org)
}
func importOrgMembers(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, count *counts, org *admin_pb.DataOrg) error {
func importOrgMembers(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, count *counts, org *admin_pb.DataOrg) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if org.OrgMembers == nil {
return nil
}
@ -967,7 +1069,10 @@ func importOrgMembers(ctx context.Context, s *Server, errors *[]*admin_pb.Import
return nil
}
func importProjectGrantMembers(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, count *counts, org *admin_pb.DataOrg) error {
func importProjectGrantMembers(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, count *counts, org *admin_pb.DataOrg) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if org.ProjectGrantMembers == nil {
return nil
}
@ -988,7 +1093,10 @@ func importProjectGrantMembers(ctx context.Context, s *Server, errors *[]*admin_
return nil
}
func importProjectMembers(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, count *counts, org *admin_pb.DataOrg) error {
func importProjectMembers(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, count *counts, org *admin_pb.DataOrg) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if org.ProjectMembers == nil {
return nil
}
@ -1018,7 +1126,10 @@ func findOldOrg(success *admin_pb.ImportDataSuccess, orgId string) *admin_pb.Imp
return nil
}
func (s *Server) importData(ctx context.Context, orgs []*admin_pb.DataOrg) (*admin_pb.ImportDataResponse, *counts, error) {
func (s *Server) importData(ctx context.Context, orgs []*admin_pb.DataOrg) (_ *admin_pb.ImportDataResponse, _ *counts, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
errors := make([]*admin_pb.ImportDataError, 0)
success := &admin_pb.ImportDataSuccess{}
count := &counts{}

View File

@ -35,7 +35,22 @@ func (s *Server) GetPasswordComplexitySettings(ctx context.Context, req *setting
return nil, err
}
return &settings.GetPasswordComplexitySettingsResponse{
Settings: passwordSettingsToPb(current),
Settings: passwordComplexitySettingsToPb(current),
Details: &object_pb.Details{
Sequence: current.Sequence,
ChangeDate: timestamppb.New(current.ChangeDate),
ResourceOwner: current.ResourceOwner,
},
}, nil
}
func (s *Server) GetPasswordExpirySettings(ctx context.Context, req *settings.GetPasswordExpirySettingsRequest) (*settings.GetPasswordExpirySettingsResponse, error) {
current, err := s.query.PasswordAgePolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false)
if err != nil {
return nil, err
}
return &settings.GetPasswordExpirySettingsResponse{
Settings: passwordExpirySettingsToPb(current),
Details: &object_pb.Details{
Sequence: current.Sequence,
ChangeDate: timestamppb.New(current.ChangeDate),

View File

@ -91,7 +91,7 @@ func multiFactorTypeToPb(typ domain.MultiFactorType) settings.MultiFactorType {
}
}
func passwordSettingsToPb(current *query.PasswordComplexityPolicy) *settings.PasswordComplexitySettings {
func passwordComplexitySettingsToPb(current *query.PasswordComplexityPolicy) *settings.PasswordComplexitySettings {
return &settings.PasswordComplexitySettings{
MinLength: current.MinLength,
RequiresUppercase: current.HasUppercase,
@ -102,6 +102,14 @@ func passwordSettingsToPb(current *query.PasswordComplexityPolicy) *settings.Pas
}
}
func passwordExpirySettingsToPb(current *query.PasswordAgePolicy) *settings.PasswordExpirySettings {
return &settings.PasswordExpirySettings{
MaxAgeDays: current.MaxAgeDays,
ExpireWarnDays: current.ExpireWarnDays,
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
}
}
func brandingSettingsToPb(current *query.LabelPolicy, assetPrefix string) *settings.BrandingSettings {
return &settings.BrandingSettings{
LightTheme: themeToPb(current.Light, assetPrefix, current.ResourceOwner),

View File

@ -213,7 +213,7 @@ func Test_multiFactorTypeToPb(t *testing.T) {
}
}
func Test_passwordSettingsToPb(t *testing.T) {
func Test_passwordComplexitySettingsToPb(t *testing.T) {
arg := &query.PasswordComplexityPolicy{
MinLength: 12,
HasUppercase: true,
@ -231,10 +231,29 @@ func Test_passwordSettingsToPb(t *testing.T) {
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
}
got := passwordSettingsToPb(arg)
got := passwordComplexitySettingsToPb(arg)
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
if !proto.Equal(got, want) {
t.Errorf("passwordSettingsToPb() =\n%v\nwant\n%v", got, want)
t.Errorf("passwordComplexitySettingsToPb() =\n%v\nwant\n%v", got, want)
}
}
func Test_passwordExpirySettingsToPb(t *testing.T) {
arg := &query.PasswordAgePolicy{
ExpireWarnDays: 80,
MaxAgeDays: 90,
IsDefault: true,
}
want := &settings.PasswordExpirySettings{
ExpireWarnDays: 80,
MaxAgeDays: 90,
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
}
got := passwordExpirySettingsToPb(arg)
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
if !proto.Equal(got, want) {
t.Errorf("passwordExpirySettingsToPb() =\n%v\nwant\n%v", got, want)
}
}

View File

@ -316,6 +316,7 @@ func PasswordChangeScreenTextToPb(text domain.PasswordChangeScreenText) *text_pb
return &text_pb.PasswordChangeScreenText{
Title: text.Title,
Description: text.Description,
ExpiredDescription: text.ExpiredDescription,
OldPasswordLabel: text.OldPasswordLabel,
NewPasswordLabel: text.NewPasswordLabel,
NewPasswordConfirmLabel: text.NewPasswordConfirmLabel,
@ -784,6 +785,7 @@ func PasswordChangeScreenTextPbToDomain(text *text_pb.PasswordChangeScreenText)
return domain.PasswordChangeScreenText{
Title: text.Title,
Description: text.Description,
ExpiredDescription: text.ExpiredDescription,
OldPasswordLabel: text.OldPasswordLabel,
NewPasswordLabel: text.NewPasswordLabel,
NewPasswordConfirmLabel: text.NewPasswordConfirmLabel,

View File

@ -1,6 +1,8 @@
package user
import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
@ -48,6 +50,10 @@ func UserTypeToPb(user *query.User, assetPrefix string) user_pb.UserType {
}
func HumanToPb(view *query.Human, assetPrefix, owner string) *user_pb.Human {
var passwordChanged *timestamppb.Timestamp
if !view.PasswordChanged.IsZero() {
passwordChanged = timestamppb.New(view.PasswordChanged)
}
return &user_pb.Human{
Profile: &user_pb.Profile{
FirstName: view.FirstName,
@ -66,6 +72,7 @@ func HumanToPb(view *query.Human, assetPrefix, owner string) *user_pb.Human {
Phone: string(view.Phone),
IsPhoneVerified: view.IsPhoneVerified,
},
PasswordChanged: passwordChanged,
}
}

View File

@ -4,6 +4,7 @@ import (
"context"
"github.com/muhlemmer/gu"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
@ -83,6 +84,10 @@ func userTypeToPb(userQ *query.User, assetPrefix string) user.UserType {
}
func humanToPb(userQ *query.Human, assetPrefix, owner string) *user.HumanUser {
var passwordChanged *timestamppb.Timestamp
if !userQ.PasswordChanged.IsZero() {
passwordChanged = timestamppb.New(userQ.PasswordChanged)
}
return &user.HumanUser{
Profile: &user.HumanProfile{
GivenName: userQ.FirstName,
@ -102,6 +107,7 @@ func humanToPb(userQ *query.Human, assetPrefix, owner string) *user.HumanUser {
IsVerified: userQ.IsPhoneVerified,
},
PasswordChangeRequired: userQ.PasswordChangeRequired,
PasswordChanged: passwordChanged,
}
}

View File

@ -23,7 +23,7 @@ func TestServer_GetUserByID(t *testing.T) {
type args struct {
ctx context.Context
req *user.GetUserByIDRequest
dep func(ctx context.Context, username string, request *user.GetUserByIDRequest) error
dep func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*timestamppb.Timestamp, error)
}
tests := []struct {
name string
@ -38,8 +38,8 @@ func TestServer_GetUserByID(t *testing.T) {
&user.GetUserByIDRequest{
UserId: "",
},
func(ctx context.Context, username string, request *user.GetUserByIDRequest) error {
return nil
func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*timestamppb.Timestamp, error) {
return nil, nil
},
},
wantErr: true,
@ -51,8 +51,8 @@ func TestServer_GetUserByID(t *testing.T) {
&user.GetUserByIDRequest{
UserId: "unknown",
},
func(ctx context.Context, username string, request *user.GetUserByIDRequest) error {
return nil
func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*timestamppb.Timestamp, error) {
return nil, nil
},
},
wantErr: true,
@ -62,10 +62,10 @@ func TestServer_GetUserByID(t *testing.T) {
args: args{
IamCTX,
&user.GetUserByIDRequest{},
func(ctx context.Context, username string, request *user.GetUserByIDRequest) error {
func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*timestamppb.Timestamp, error) {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
request.UserId = resp.GetUserId()
return nil
return nil, nil
},
},
want: &user.GetUserByIDResponse{
@ -106,11 +106,11 @@ func TestServer_GetUserByID(t *testing.T) {
args: args{
IamCTX,
&user.GetUserByIDRequest{},
func(ctx context.Context, username string, request *user.GetUserByIDRequest) error {
func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*timestamppb.Timestamp, error) {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
request.UserId = resp.GetUserId()
Tester.SetUserPassword(ctx, resp.GetUserId(), integration.UserPassword, true)
return nil
changed := Tester.SetUserPassword(ctx, resp.GetUserId(), integration.UserPassword, true)
return changed, nil
},
},
want: &user.GetUserByIDResponse{
@ -138,6 +138,7 @@ func TestServer_GetUserByID(t *testing.T) {
IsVerified: true,
},
PasswordChangeRequired: true,
PasswordChanged: timestamppb.Now(),
},
},
},
@ -151,7 +152,7 @@ func TestServer_GetUserByID(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
username := fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())
err := tt.args.dep(tt.args.ctx, username, tt.args.req)
changed, err := tt.args.dep(tt.args.ctx, username, tt.args.req)
require.NoError(t, err)
retryDuration := time.Minute
if ctxDeadline, ok := CTX.Deadline(); ok {
@ -173,6 +174,9 @@ func TestServer_GetUserByID(t *testing.T) {
tt.want.User.LoginNames = []string{username}
if human := tt.want.User.GetHuman(); human != nil {
human.Email.Email = username
if tt.want.User.GetHuman().GetPasswordChanged() != nil {
human.PasswordChanged = changed
}
}
assert.Equal(ttt, tt.want.User, got.User)
integration.AssertDetails(t, tt.want, got)
@ -316,6 +320,7 @@ func TestServer_GetUserByID_Permission(t *testing.T) {
type userAttr struct {
UserID string
Username string
Changed *timestamppb.Timestamp
}
func TestServer_ListUsers(t *testing.T) {
@ -369,7 +374,7 @@ func TestServer_ListUsers(t *testing.T) {
for i, username := range usernames {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
userIDs[i] = resp.GetUserId()
infos[i] = userAttr{resp.GetUserId(), username}
infos[i] = userAttr{resp.GetUserId(), username, nil}
}
request.Queries = append(request.Queries, InUserIDsQuery(userIDs))
return infos, nil
@ -423,8 +428,8 @@ func TestServer_ListUsers(t *testing.T) {
for i, username := range usernames {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
userIDs[i] = resp.GetUserId()
Tester.SetUserPassword(ctx, resp.GetUserId(), integration.UserPassword, true)
infos[i] = userAttr{resp.GetUserId(), username}
changed := Tester.SetUserPassword(ctx, resp.GetUserId(), integration.UserPassword, true)
infos[i] = userAttr{resp.GetUserId(), username, changed}
}
request.Queries = append(request.Queries, InUserIDsQuery(userIDs))
return infos, nil
@ -457,6 +462,7 @@ func TestServer_ListUsers(t *testing.T) {
IsVerified: true,
},
PasswordChangeRequired: true,
PasswordChanged: timestamppb.Now(),
},
},
},
@ -479,7 +485,7 @@ func TestServer_ListUsers(t *testing.T) {
for i, username := range usernames {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
userIDs[i] = resp.GetUserId()
infos[i] = userAttr{resp.GetUserId(), username}
infos[i] = userAttr{resp.GetUserId(), username, nil}
}
request.Queries = append(request.Queries, InUserIDsQuery(userIDs))
return infos, nil
@ -575,7 +581,7 @@ func TestServer_ListUsers(t *testing.T) {
for i, username := range usernames {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
userIDs[i] = resp.GetUserId()
infos[i] = userAttr{resp.GetUserId(), username}
infos[i] = userAttr{resp.GetUserId(), username, nil}
request.Queries = append(request.Queries, UsernameQuery(username))
}
return infos, nil
@ -627,7 +633,7 @@ func TestServer_ListUsers(t *testing.T) {
infos := make([]userAttr, len(usernames))
for i, username := range usernames {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
infos[i] = userAttr{resp.GetUserId(), username}
infos[i] = userAttr{resp.GetUserId(), username, nil}
}
request.Queries = append(request.Queries, InUserEmailsQuery(usernames))
return infos, nil
@ -679,7 +685,7 @@ func TestServer_ListUsers(t *testing.T) {
infos := make([]userAttr, len(usernames))
for i, username := range usernames {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
infos[i] = userAttr{resp.GetUserId(), username}
infos[i] = userAttr{resp.GetUserId(), username, nil}
}
request.Queries = append(request.Queries, InUserEmailsQuery(usernames))
return infos, nil
@ -794,7 +800,7 @@ func TestServer_ListUsers(t *testing.T) {
infos := make([]userAttr, len(usernames))
for i, username := range usernames {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
infos[i] = userAttr{resp.GetUserId(), username}
infos[i] = userAttr{resp.GetUserId(), username, nil}
}
request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId))
request.Queries = append(request.Queries, InUserEmailsQuery(usernames))
@ -910,6 +916,9 @@ func TestServer_ListUsers(t *testing.T) {
tt.want.Result[i].LoginNames = []string{infos[i].Username}
if human := tt.want.Result[i].GetHuman(); human != nil {
human.Email.Email = infos[i].Username
if tt.want.Result[i].GetHuman().GetPasswordChanged() != nil {
human.PasswordChanged = infos[i].Changed
}
}
}
for i := range tt.want.Result {

View File

@ -75,6 +75,7 @@ func (o *OPStorage) createAuthRequestLoginClient(ctx context.Context, req *oidc.
Audience: audience,
NeedRefreshToken: slices.Contains(scope, oidc.ScopeOfflineAccess),
ResponseType: ResponseTypeToBusiness(req.ResponseType),
ResponseMode: ResponseModeToBusiness(req.ResponseMode),
CodeChallenge: CodeChallengeToBusiness(req.CodeChallenge, req.CodeChallengeMethod),
Prompt: PromptToBusiness(req.Prompt),
UILocales: UILocalesToBusiness(req.UILocales),

View File

@ -6,6 +6,7 @@ import (
"strings"
"time"
"github.com/zitadel/logging"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op"
"golang.org/x/text/language"
@ -75,7 +76,7 @@ func (a *AuthRequest) GetResponseType() oidc.ResponseType {
}
func (a *AuthRequest) GetResponseMode() oidc.ResponseMode {
return ""
return ResponseModeToOIDC(a.oidc().ResponseMode)
}
func (a *AuthRequest) GetScopes() []string {
@ -121,6 +122,7 @@ func CreateAuthRequestToBusiness(ctx context.Context, authReq *oidc.AuthRequest,
Request: &domain.AuthRequestOIDC{
Scopes: authReq.Scopes,
ResponseType: ResponseTypeToBusiness(authReq.ResponseType),
ResponseMode: ResponseModeToBusiness(authReq.ResponseMode),
Nonce: authReq.Nonce,
CodeChallenge: CodeChallengeToBusiness(authReq.CodeChallenge, authReq.CodeChallengeMethod),
},
@ -232,6 +234,27 @@ func ResponseTypeToOIDC(responseType domain.OIDCResponseType) oidc.ResponseType
}
}
// ResponseModeToBusiness returns the OIDCResponseMode enum value from the domain package.
// An empty or invalid value defaults to unspecified.
func ResponseModeToBusiness(responseMode oidc.ResponseMode) domain.OIDCResponseMode {
if responseMode == "" {
return domain.OIDCResponseModeUnspecified
}
out, err := domain.OIDCResponseModeString(string(responseMode))
logging.OnError(err).Debugln("invalid oidc response_mode, using default")
return out
}
// ResponseModeToOIDC return the oidc string representation of the enum value from the domain package.
// When responseMode is `0 - unspecified`, an empty string is returned.
// This allows the oidc package to pick the appropriate response mode based on the response type.
func ResponseModeToOIDC(responseMode domain.OIDCResponseMode) oidc.ResponseMode {
if responseMode == domain.OIDCResponseModeUnspecified || !responseMode.IsAOIDCResponseMode() {
return ""
}
return oidc.ResponseMode(responseMode.String())
}
func CodeChallengeToBusiness(challenge string, method oidc.CodeChallengeMethod) *domain.OIDCCodeChallenge {
if challenge == "" {
return nil

View File

@ -0,0 +1,96 @@
package oidc
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/zitadel/internal/domain"
)
func TestResponseModeToBusiness(t *testing.T) {
type args struct {
responseMode oidc.ResponseMode
}
tests := []struct {
name string
args args
want domain.OIDCResponseMode
}{
{
name: "empty",
args: args{""},
want: domain.OIDCResponseModeUnspecified,
},
{
name: "invalid",
args: args{"foo"},
want: domain.OIDCResponseModeUnspecified,
},
{
name: "query",
args: args{oidc.ResponseModeQuery},
want: domain.OIDCResponseModeQuery,
},
{
name: "fragment",
args: args{oidc.ResponseModeFragment},
want: domain.OIDCResponseModeFragment,
},
{
name: "post_form",
args: args{oidc.ResponseModeFormPost},
want: domain.OIDCResponseModeFormPost,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ResponseModeToBusiness(tt.args.responseMode)
assert.Equal(t, tt.want, got)
})
}
}
func TestResponseModeToOIDC(t *testing.T) {
type args struct {
responseMode domain.OIDCResponseMode
}
tests := []struct {
name string
args args
want oidc.ResponseMode
}{
{
name: "unspecified",
args: args{domain.OIDCResponseModeUnspecified},
want: "",
},
{
name: "invalid",
args: args{99},
want: "",
},
{
name: "query",
args: args{domain.OIDCResponseModeQuery},
want: oidc.ResponseModeQuery,
},
{
name: "fragment",
args: args{domain.OIDCResponseModeFragment},
want: oidc.ResponseModeFragment,
},
{
name: "form_post",
args: args{domain.OIDCResponseModeFormPost},
want: oidc.ResponseModeFormPost,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ResponseModeToOIDC(tt.args.responseMode)
assert.Equal(t, tt.want, got)
})
}
}

View File

@ -53,7 +53,7 @@ func (a *AuthRequestV2) GetResponseType() oidc.ResponseType {
}
func (a *AuthRequestV2) GetResponseMode() oidc.ResponseMode {
return ""
return ResponseModeToOIDC(a.ResponseMode)
}
func (a *AuthRequestV2) GetScopes() []string {

View File

@ -242,6 +242,9 @@ func isScopeAllowed(scope string, allowedScopes ...string) bool {
if strings.HasPrefix(scope, domain.SelectIDPScope) {
return true
}
if strings.HasPrefix(scope, domain.OrgRoleIDScope) {
return true
}
if scope == ScopeUserMetaData {
return true
}

View File

@ -140,6 +140,15 @@ func TestServer_Introspect(t *testing.T) {
}
}
func TestServer_Introspect_invalid_auth_invalid_token(t *testing.T) {
// ensure that when an invalid authentication and token is sent, the authentication error is returned
// https://github.com/zitadel/zitadel/pull/8133
resourceServer, err := Tester.CreateResourceServerClientCredentials(CTX, "xxxxx", "xxxxx")
require.NoError(t, err)
_, err = rs.Introspect[*oidc.IntrospectionResponse](context.Background(), resourceServer, "xxxxx")
require.Error(t, err)
}
func assertIntrospection(
t *testing.T,
introspection *oidc.IntrospectionResponse,

View File

@ -54,19 +54,20 @@ func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionR
select {
case client = <-clientChan:
resErr = client.err
if resErr != nil {
// we prioritize the client error over the token error
err = resErr
cancel()
}
case token = <-tokenChan:
resErr = token.err
}
if resErr == nil {
continue
}
cancel()
// we only care for the first error that occurred,
// as the next error is most probably a context error.
if err == nil {
err = resErr
if resErr == nil {
continue
}
// we prioritize the client error over the token error
if err == nil {
err = resErr
}
}
}

View File

@ -173,23 +173,28 @@ func (s *Server) EndSession(ctx context.Context, r *op.Request[oidc.EndSessionRe
func (s *Server) createDiscoveryConfig(ctx context.Context, supportedUILocales oidc.Locales) *oidc.DiscoveryConfiguration {
issuer := op.IssuerFromContext(ctx)
return &oidc.DiscoveryConfiguration{
Issuer: issuer,
AuthorizationEndpoint: s.Endpoints().Authorization.Absolute(issuer),
TokenEndpoint: s.Endpoints().Token.Absolute(issuer),
IntrospectionEndpoint: s.Endpoints().Introspection.Absolute(issuer),
UserinfoEndpoint: s.Endpoints().Userinfo.Absolute(issuer),
RevocationEndpoint: s.Endpoints().Revocation.Absolute(issuer),
EndSessionEndpoint: s.Endpoints().EndSession.Absolute(issuer),
JwksURI: s.Endpoints().JwksURI.Absolute(issuer),
DeviceAuthorizationEndpoint: s.Endpoints().DeviceAuthorization.Absolute(issuer),
ScopesSupported: op.Scopes(s.Provider()),
ResponseTypesSupported: op.ResponseTypes(s.Provider()),
GrantTypesSupported: op.GrantTypes(s.Provider()),
SubjectTypesSupported: op.SubjectTypes(s.Provider()),
IDTokenSigningAlgValuesSupported: []string{s.signingKeyAlgorithm},
RequestObjectSigningAlgValuesSupported: op.RequestObjectSigAlgorithms(s.Provider()),
TokenEndpointAuthMethodsSupported: op.AuthMethodsTokenEndpoint(s.Provider()),
TokenEndpointAuthSigningAlgValuesSupported: op.TokenSigAlgorithms(s.Provider()),
Issuer: issuer,
AuthorizationEndpoint: s.Endpoints().Authorization.Absolute(issuer),
TokenEndpoint: s.Endpoints().Token.Absolute(issuer),
IntrospectionEndpoint: s.Endpoints().Introspection.Absolute(issuer),
UserinfoEndpoint: s.Endpoints().Userinfo.Absolute(issuer),
RevocationEndpoint: s.Endpoints().Revocation.Absolute(issuer),
EndSessionEndpoint: s.Endpoints().EndSession.Absolute(issuer),
JwksURI: s.Endpoints().JwksURI.Absolute(issuer),
DeviceAuthorizationEndpoint: s.Endpoints().DeviceAuthorization.Absolute(issuer),
ScopesSupported: op.Scopes(s.Provider()),
ResponseTypesSupported: op.ResponseTypes(s.Provider()),
ResponseModesSupported: []string{
string(oidc.ResponseModeQuery),
string(oidc.ResponseModeFragment),
string(oidc.ResponseModeFormPost),
},
GrantTypesSupported: op.GrantTypes(s.Provider()),
SubjectTypesSupported: op.SubjectTypes(s.Provider()),
IDTokenSigningAlgValuesSupported: []string{s.signingKeyAlgorithm},
RequestObjectSigningAlgValuesSupported: op.RequestObjectSigAlgorithms(s.Provider()),
TokenEndpointAuthMethodsSupported: op.AuthMethodsTokenEndpoint(s.Provider()),
TokenEndpointAuthSigningAlgValuesSupported: op.TokenSigAlgorithms(s.Provider()),
IntrospectionEndpointAuthSigningAlgValuesSupported: op.IntrospectionSigAlgorithms(s.Provider()),
IntrospectionEndpointAuthMethodsSupported: op.AuthMethodsIntrospectionEndpoint(s.Provider()),
RevocationEndpointAuthSigningAlgValuesSupported: op.RevocationSigAlgorithms(s.Provider()),

View File

@ -73,7 +73,7 @@ func TestServer_createDiscoveryConfig(t *testing.T) {
RegistrationEndpoint: "",
ScopesSupported: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopePhone, oidc.ScopeAddress, oidc.ScopeOfflineAccess},
ResponseTypesSupported: []string{string(oidc.ResponseTypeCode), string(oidc.ResponseTypeIDTokenOnly), string(oidc.ResponseTypeIDToken)},
ResponseModesSupported: nil,
ResponseModesSupported: []string{string(oidc.ResponseModeQuery), string(oidc.ResponseModeFragment), string(oidc.ResponseModeFormPost)},
GrantTypesSupported: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeImplicit, oidc.GrantTypeRefreshToken, oidc.GrantTypeBearer},
ACRValuesSupported: nil,
SubjectTypesSupported: []string{"public"},

View File

@ -104,7 +104,8 @@ func (s *Server) userInfo(
defer func() { span.EndWithError(err) }()
roleAudience, requestedRoles = prepareRoles(ctx, scope, projectID, projectRoleAssertion, currentProjectOnly)
qu, err = s.query.GetOIDCUserInfo(ctx, userID, roleAudience)
roleOrgIDs := domain.RoleOrgIDsFromScope(scope)
qu, err = s.query.GetOIDCUserInfo(ctx, userID, roleAudience, roleOrgIDs...)
if err != nil {
return
}

Some files were not shown because too many files have changed in this diff Show More