mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
# Which Problems Are Solved
The backend and the new login already support the dedicated ZITADEL IdP
template
(`AddZitadelProvider`/`UpdateZitadelProvider`, `PROVIDER_TYPE_ZITADEL`,
`InstanceRolesInfo`),
but the Console does not expose it:
- A ZITADEL provider cannot be created or edited from the Console,
neither at instance nor at
organization level — it is missing from the "add provider" list in the
IdP settings.
- Providers created through the API are listed in the IdP table as
`coming soon` (the type
`@switch` has no `PROVIDER_TYPE_ZITADEL` case) and the row has no detail
route, so an existing
provider cannot be opened.
- `InstanceRolesInfo` — the organization ID/domain pairs that drive
instance-role synchronization
for instance-level providers — is not configurable outside the API.
# How the Problems Are Solved
- Added `ProviderZitadelComponent`
(`console/src/app/modules/providers/provider-zitadel/`),
modeled on the generic OIDC provider component:
- Form for `name`, `issuer`, `clientId`, `clientSecret` and `scopesList`
(chip input, defaulting
to `openid`, `profile`, `email`), plus the shared
`cnsl-provider-options` block.
- On edit the client secret is optional and is only sent when the
"update client secret"
checkbox is set.
- Reuses `cnsl-provider-next` for the callback URL, activation and "what
now" flow.
- Added the instance role mapping section, rendered only for
instance-level providers
(`serviceType === ADMIN`), since the API rejects `instance_roles_info`
on organization-scoped
ZITADEL providers. Entries are added as organization ID + organization
domain pairs and are
submitted as `InstanceRolesInfo`; both values are required and are
matched together on login.
- Added `addZitadelProvider`/`updateZitadelProvider` to `AdminService`
and `ManagementService`
wrapping the existing gRPC calls.
- Declared the component in `providers.module.ts` and mapped
`PROVIDER_TYPE_ZITADEL` to the
`zitadel` path in `providers-routing.module.ts`, so
`/instance/provider/zitadel/...` and
`/org/provider/zitadel/...` resolve for both create and edit.
- Added a ZITADEL tile to the "add provider" list in the IdP settings
and a
`PROVIDER_TYPE_ZITADEL` case to the IdP table;
`IdpTableComponent.routerLinkForRow` now returns
the detail route for ZITADEL rows.
# Additional Changes
- Added the new i18n keys `IDP.CREATE.ZITADEL.TITLE`/`DESCRIPTION`,
`IDP.INSTANCEROLESINFO`,
`IDP.INSTANCEROLESINFO_DESC`, `IDP.ORGANIZATIONID` and
`IDP.ORGANIZATIONDOMAIN` to all 22
Console locale files with actual translations instead of English
fallbacks.
- The provider tile, table entry and detail header use the colored
ZITADEL logo assets
(`zitadel-logo-solo-light.svg` in the dark theme,
`zitadel-logo-solo-dark.svg` in the light
theme), consistent with the header, rather than the monochrome
`*-solo-*-icon.svg` variants.
- Additional sync roles trigger for the support IDP to behave correctly
on first login after a disable/enable cycle — where auto-linking jumps
in — but not required for the link to be established.
# Additional Context
- Closes #11825
- Part of the "Zitadel as an Identity Provider" epic #5127
- Console counterpart to the API work in #12018, #12056 (add), #12371,
#12378, #12384 (update),
#12394 (get/list), #12396 (delete), #12422 (active IdPs) and to the
login support in #12469,
#12530 and #12568
---------
Co-authored-by: Livio Spring <[email protected]>
172 lines
7.1 KiB
HTML
172 lines
7.1 KiB
HTML
<cnsl-create-layout
|
|
title="{{ id ? ('IDP.DETAIL.TITLE' | translate) : ('IDP.CREATE.TITLE' | translate) }}"
|
|
(closed)="close()"
|
|
>
|
|
<div class="identity-provider-create-content">
|
|
<div class="identity-provider-title-row">
|
|
<img class="idp-logo dark" src="./assets/images/zitadel-logo-solo-light.svg" alt="zitadel" />
|
|
<img class="idp-logo light" src="./assets/images/zitadel-logo-solo-dark.svg" alt="zitadel" />
|
|
<h1>{{ 'IDP.CREATE.ZITADEL.TITLE' | translate }}</h1>
|
|
@if (exists$ | async) {
|
|
@if ({ isNotActive: (activateLink$ | async) }; as idp) {
|
|
<div
|
|
class="cnsl-state-dot"
|
|
[matTooltip]="(idp.isNotActive ? 'IDP.STATES.2' : 'IDP.STATES.1') | translate"
|
|
[ngClass]="{ active: !idp.isNotActive, inactive: !!idp.isNotActive }"
|
|
></div>
|
|
}
|
|
}
|
|
@if (loading) {
|
|
<mat-spinner diameter="25" color="primary"></mat-spinner>
|
|
}
|
|
</div>
|
|
|
|
<cnsl-provider-next
|
|
[configureProvider]="(justCreated$ | async) === ''"
|
|
[configureTitle]="'DESCRIPTIONS.SETTINGS.IDPS.CALLBACK.TITLE' | translate: { provider: 'ZITADEL' }"
|
|
[configureDescription]="'DESCRIPTIONS.SETTINGS.IDPS.CALLBACK.DESCRIPTION' | translate: { provider: 'ZITADEL' }"
|
|
configureLink="https://zitadel.com/docs/guides/integrate/identity-providers"
|
|
[autofillLink]="autofillLink$ | async"
|
|
[activateLink]="activateLink$ | async"
|
|
[copyUrls]="copyUrls$ | async"
|
|
[expanded]="!!(expandWhatNow$ | async)"
|
|
(activate)="activate()"
|
|
></cnsl-provider-next>
|
|
|
|
<p class="identity-provider-desc cnsl-secondary-text">{{ 'IDP.CREATE.ZITADEL.DESCRIPTION' | translate }}</p>
|
|
|
|
<form [formGroup]="form" (ngSubmit)="submitForm()">
|
|
<div class="identity-provider-content">
|
|
<cnsl-form-field class="formfield">
|
|
<cnsl-label>{{ 'IDP.NAME' | translate }}</cnsl-label>
|
|
<input cnslInput formControlName="name" />
|
|
</cnsl-form-field>
|
|
<cnsl-form-field class="formfield">
|
|
<cnsl-label>{{ 'IDP.ISSUER' | translate }}</cnsl-label>
|
|
<input cnslInput formControlName="issuer" />
|
|
</cnsl-form-field>
|
|
</div>
|
|
|
|
<div class="identity-provider-content">
|
|
<cnsl-form-field class="formfield">
|
|
<cnsl-label>{{ 'IDP.CLIENTID' | translate }}</cnsl-label>
|
|
<input cnslInput formControlName="clientId" />
|
|
</cnsl-form-field>
|
|
|
|
@if (provider) {
|
|
<mat-checkbox [(ngModel)]="updateClientSecret" [ngModelOptions]="{ standalone: true }">{{
|
|
'IDP.UPDATECLIENTSECRET' | translate
|
|
}}</mat-checkbox>
|
|
}
|
|
@if (!provider || (provider && updateClientSecret)) {
|
|
<cnsl-form-field class="formfield">
|
|
<cnsl-label>{{ 'IDP.CLIENTSECRET' | translate }}</cnsl-label>
|
|
<input cnslInput formControlName="clientSecret" />
|
|
</cnsl-form-field>
|
|
}
|
|
|
|
@if (isAdmin) {
|
|
<div class="instance-roles-info">
|
|
<h2>{{ 'IDP.INSTANCEROLESINFO' | translate }}</h2>
|
|
<p class="checkbox-desc cnsl-secondary-text">{{ 'IDP.INSTANCEROLESINFO_DESC' | translate }}</p>
|
|
@for (info of instanceRolesInfoList; track $index) {
|
|
<div class="flex-line">
|
|
<cnsl-form-field class="formfield">
|
|
<cnsl-label>{{ 'IDP.ORGANIZATIONID' | translate }}</cnsl-label>
|
|
<input cnslInput [value]="info.organizationId" disabled />
|
|
</cnsl-form-field>
|
|
<cnsl-form-field class="formfield">
|
|
<cnsl-label>{{ 'IDP.ORGANIZATIONDOMAIN' | translate }}</cnsl-label>
|
|
<input cnslInput [value]="info.organizationDomain" disabled />
|
|
</cnsl-form-field>
|
|
<button type="button" mat-icon-button (click)="removeRolesInfo($index)">
|
|
<mat-icon>remove_circle_outline</mat-icon>
|
|
</button>
|
|
</div>
|
|
}
|
|
<div class="flex-line">
|
|
<cnsl-form-field class="formfield">
|
|
<cnsl-label>{{ 'IDP.ORGANIZATIONID' | translate }}</cnsl-label>
|
|
<input cnslInput [formControl]="rolesInfoOrgId" />
|
|
</cnsl-form-field>
|
|
<cnsl-form-field class="formfield">
|
|
<cnsl-label>{{ 'IDP.ORGANIZATIONDOMAIN' | translate }}</cnsl-label>
|
|
<input cnslInput [formControl]="rolesInfoOrgDomain" />
|
|
</cnsl-form-field>
|
|
<button
|
|
type="button"
|
|
mat-icon-button
|
|
[disabled]="!rolesInfoOrgId.value || !rolesInfoOrgDomain.value"
|
|
(click)="addRolesInfo()"
|
|
>
|
|
<mat-icon>add</mat-icon>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<div class="identity-provider-optional-h-wrapper">
|
|
<h2>{{ 'IDP.OPTIONAL' | translate }}</h2>
|
|
<button (click)="showOptional = !showOptional" type="button" mat-icon-button>
|
|
@if (showOptional) {
|
|
<mat-icon>keyboard_arrow_up</mat-icon>
|
|
}
|
|
@if (!showOptional) {
|
|
<mat-icon>keyboard_arrow_down</mat-icon>
|
|
}
|
|
</button>
|
|
</div>
|
|
@if (showOptional) {
|
|
<div>
|
|
<div class="idp-scopes">
|
|
<div class="flex-line">
|
|
<cnsl-form-field class="formfield">
|
|
<cnsl-label>{{ 'IDP.SCOPESLIST' | translate }}</cnsl-label>
|
|
<input
|
|
cnslInput
|
|
#scopeInput
|
|
[matChipInputFor]="chipScopesList"
|
|
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
|
[matChipInputAddOnBlur]="true"
|
|
(matChipInputTokenEnd)="addScope($event)"
|
|
/>
|
|
</cnsl-form-field>
|
|
<button class="scope-add-button" type="button" (click)="addScopeFromInput(scopeInput)" mat-icon-button>
|
|
<mat-icon>add</mat-icon>
|
|
</button>
|
|
</div>
|
|
<cnsl-form-field class="formfield">
|
|
<mat-chip-grid #chipScopesList aria-label="scope selection">
|
|
@for (scope of scopesList?.value; track scope) {
|
|
<mat-chip-row class="chip" removable (removed)="removeScope(scope)">
|
|
{{ scope }} <mat-icon matChipRemove>cancel</mat-icon>
|
|
</mat-chip-row>
|
|
}
|
|
</mat-chip-grid>
|
|
</cnsl-form-field>
|
|
</div>
|
|
<cnsl-provider-options
|
|
[initialOptions]="provider?.config?.options"
|
|
(optionsChanged)="options = $event"
|
|
></cnsl-provider-options>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<div class="identity-provider-create-actions">
|
|
<button
|
|
color="primary"
|
|
mat-raised-button
|
|
class="continue-button"
|
|
[disabled]="form.invalid || form.disabled"
|
|
type="submit"
|
|
>
|
|
@if ({ exists: (exists$ | async) }; as idp) {
|
|
<span>{{ (!!idp.exists ? 'ACTIONS.SAVE' : 'ACTIONS.CREATE') | translate }}</span>
|
|
}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</cnsl-create-layout>
|