feat(saml): implementation of saml for ZITADEL v2 (#3618)
5
.github/workflows/test-code.yml
vendored
@ -46,6 +46,10 @@ jobs:
|
|||||||
working-directory: e2e
|
working-directory: e2e
|
||||||
env:
|
env:
|
||||||
ZITADEL_IMAGE: zitadel:pr
|
ZITADEL_IMAGE: zitadel:pr
|
||||||
|
- name: Save ZITADEL Logs
|
||||||
|
if: always()
|
||||||
|
run: docker compose logs zitadel > ../.artifacts/e2e-compose-zitadel.log
|
||||||
|
working-directory: e2e
|
||||||
- name: Archive Test Results
|
- name: Archive Test Results
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
@ -55,4 +59,5 @@ jobs:
|
|||||||
e2e/cypress/results
|
e2e/cypress/results
|
||||||
e2e/cypress/videos
|
e2e/cypress/videos
|
||||||
e2e/cypress/screenshots
|
e2e/cypress/screenshots
|
||||||
|
.artifacts/e2e-compose-zitadel.log
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
branches: [
|
branches: [
|
||||||
{name: 'main'},
|
{name: 'main'},
|
||||||
{name: '1.87.x', range: '1.87.x', channel: '1.87.x'},
|
{name: '1.87.x', range: '1.87.x', channel: '1.87.x'}
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
"@semantic-release/commit-analyzer"
|
"@semantic-release/commit-analyzer"
|
||||||
|
@ -79,8 +79,8 @@ It's free for up to 25'000 authenticated requests. Learn more about the [pay-as-
|
|||||||
- Self-registration including verification
|
- Self-registration including verification
|
||||||
- User self service
|
- User self service
|
||||||
- [Service Accounts](https://docs.zitadel.com/docs/guides/integrate/serviceusers)
|
- [Service Accounts](https://docs.zitadel.com/docs/guides/integrate/serviceusers)
|
||||||
- [OpenID Connect certified](https://openid.net/certification/#OPs)
|
- [OpenID Connect certified](https://openid.net/certification/#OPs) => [OIDC Endpoints](https://docs.zitadel.com/docs/apis/openidoauth/endpoints), [OIDC Integration Guides](https://docs.zitadel.com/docs/guides/integrate/auth0-oidc)
|
||||||
- 🚧 [SAML 2.0](https://github.com/zitadel/zitadel/pull/3618)
|
- [SAML 2.0](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html) => [SAML Endpoints](https://docs.zitadel.com/docs/apis/saml/endpoints), [SAML Integration Guides](https://docs.zitadel.com/docs/guides/integrate/auth0-saml)
|
||||||
- [Postgres](https://docs.zitadel.com/docs/guides/manage/self-hosted/database#postgres) (version >= 14) or [CockroachDB](https://docs.zitadel.com/docs/guides/manage/self-hosted/database#cockroach) (version >= 22.0)
|
- [Postgres](https://docs.zitadel.com/docs/guides/manage/self-hosted/database#postgres) (version >= 14) or [CockroachDB](https://docs.zitadel.com/docs/guides/manage/self-hosted/database#cockroach) (version >= 22.0)
|
||||||
|
|
||||||
Track upcoming features on our [roadmap](https://zitadel.com/roadmap).
|
Track upcoming features on our [roadmap](https://zitadel.com/roadmap).
|
||||||
|
@ -200,6 +200,25 @@ OIDC:
|
|||||||
Keys:
|
Keys:
|
||||||
Path: /oauth/v2/keys
|
Path: /oauth/v2/keys
|
||||||
|
|
||||||
|
SAML:
|
||||||
|
ProviderConfig:
|
||||||
|
MetadataConfig:
|
||||||
|
Path: "/metadata"
|
||||||
|
SignatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
|
||||||
|
IDPConfig:
|
||||||
|
SignatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
|
||||||
|
WantAuthRequestsSigned: true
|
||||||
|
Endpoints:
|
||||||
|
#Organisation:
|
||||||
|
# Name: ZITADEL
|
||||||
|
# URL: https://zitadel.com
|
||||||
|
#ContactPerson:
|
||||||
|
# ContactType: "technical"
|
||||||
|
# Company: ZITADEL
|
||||||
|
# EmailAddress: hi@zitadel.com
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Login:
|
Login:
|
||||||
LanguageCookieName: zitadel.login.lang
|
LanguageCookieName: zitadel.login.lang
|
||||||
CSRFCookieName: zitadel.login.csrf
|
CSRFCookieName: zitadel.login.csrf
|
||||||
@ -234,6 +253,9 @@ EncryptionKeys:
|
|||||||
OIDC:
|
OIDC:
|
||||||
EncryptionKeyID: "oidcKey"
|
EncryptionKeyID: "oidcKey"
|
||||||
DecryptionKeyIDs:
|
DecryptionKeyIDs:
|
||||||
|
SAML:
|
||||||
|
EncryptionKeyID: "samlKey"
|
||||||
|
DecryptionKeyIDs:
|
||||||
OTP:
|
OTP:
|
||||||
EncryptionKeyID: "otpKey"
|
EncryptionKeyID: "otpKey"
|
||||||
DecryptionKeyIDs:
|
DecryptionKeyIDs:
|
||||||
@ -277,8 +299,10 @@ SystemDefaults:
|
|||||||
FileSystemPath: ".notifications/"
|
FileSystemPath: ".notifications/"
|
||||||
KeyConfig:
|
KeyConfig:
|
||||||
Size: 2048
|
Size: 2048
|
||||||
|
CertificateSize: 4096
|
||||||
PrivateKeyLifetime: 6h
|
PrivateKeyLifetime: 6h
|
||||||
PublicKeyLifetime: 30h
|
PublicKeyLifetime: 30h
|
||||||
|
CertificateLifetime: 8766h
|
||||||
|
|
||||||
DefaultInstance:
|
DefaultInstance:
|
||||||
InstanceName:
|
InstanceName:
|
||||||
|
@ -70,7 +70,10 @@ func (mig *FirstInstance) Execute(ctx context.Context) error {
|
|||||||
nil,
|
nil,
|
||||||
userAlg,
|
userAlg,
|
||||||
nil,
|
nil,
|
||||||
nil)
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -47,7 +47,10 @@ func (mig *externalConfigChange) Execute(ctx context.Context) error {
|
|||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
nil)
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/zitadel/logging"
|
"github.com/zitadel/logging"
|
||||||
|
"github.com/zitadel/zitadel/internal/api/saml"
|
||||||
|
|
||||||
admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing"
|
admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing"
|
||||||
internal_authz "github.com/zitadel/zitadel/internal/api/authz"
|
internal_authz "github.com/zitadel/zitadel/internal/api/authz"
|
||||||
@ -45,6 +46,7 @@ type Config struct {
|
|||||||
Admin admin_es.Config
|
Admin admin_es.Config
|
||||||
UserAgentCookie *middleware.UserAgentCookieConfig
|
UserAgentCookie *middleware.UserAgentCookieConfig
|
||||||
OIDC oidc.Config
|
OIDC oidc.Config
|
||||||
|
SAML saml.Config
|
||||||
Login login.Config
|
Login login.Config
|
||||||
Console console.Config
|
Console console.Config
|
||||||
AssetStorage static_config.AssetStorageConfig
|
AssetStorage static_config.AssetStorageConfig
|
||||||
@ -90,6 +92,7 @@ type encryptionKeyConfig struct {
|
|||||||
DomainVerification *crypto.KeyConfig
|
DomainVerification *crypto.KeyConfig
|
||||||
IDPConfig *crypto.KeyConfig
|
IDPConfig *crypto.KeyConfig
|
||||||
OIDC *crypto.KeyConfig
|
OIDC *crypto.KeyConfig
|
||||||
|
SAML *crypto.KeyConfig
|
||||||
OTP *crypto.KeyConfig
|
OTP *crypto.KeyConfig
|
||||||
SMS *crypto.KeyConfig
|
SMS *crypto.KeyConfig
|
||||||
SMTP *crypto.KeyConfig
|
SMTP *crypto.KeyConfig
|
||||||
|
@ -10,6 +10,7 @@ var (
|
|||||||
"domainVerificationKey",
|
"domainVerificationKey",
|
||||||
"idpConfigKey",
|
"idpConfigKey",
|
||||||
"oidcKey",
|
"oidcKey",
|
||||||
|
"samlKey",
|
||||||
"otpKey",
|
"otpKey",
|
||||||
"smsKey",
|
"smsKey",
|
||||||
"smtpKey",
|
"smtpKey",
|
||||||
@ -23,6 +24,7 @@ type encryptionKeys struct {
|
|||||||
DomainVerification crypto.EncryptionAlgorithm
|
DomainVerification crypto.EncryptionAlgorithm
|
||||||
IDPConfig crypto.EncryptionAlgorithm
|
IDPConfig crypto.EncryptionAlgorithm
|
||||||
OIDC crypto.EncryptionAlgorithm
|
OIDC crypto.EncryptionAlgorithm
|
||||||
|
SAML crypto.EncryptionAlgorithm
|
||||||
OTP crypto.EncryptionAlgorithm
|
OTP crypto.EncryptionAlgorithm
|
||||||
SMS crypto.EncryptionAlgorithm
|
SMS crypto.EncryptionAlgorithm
|
||||||
SMTP crypto.EncryptionAlgorithm
|
SMTP crypto.EncryptionAlgorithm
|
||||||
@ -49,6 +51,10 @@ func ensureEncryptionKeys(keyConfig *encryptionKeyConfig, keyStorage crypto.KeyS
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
keys.SAML, err = crypto.NewAESCrypto(keyConfig.SAML, keyStorage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
key, err := crypto.LoadKey(keyConfig.OIDC.EncryptionKeyID, keyStorage)
|
key, err := crypto.LoadKey(keyConfig.OIDC.EncryptionKeyID, keyStorage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -13,6 +13,10 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/zitadel/saml/pkg/provider"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/saml"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
@ -100,7 +104,7 @@ func startZitadel(config *Config, masterKey string) error {
|
|||||||
return fmt.Errorf("cannot start eventstore for queries: %w", err)
|
return fmt.Errorf("cannot start eventstore for queries: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
queries, err := query.StartQueries(ctx, eventstoreClient, dbClient, config.Projections, config.SystemDefaults, keys.IDPConfig, keys.OTP, keys.OIDC, config.InternalAuthZ.RolePermissionMappings)
|
queries, err := query.StartQueries(ctx, eventstoreClient, dbClient, config.Projections, config.SystemDefaults, keys.IDPConfig, keys.OTP, keys.OIDC, keys.SAML, config.InternalAuthZ.RolePermissionMappings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot start queries: %w", err)
|
return fmt.Errorf("cannot start queries: %w", err)
|
||||||
}
|
}
|
||||||
@ -134,6 +138,8 @@ func startZitadel(config *Config, masterKey string) error {
|
|||||||
keys.User,
|
keys.User,
|
||||||
keys.DomainVerification,
|
keys.DomainVerification,
|
||||||
keys.OIDC,
|
keys.OIDC,
|
||||||
|
keys.SAML,
|
||||||
|
&http.Client{},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot start commands: %w", err)
|
return fmt.Errorf("cannot start commands: %w", err)
|
||||||
@ -208,13 +214,19 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman
|
|||||||
return fmt.Errorf("unable to start oidc provider: %w", err)
|
return fmt.Errorf("unable to start oidc provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
samlProvider, err := saml.NewProvider(ctx, config.SAML, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.SAML, eventstore, dbClient, instanceInterceptor.Handler, userAgentInterceptor)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to start saml provider: %w", err)
|
||||||
|
}
|
||||||
|
apis.RegisterHandler(saml.HandlerPrefix, samlProvider.HttpHandler())
|
||||||
|
|
||||||
c, err := console.Start(config.Console, config.ExternalSecure, oidcProvider.IssuerFromRequest, instanceInterceptor.Handler, config.CustomerPortal)
|
c, err := console.Start(config.Console, config.ExternalSecure, oidcProvider.IssuerFromRequest, instanceInterceptor.Handler, config.CustomerPortal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to start console: %w", err)
|
return fmt.Errorf("unable to start console: %w", err)
|
||||||
}
|
}
|
||||||
apis.RegisterHandler(console.HandlerPrefix, c)
|
apis.RegisterHandler(console.HandlerPrefix, c)
|
||||||
|
|
||||||
l, err := login.CreateLogin(config.Login, commands, queries, authRepo, store, console.HandlerPrefix+"/", op.AuthCallbackURL(oidcProvider), config.ExternalSecure, userAgentInterceptor, op.NewIssuerInterceptor(oidcProvider.IssuerFromRequest).Handler, instanceInterceptor.Handler, assetsCache.Handler, keys.User, keys.IDPConfig, keys.CSRFCookieKey)
|
l, err := login.CreateLogin(config.Login, commands, queries, authRepo, store, console.HandlerPrefix+"/", op.AuthCallbackURL(oidcProvider), provider.AuthCallbackURL(samlProvider), config.ExternalSecure, userAgentInterceptor, op.NewIssuerInterceptor(oidcProvider.IssuerFromRequest).Handler, provider.NewIssuerInterceptor(samlProvider.IssuerFromRequest).Handler, instanceInterceptor.Handler, assetsCache.Handler, keys.User, keys.IDPConfig, keys.CSRFCookieKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to start login: %w", err)
|
return fmt.Errorf("unable to start login: %w", err)
|
||||||
}
|
}
|
||||||
|
895
console/package-lock.json
generated
@ -64,8 +64,8 @@
|
|||||||
"@types/jasminewd2": "~2.0.10",
|
"@types/jasminewd2": "~2.0.10",
|
||||||
"@types/jsonwebtoken": "^8.5.5",
|
"@types/jsonwebtoken": "^8.5.5",
|
||||||
"@types/node": "^17.0.42",
|
"@types/node": "^17.0.42",
|
||||||
"@typescript-eslint/eslint-plugin": "5.35.1",
|
"@typescript-eslint/eslint-plugin": "5.36.1",
|
||||||
"@typescript-eslint/parser": "5.30.4",
|
"@typescript-eslint/parser": "5.36.1",
|
||||||
"codelyzer": "^6.0.0",
|
"codelyzer": "^6.0.0",
|
||||||
"eslint": "^8.18.0",
|
"eslint": "^8.18.0",
|
||||||
"jasmine-core": "~4.2.0",
|
"jasmine-core": "~4.2.0",
|
||||||
|
@ -86,6 +86,7 @@ const authConfig: AuthConfig = {
|
|||||||
scope: 'openid profile email', // offline_access
|
scope: 'openid profile email', // offline_access
|
||||||
responseType: 'code',
|
responseType: 'code',
|
||||||
oidc: true,
|
oidc: true,
|
||||||
|
requireHttps: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -1,5 +1,13 @@
|
|||||||
<div class="cnsl-app-card" [ngClass]="{'add': type === OIDCAppType.ADD,'web': type === OIDCAppType.OIDC_APP_TYPE_WEB,
|
<div
|
||||||
'useragent': type === OIDCAppType.OIDC_APP_TYPE_USER_AGENT,
|
class="cnsl-app-card"
|
||||||
'native': type === OIDCAppType.OIDC_APP_TYPE_NATIVE, 'api': isApiApp}">
|
[ngClass]="{
|
||||||
|
add: type === OIDCAppType.ADD,
|
||||||
|
web: type === OIDCAppType.OIDC_APP_TYPE_WEB,
|
||||||
|
useragent: type === OIDCAppType.OIDC_APP_TYPE_USER_AGENT,
|
||||||
|
native: type === OIDCAppType.OIDC_APP_TYPE_NATIVE,
|
||||||
|
api: isApiApp,
|
||||||
|
saml: type === 'SAML'
|
||||||
|
}"
|
||||||
|
>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
@ -55,5 +55,11 @@
|
|||||||
border: none;
|
border: none;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.saml {
|
||||||
|
background: linear-gradient(40deg, rgb(110, 56, 124), rgb(88, 37, 103));
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import { OIDCAppType } from 'src/app/proto/generated/zitadel/app_pb';
|
|||||||
})
|
})
|
||||||
export class AppCardComponent {
|
export class AppCardComponent {
|
||||||
@Input() public outline: boolean = false;
|
@Input() public outline: boolean = false;
|
||||||
@Input() public type: OIDCAppType | undefined = undefined;
|
@Input() public type: OIDCAppType | 'SAML' | undefined = undefined;
|
||||||
@Input() public isApiApp: boolean = false;
|
@Input() public isApiApp: boolean = false;
|
||||||
public OIDCAppType: any = OIDCAppType;
|
public OIDCAppType: any = OIDCAppType;
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,13 @@
|
|||||||
<ng-container *ngFor="let type of types">
|
<ng-container *ngFor="let type of types">
|
||||||
<input class="app" type="radio" (change)="emitChange()" [value]="type" [(ngModel)]="selected" [id]="type.prefix" />
|
<input class="app" type="radio" (change)="emitChange()" [value]="type" [(ngModel)]="selected" [id]="type.prefix" />
|
||||||
<label class="cnsl-type-radio-button" [for]="type.prefix">
|
<label class="cnsl-type-radio-button" [for]="type.prefix">
|
||||||
<div class="cnsl-type-radio-header" [ngStyle]="{'background': type.background}">
|
<div class="cnsl-type-radio-header" [ngStyle]="{ background: type.background }">
|
||||||
<span>{{type.prefix}}</span>
|
<span>{{ type.prefix }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p>{{type.titleI18nKey | translate}}</p>
|
<p>{{ type.titleI18nKey | translate }}</p>
|
||||||
<p class="type-desc cnsl-secondary-text">{{type.descI18nKey | translate}}</p>
|
<p class="type-desc cnsl-secondary-text">{{ type.descI18nKey | translate }}</p>
|
||||||
<span class="fill-space"></span>
|
<span class="fill-space"></span>
|
||||||
|
<span class="cnsl-type-protocol state" *ngIf="type.protocol">{{ type.protocol }}</span>
|
||||||
</label>
|
</label>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
@ -60,6 +60,8 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-top-left-radius: 6px;
|
border-top-left-radius: 6px;
|
||||||
border-top-right-radius: 6px;
|
border-top-right-radius: 6px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
margin: 2rem;
|
margin: 2rem;
|
||||||
@ -76,5 +78,10 @@
|
|||||||
.type-desc {
|
.type-desc {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cnsl-type-protocol {
|
||||||
|
width: fit-content;
|
||||||
|
margin: 0.5rem auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,24 @@
|
|||||||
<cnsl-create-layout
|
<cnsl-create-layout
|
||||||
title="{{ 'APP.PAGES.CREATE_OIDC' | translate }}"
|
title="{{ 'APP.PAGES.CREATE' | translate }}"
|
||||||
[createSteps]="createSteps"
|
[createSteps]="
|
||||||
|
appType?.value?.createType === AppCreateType.OIDC
|
||||||
|
? appType?.value.oidcAppType !== OIDCAppType.OIDC_APP_TYPE_NATIVE
|
||||||
|
? 4
|
||||||
|
: 3
|
||||||
|
: appType?.value?.createType === AppCreateType.API
|
||||||
|
? 3
|
||||||
|
: appType?.value?.createType === AppCreateType.SAML
|
||||||
|
? 3
|
||||||
|
: 0
|
||||||
|
"
|
||||||
[currentCreateStep]="currentCreateStep"
|
[currentCreateStep]="currentCreateStep"
|
||||||
(closed)="close()"
|
(closed)="close()"
|
||||||
>
|
>
|
||||||
<h1>{{ 'APP.PAGES.CREATE_OIDC_DESC_TITLE' | translate }}</h1>
|
<h1>{{ 'APP.PAGES.CREATE_DESC_TITLE' | translate }}</h1>
|
||||||
<mat-progress-bar class="progress-bar" color="primary" *ngIf="loading" mode="indeterminate"></mat-progress-bar>
|
<mat-progress-bar class="progress-bar" color="primary" *ngIf="loading" mode="indeterminate"></mat-progress-bar>
|
||||||
|
|
||||||
<mat-checkbox class="proswitch" color="primary" [(ngModel)]="devmode">
|
<mat-checkbox class="proswitch" color="primary" [(ngModel)]="devmode">
|
||||||
{{ 'APP.OIDC.PROSWITCH' | translate }}
|
{{ 'APP.PROSWITCH' | translate }}
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
|
|
||||||
<mat-horizontal-stepper
|
<mat-horizontal-stepper
|
||||||
@ -21,16 +31,16 @@
|
|||||||
>
|
>
|
||||||
<mat-step [stepControl]="firstFormGroup" [editable]="true">
|
<mat-step [stepControl]="firstFormGroup" [editable]="true">
|
||||||
<form [formGroup]="firstFormGroup">
|
<form [formGroup]="firstFormGroup">
|
||||||
<ng-template matStepLabel>{{ 'APP.OIDC.NAMEANDTYPESECTION' | translate }}</ng-template>
|
<ng-template matStepLabel>{{ 'APP.NAMEANDTYPESECTION' | translate }}</ng-template>
|
||||||
|
|
||||||
<p class="step-title">{{ 'APP.OIDC.TITLEFIRST' | translate }}</p>
|
<p class="step-title">{{ 'APP.TITLEFIRST' | translate }}</p>
|
||||||
<cnsl-form-field class="name-formfield">
|
<cnsl-form-field class="name-formfield">
|
||||||
<cnsl-label>{{ 'APP.NAME' | translate }}</cnsl-label>
|
<cnsl-label>{{ 'APP.NAME' | translate }}</cnsl-label>
|
||||||
<input cnslInput cdkFocusInitial formControlName="name" />
|
<input cnslInput cdkFocusInitial formControlName="name" />
|
||||||
<span cnslError *ngIf="name?.errors?.required">{{ 'PROJECT.APP.NAMEREQUIRED' | translate }}</span>
|
<span cnslError *ngIf="name?.errors?.required">{{ 'PROJECT.APP.NAMEREQUIRED' | translate }}</span>
|
||||||
</cnsl-form-field>
|
</cnsl-form-field>
|
||||||
|
|
||||||
<p class="step-title">{{ 'APP.OIDC.TYPETITLE' | translate }}</p>
|
<p class="step-title">{{ 'APP.TYPETITLE' | translate }}</p>
|
||||||
|
|
||||||
<cnsl-type-radio [types]="appTypes" (selectedType)="appType?.setValue($event)" [selected]="appType?.value">
|
<cnsl-type-radio [types]="appTypes" (selectedType)="appType?.setValue($event)" [selected]="appType?.value">
|
||||||
</cnsl-type-radio>
|
</cnsl-type-radio>
|
||||||
@ -48,9 +58,13 @@
|
|||||||
</form>
|
</form>
|
||||||
</mat-step>
|
</mat-step>
|
||||||
|
|
||||||
<!-- skip for native applications -->
|
<!-- skip for native OIDC and SAML applications -->
|
||||||
<mat-step
|
<mat-step
|
||||||
*ngIf="oidcAppRequest.appType !== OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
*ngIf="
|
||||||
|
(appType?.value?.createType === AppCreateType.OIDC &&
|
||||||
|
appType?.value.oidcAppType !== OIDCAppType.OIDC_APP_TYPE_NATIVE) ||
|
||||||
|
appType?.value?.createType === AppCreateType.API
|
||||||
|
"
|
||||||
[stepControl]="secondFormGroup"
|
[stepControl]="secondFormGroup"
|
||||||
[editable]="true"
|
[editable]="true"
|
||||||
>
|
>
|
||||||
@ -85,33 +99,39 @@
|
|||||||
<ng-template matStepLabel>{{ 'APP.OIDC.REDIRECTSECTION' | translate }}</ng-template>
|
<ng-template matStepLabel>{{ 'APP.OIDC.REDIRECTSECTION' | translate }}</ng-template>
|
||||||
|
|
||||||
<p class="step-title">{{ 'APP.OIDC.REDIRECTTITLE' | translate }}</p>
|
<p class="step-title">{{ 'APP.OIDC.REDIRECTTITLE' | translate }}</p>
|
||||||
<p class="step-description cnsl-secondary-text" *ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
|
<p
|
||||||
|
class="step-description cnsl-secondary-text"
|
||||||
|
*ngIf="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
||||||
|
>
|
||||||
{{ 'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate }}
|
{{ 'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate }}
|
||||||
</p>
|
</p>
|
||||||
<p class="step-description cnsl-secondary-text" *ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_WEB">
|
<p class="step-description cnsl-secondary-text" *ngIf="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_WEB">
|
||||||
{{ 'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate }}
|
{{ 'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<cnsl-redirect-uris
|
<cnsl-redirect-uris
|
||||||
class="redirect-section"
|
class="redirect-section"
|
||||||
[canWrite]="true"
|
[canWrite]="true"
|
||||||
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
[isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
||||||
(changedUris)="oidcAppRequest.redirectUrisList = $any($event)"
|
(changedUris)="oidcAppRequest.setRedirectUrisList($any($event))"
|
||||||
[urisList]="oidcAppRequest.redirectUrisList"
|
[urisList]="oidcAppRequest.toObject().redirectUrisList"
|
||||||
[getValues]="requestRedirectValuesSubject$"
|
[getValues]="requestRedirectValuesSubject$"
|
||||||
title="{{ 'APP.OIDC.REDIRECT' | translate }}"
|
title="{{ 'APP.OIDC.REDIRECT' | translate }}"
|
||||||
>
|
>
|
||||||
</cnsl-redirect-uris>
|
</cnsl-redirect-uris>
|
||||||
|
|
||||||
<p class="step-title">{{ 'APP.OIDC.POSTREDIRECTTITLE' | translate }}</p>
|
<p class="step-title">{{ 'APP.OIDC.POSTREDIRECTTITLE' | translate }}</p>
|
||||||
<p class="step-description cnsl-secondary-text" *ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
|
<p
|
||||||
|
class="step-description cnsl-secondary-text"
|
||||||
|
*ngIf="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
||||||
|
>
|
||||||
{{ 'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate }}
|
{{ 'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate }}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
class="step-description cnsl-secondary-text"
|
class="step-description cnsl-secondary-text"
|
||||||
*ngIf="
|
*ngIf="
|
||||||
oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_WEB ||
|
appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_WEB ||
|
||||||
oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_USER_AGENT
|
appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_USER_AGENT
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{ 'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate }}
|
{{ 'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate }}
|
||||||
@ -120,11 +140,11 @@
|
|||||||
<cnsl-redirect-uris
|
<cnsl-redirect-uris
|
||||||
class="redirect-section"
|
class="redirect-section"
|
||||||
[canWrite]="true"
|
[canWrite]="true"
|
||||||
(changedUris)="oidcAppRequest.postLogoutRedirectUrisList = $any($event)"
|
(changedUris)="oidcAppRequest.setPostLogoutRedirectUrisList($any($event))"
|
||||||
[urisList]="oidcAppRequest.postLogoutRedirectUrisList"
|
[urisList]="oidcAppRequest.toObject().postLogoutRedirectUrisList"
|
||||||
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
|
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
|
||||||
[getValues]="requestRedirectValuesSubject$"
|
[getValues]="requestRedirectValuesSubject$"
|
||||||
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
[isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
||||||
>
|
>
|
||||||
</cnsl-redirect-uris>
|
</cnsl-redirect-uris>
|
||||||
|
|
||||||
@ -136,97 +156,173 @@
|
|||||||
</div>
|
</div>
|
||||||
</mat-step>
|
</mat-step>
|
||||||
|
|
||||||
|
<mat-step *ngIf="appType?.value?.createType === AppCreateType.SAML" [editable]="true">
|
||||||
|
<ng-template matStepLabel>{{ 'APP.SAML.CONFIGSECTION' | translate }}</ng-template>
|
||||||
|
|
||||||
|
<form [formGroup]="samlConfigForm">
|
||||||
|
<cnsl-form-field class="formfield">
|
||||||
|
<cnsl-label>{{ 'APP.SAML.URL' | translate }}</cnsl-label>
|
||||||
|
<input cnslInput formControlName="metadataUrl" placeholder="https://" />
|
||||||
|
</cnsl-form-field>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<span class="cnsl-app-or cnsl-secondary-text">{{ 'APP.SAML.OR' | translate }}</span>
|
||||||
|
|
||||||
|
<input
|
||||||
|
#xmlFileInput
|
||||||
|
style="display: none"
|
||||||
|
class="file-input"
|
||||||
|
type="file"
|
||||||
|
accept="text/xml,application/xml"
|
||||||
|
(change)="onDropXML($any($event.target).files)"
|
||||||
|
/>
|
||||||
|
<button type="button" mat-stroked-button (click)="$event.preventDefault(); xmlFileInput.click()">
|
||||||
|
{{ 'APP.SAML.XML' | translate }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="saml-xml"
|
||||||
|
[ngClass]="{ disabled: !!metadataUrl?.value }"
|
||||||
|
*ngIf="decodedBase64 && samlAppRequest.toObject().metadataXml && !samlAppRequest.toObject().metadataUrl"
|
||||||
|
>
|
||||||
|
<!-- <p class="preview-text cnsl-secondary-text">PREVIEW</p> -->
|
||||||
|
<ngx-codemirror
|
||||||
|
[(ngModel)]="decodedBase64"
|
||||||
|
[options]="{
|
||||||
|
lineNumbers: true,
|
||||||
|
theme: 'material',
|
||||||
|
mode: 'application/xml'
|
||||||
|
}"
|
||||||
|
></ngx-codemirror>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="app-create-actions">
|
||||||
|
<button mat-stroked-button class="bck-button" matStepperPrevious>{{ 'ACTIONS.BACK' | translate }}</button>
|
||||||
|
<button mat-raised-button color="primary" matStepperNext [attr.data-e2e]="'continue-button-redirecturis'">
|
||||||
|
{{ 'ACTIONS.CONTINUE' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</mat-step>
|
||||||
|
|
||||||
<mat-step>
|
<mat-step>
|
||||||
<ng-template matStepLabel>{{ 'APP.OIDC.OVERVIEWSECTION' | translate }}</ng-template>
|
<ng-template matStepLabel>{{ 'APP.OIDC.OVERVIEWSECTION' | translate }}</ng-template>
|
||||||
<p class="step-title">{{ 'APP.OIDC.OVERVIEWTITLE' | translate }}</p>
|
<p class="step-title">{{ 'APP.OIDC.OVERVIEWTITLE' | translate }}</p>
|
||||||
<div class="row cnsl-secondary-text">
|
<div class="row">
|
||||||
<span class="left">
|
<span class="left cnsl-secondary-text">
|
||||||
{{ 'APP.NAME' | translate }}
|
{{ 'APP.NAME' | translate }}
|
||||||
</span>
|
</span>
|
||||||
<span class="right">
|
<span class="right">
|
||||||
{{ oidcAppRequest.name }}
|
{{ name?.value }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="appType?.value?.createType === AppCreateType.OIDC">
|
<ng-container *ngIf="appType?.value?.createType === AppCreateType.OIDC">
|
||||||
<div class="row cnsl-secondary-text">
|
<div class="row">
|
||||||
<span class="left">
|
<span class="left cnsl-secondary-text">
|
||||||
{{ 'APP.TYPE' | translate }}
|
{{ 'APP.TYPE' | translate }}
|
||||||
</span>
|
</span>
|
||||||
<span class="right">
|
<span class="right">
|
||||||
{{ 'APP.OIDC.APPTYPE.' + oidcAppRequest.appType | translate }}
|
{{ 'APP.OIDC.APPTYPE.' + oidcAppRequest.toObject().appType | translate }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row cnsl-secondary-text">
|
<div class="row">
|
||||||
<span class="left">
|
<span class="left cnsl-secondary-text">
|
||||||
{{ 'APP.GRANT' | translate }}
|
{{ 'APP.GRANT' | translate }}
|
||||||
</span>
|
</span>
|
||||||
<span class="right" *ngIf="oidcAppRequest.grantTypesList && oidcAppRequest.grantTypesList.length > 0">
|
<span
|
||||||
[<span *ngFor="let element of oidcAppRequest.grantTypesList; index as i">
|
class="right"
|
||||||
|
*ngIf="oidcAppRequest.toObject().grantTypesList && oidcAppRequest.toObject().grantTypesList.length > 0"
|
||||||
|
>
|
||||||
|
[<span *ngFor="let element of oidcAppRequest.toObject().grantTypesList; index as i">
|
||||||
{{ 'APP.OIDC.GRANT.' + element | translate }}
|
{{ 'APP.OIDC.GRANT.' + element | translate }}
|
||||||
{{ i < oidcAppRequest.grantTypesList.length - 1 ? ', ' : '' }} </span
|
{{ i < oidcAppRequest.toObject().grantTypesList.length - 1 ? ', ' : '' }} </span
|
||||||
>]
|
>]
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row cnsl-secondary-text">
|
<div class="row">
|
||||||
<span class="left">
|
<span class="left cnsl-secondary-text">
|
||||||
{{ 'APP.OIDC.RESPONSETYPE' | translate }}
|
{{ 'APP.OIDC.RESPONSETYPE' | translate }}
|
||||||
</span>
|
</span>
|
||||||
<span class="right" *ngIf="oidcAppRequest.responseTypesList && oidcAppRequest.responseTypesList.length > 0">
|
<span
|
||||||
[<span *ngFor="let element of oidcAppRequest.responseTypesList; index as i">
|
class="right"
|
||||||
|
*ngIf="oidcAppRequest.toObject().responseTypesList && oidcAppRequest.toObject().responseTypesList.length > 0"
|
||||||
|
>
|
||||||
|
[<span *ngFor="let element of oidcAppRequest.toObject().responseTypesList; index as i">
|
||||||
{{ 'APP.OIDC.RESPONSE.' + element | translate }}
|
{{ 'APP.OIDC.RESPONSE.' + element | translate }}
|
||||||
{{ i < oidcAppRequest.responseTypesList.length - 1 ? ', ' : '' }} </span
|
{{ i < oidcAppRequest.toObject().responseTypesList.length - 1 ? ', ' : '' }} </span
|
||||||
>]
|
>]
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row cnsl-secondary-text">
|
<div class="row">
|
||||||
<span class="left">
|
<span class="left cnsl-secondary-text">
|
||||||
{{ 'APP.AUTHMETHOD' | translate }}
|
{{ 'APP.AUTHMETHOD' | translate }}
|
||||||
</span>
|
</span>
|
||||||
<span class="right">
|
<span class="right">
|
||||||
<span>
|
<span>
|
||||||
{{ 'APP.OIDC.AUTHMETHOD.' + oidcAppRequest.authMethodType | translate }}
|
{{ 'APP.OIDC.AUTHMETHOD.' + oidcAppRequest.toObject().authMethodType | translate }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row cnsl-secondary-text">
|
<div class="row">
|
||||||
<span class="left">
|
<span class="left cnsl-secondary-text">
|
||||||
{{ 'APP.OIDC.REDIRECT' | translate }}
|
{{ 'APP.OIDC.REDIRECT' | translate }}
|
||||||
</span>
|
</span>
|
||||||
<span class="right" *ngIf="oidcAppRequest.redirectUrisList && oidcAppRequest.redirectUrisList.length > 0">
|
<span
|
||||||
[<span *ngFor="let redirect of oidcAppRequest.redirectUrisList; index as i">
|
class="right"
|
||||||
|
*ngIf="oidcAppRequest.toObject().redirectUrisList && oidcAppRequest.toObject().redirectUrisList.length > 0"
|
||||||
|
>
|
||||||
|
[<span *ngFor="let redirect of oidcAppRequest.toObject().redirectUrisList; index as i">
|
||||||
{{ redirect }}
|
{{ redirect }}
|
||||||
{{ i < oidcAppRequest.redirectUrisList.length - 1 ? ', ' : '' }} </span
|
{{ i < oidcAppRequest.toObject().redirectUrisList.length - 1 ? ', ' : '' }} </span
|
||||||
>]
|
>]
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row cnsl-secondary-text">
|
<div class="row">
|
||||||
<span class="left">
|
<span class="left cnsl-secondary-text">
|
||||||
{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}
|
{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="right"
|
class="right"
|
||||||
*ngIf="oidcAppRequest.postLogoutRedirectUrisList && oidcAppRequest.postLogoutRedirectUrisList.length > 0"
|
*ngIf="
|
||||||
|
oidcAppRequest.toObject().postLogoutRedirectUrisList &&
|
||||||
|
oidcAppRequest.toObject().postLogoutRedirectUrisList.length > 0
|
||||||
|
"
|
||||||
>
|
>
|
||||||
[<span *ngFor="let redirect of oidcAppRequest.postLogoutRedirectUrisList; index as i">
|
[<span *ngFor="let redirect of oidcAppRequest.toObject().postLogoutRedirectUrisList; index as i">
|
||||||
{{ redirect }}
|
{{ redirect }}
|
||||||
{{ i < oidcAppRequest.postLogoutRedirectUrisList.length - 1 ? ', ' : '' }} </span
|
{{ i < oidcAppRequest.toObject().postLogoutRedirectUrisList.length - 1 ? ', ' : '' }} </span
|
||||||
>]
|
>]
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="appType?.value?.createType === AppCreateType.API">
|
<ng-container *ngIf="appType?.value?.createType === AppCreateType.API">
|
||||||
<div class="row cnsl-secondary-text">
|
<div class="row">
|
||||||
<span class="left">
|
<span class="left cnsl-secondary-text">
|
||||||
{{ 'APP.AUTHMETHOD' | translate }}
|
{{ 'APP.AUTHMETHOD' | translate }}
|
||||||
</span>
|
</span>
|
||||||
<span class="right">
|
<span class="right">
|
||||||
<span>
|
<span>
|
||||||
{{ 'APP.API.AUTHMETHOD.' + apiAppRequest.authMethodType | translate }}
|
{{ 'APP.API.AUTHMETHOD.' + authMethodType?.value | translate }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="appType?.value?.createType === AppCreateType.SAML">
|
||||||
|
<div class="row">
|
||||||
|
<span class="left cnsl-secondary-text">
|
||||||
|
{{ 'APP.SAML.METADATA' | translate }}
|
||||||
|
</span>
|
||||||
|
<span class="right">
|
||||||
|
<span *ngIf="metadataUrl?.value">
|
||||||
|
{{ metadataUrl?.value }}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="samlAppRequest.toObject().metadataXml">
|
||||||
|
{{ 'APP.SAML.METADATAFROMFILE' | translate }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -278,6 +374,7 @@
|
|||||||
</cnsl-form-field>
|
</cnsl-form-field>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="formappType?.value?.createType !== AppCreateType.SAML">
|
||||||
<cnsl-form-field class="formfield">
|
<cnsl-form-field class="formfield">
|
||||||
<cnsl-label>{{ 'APP.AUTHMETHOD' | translate }}</cnsl-label>
|
<cnsl-label>{{ 'APP.AUTHMETHOD' | translate }}</cnsl-label>
|
||||||
<mat-select formControlName="authMethodType">
|
<mat-select formControlName="authMethodType">
|
||||||
@ -287,6 +384,46 @@
|
|||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</cnsl-form-field>
|
</cnsl-form-field>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<ng-container *ngIf="formappType?.value?.createType === AppCreateType.SAML">
|
||||||
|
<div class="saml">
|
||||||
|
<cnsl-form-field class="formfield">
|
||||||
|
<cnsl-label>{{ 'APP.SAML.URL' | translate }}</cnsl-label>
|
||||||
|
<input cnslInput formControlName="metadataUrl" placeholder="https://" />
|
||||||
|
</cnsl-form-field>
|
||||||
|
|
||||||
|
<span class="cnsl-app-or cnsl-secondary-text">{{ 'APP.SAML.OR' | translate }}</span>
|
||||||
|
|
||||||
|
<input
|
||||||
|
#xmlFileInput
|
||||||
|
style="display: none"
|
||||||
|
class="file-input"
|
||||||
|
type="file"
|
||||||
|
(change)="onDropXML($any($event.target).files)"
|
||||||
|
/>
|
||||||
|
<button type="button" mat-stroked-button (click)="$event.preventDefault(); xmlFileInput.click()">
|
||||||
|
{{ 'APP.SAML.XML' | translate }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="saml-xml"
|
||||||
|
[ngClass]="{ disabled: !!formMetadataUrl?.value }"
|
||||||
|
*ngIf="decodedBase64 && samlAppRequest.toObject().metadataXml && !samlAppRequest.toObject().metadataUrl"
|
||||||
|
>
|
||||||
|
<ngx-codemirror
|
||||||
|
[(ngModel)]="decodedBase64"
|
||||||
|
[options]="{
|
||||||
|
lineNumbers: true,
|
||||||
|
theme: 'material',
|
||||||
|
mode: 'application/xml'
|
||||||
|
}"
|
||||||
|
></ngx-codemirror>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content" *ngIf="formappType?.value?.createType === AppCreateType.OIDC">
|
<div class="content" *ngIf="formappType?.value?.createType === AppCreateType.OIDC">
|
||||||
@ -294,22 +431,22 @@
|
|||||||
<cnsl-redirect-uris
|
<cnsl-redirect-uris
|
||||||
class="redirect-section"
|
class="redirect-section"
|
||||||
[canWrite]="true"
|
[canWrite]="true"
|
||||||
(changedUris)="oidcAppRequest.redirectUrisList = $any($event)"
|
(changedUris)="oidcAppRequest.setRedirectUrisList($any($event))"
|
||||||
[urisList]="oidcAppRequest.redirectUrisList"
|
[urisList]="oidcAppRequest.toObject().redirectUrisList"
|
||||||
title="{{ 'APP.OIDC.REDIRECT' | translate }}"
|
title="{{ 'APP.OIDC.REDIRECT' | translate }}"
|
||||||
[getValues]="requestRedirectValuesSubject$"
|
[getValues]="requestRedirectValuesSubject$"
|
||||||
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
[isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
||||||
>
|
>
|
||||||
</cnsl-redirect-uris>
|
</cnsl-redirect-uris>
|
||||||
|
|
||||||
<cnsl-redirect-uris
|
<cnsl-redirect-uris
|
||||||
class="redirect-section"
|
class="redirect-section"
|
||||||
[canWrite]="true"
|
[canWrite]="true"
|
||||||
(changedUris)="oidcAppRequest.postLogoutRedirectUrisList = $any($event)"
|
(changedUris)="oidcAppRequest.setPostLogoutRedirectUrisList($any($event))"
|
||||||
[urisList]="oidcAppRequest.postLogoutRedirectUrisList"
|
[urisList]="oidcAppRequest.toObject().postLogoutRedirectUrisList"
|
||||||
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
|
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
|
||||||
[getValues]="requestRedirectValuesSubject$"
|
[getValues]="requestRedirectValuesSubject$"
|
||||||
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
[isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
||||||
>
|
>
|
||||||
</cnsl-redirect-uris>
|
</cnsl-redirect-uris>
|
||||||
</div>
|
</div>
|
||||||
|
@ -61,6 +61,10 @@ p.desc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cnsl-app-or {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -72,10 +76,20 @@ p.desc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.saml-xml {
|
||||||
|
margin-top: 2rem;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.app-create-actions {
|
.app-create-actions {
|
||||||
margin-top: 1rem;
|
margin-top: 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
.bck-button {
|
.bck-button {
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
@ -102,6 +116,12 @@ p.desc {
|
|||||||
flex-basis: 80%;
|
flex-basis: 80%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.saml {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.continue-button {
|
.continue-button {
|
||||||
|
@ -5,6 +5,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
|
|||||||
import { AbstractControl, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms';
|
import { AbstractControl, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||||
|
import { Buffer } from 'buffer';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
import { debounceTime, takeUntil } from 'rxjs/operators';
|
import { debounceTime, takeUntil } from 'rxjs/operators';
|
||||||
import { RadioItemAuthType } from 'src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component';
|
import { RadioItemAuthType } from 'src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component';
|
||||||
@ -14,12 +15,14 @@ import {
|
|||||||
OIDCAuthMethodType,
|
OIDCAuthMethodType,
|
||||||
OIDCGrantType,
|
OIDCGrantType,
|
||||||
OIDCResponseType,
|
OIDCResponseType,
|
||||||
|
SAMLConfig,
|
||||||
} from 'src/app/proto/generated/zitadel/app_pb';
|
} from 'src/app/proto/generated/zitadel/app_pb';
|
||||||
import {
|
import {
|
||||||
AddAPIAppRequest,
|
AddAPIAppRequest,
|
||||||
AddAPIAppResponse,
|
AddAPIAppResponse,
|
||||||
AddOIDCAppRequest,
|
AddOIDCAppRequest,
|
||||||
AddOIDCAppResponse,
|
AddOIDCAppResponse,
|
||||||
|
AddSAMLAppRequest,
|
||||||
} from 'src/app/proto/generated/zitadel/management_pb';
|
} from 'src/app/proto/generated/zitadel/management_pb';
|
||||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||||
@ -35,7 +38,9 @@ import {
|
|||||||
PKCE_METHOD,
|
PKCE_METHOD,
|
||||||
POST_METHOD,
|
POST_METHOD,
|
||||||
} from '../authmethods';
|
} from '../authmethods';
|
||||||
import { API_TYPE, AppCreateType, NATIVE_TYPE, RadioItemAppType, USER_AGENT_TYPE, WEB_TYPE } from '../authtypes';
|
import { API_TYPE, AppCreateType, NATIVE_TYPE, RadioItemAppType, SAML_TYPE, USER_AGENT_TYPE, WEB_TYPE } from '../authtypes';
|
||||||
|
|
||||||
|
const MAX_ALLOWED_SIZE = 1 * 1024 * 1024;
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cnsl-app-create',
|
selector: 'cnsl-app-create',
|
||||||
@ -49,11 +54,11 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
|||||||
public projectId: string = '';
|
public projectId: string = '';
|
||||||
public loading: boolean = false;
|
public loading: boolean = false;
|
||||||
|
|
||||||
public createSteps: number = 4;
|
|
||||||
public currentCreateStep: number = 1;
|
public currentCreateStep: number = 1;
|
||||||
|
|
||||||
public oidcAppRequest: AddOIDCAppRequest.AsObject = new AddOIDCAppRequest().toObject();
|
public oidcAppRequest: AddOIDCAppRequest = new AddOIDCAppRequest();
|
||||||
public apiAppRequest: AddAPIAppRequest.AsObject = new AddAPIAppRequest().toObject();
|
public apiAppRequest: AddAPIAppRequest = new AddAPIAppRequest();
|
||||||
|
public samlAppRequest: AddSAMLAppRequest = new AddSAMLAppRequest();
|
||||||
|
|
||||||
public oidcResponseTypes: { type: OIDCResponseType; checked: boolean; disabled: boolean }[] = [
|
public oidcResponseTypes: { type: OIDCResponseType; checked: boolean; disabled: boolean }[] = [
|
||||||
{ type: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE, checked: false, disabled: false },
|
{ type: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE, checked: false, disabled: false },
|
||||||
@ -66,7 +71,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
|||||||
OIDCAppType.OIDC_APP_TYPE_NATIVE,
|
OIDCAppType.OIDC_APP_TYPE_NATIVE,
|
||||||
OIDCAppType.OIDC_APP_TYPE_USER_AGENT,
|
OIDCAppType.OIDC_APP_TYPE_USER_AGENT,
|
||||||
];
|
];
|
||||||
public appTypes: any = [WEB_TYPE, NATIVE_TYPE, USER_AGENT_TYPE, API_TYPE];
|
public appTypes: any = [WEB_TYPE, NATIVE_TYPE, USER_AGENT_TYPE, API_TYPE, SAML_TYPE];
|
||||||
|
|
||||||
public authMethods: RadioItemAuthType[] = [PKCE_METHOD, CODE_METHOD, PK_JWT_METHOD, POST_METHOD];
|
public authMethods: RadioItemAuthType[] = [PKCE_METHOD, CODE_METHOD, PK_JWT_METHOD, POST_METHOD];
|
||||||
|
|
||||||
@ -84,8 +89,12 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// stepper
|
// stepper
|
||||||
firstFormGroup!: UntypedFormGroup;
|
public firstFormGroup!: UntypedFormGroup;
|
||||||
secondFormGroup!: UntypedFormGroup;
|
public secondFormGroup!: UntypedFormGroup;
|
||||||
|
public samlConfigForm!: UntypedFormGroup;
|
||||||
|
|
||||||
|
public redirectUrisList: string[] = [];
|
||||||
|
public postLogoutRedirectUrisList: string[] = [];
|
||||||
|
|
||||||
// devmode
|
// devmode
|
||||||
public form!: UntypedFormGroup;
|
public form!: UntypedFormGroup;
|
||||||
@ -121,10 +130,13 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
|||||||
) {
|
) {
|
||||||
this.form = this.fb.group({
|
this.form = this.fb.group({
|
||||||
name: ['', [Validators.required]],
|
name: ['', [Validators.required]],
|
||||||
responseTypesList: ['', [Validators.required]],
|
|
||||||
grantTypesList: ['', [Validators.required]],
|
|
||||||
appType: ['', [Validators.required]],
|
appType: ['', [Validators.required]],
|
||||||
authMethodType: ['', [Validators.required]],
|
// apptype OIDC
|
||||||
|
responseTypesList: ['', []],
|
||||||
|
grantTypesList: ['', []],
|
||||||
|
authMethodType: ['', []],
|
||||||
|
// apptype SAML
|
||||||
|
metadataUrl: ['', []],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.initForm();
|
this.initForm();
|
||||||
@ -134,25 +146,30 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
|||||||
appType: [WEB_TYPE, [Validators.required]],
|
appType: [WEB_TYPE, [Validators.required]],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.samlConfigForm = this.fb.group({
|
||||||
|
metadataUrl: ['', []],
|
||||||
|
});
|
||||||
|
|
||||||
this.firstFormGroup.valueChanges.subscribe((value) => {
|
this.firstFormGroup.valueChanges.subscribe((value) => {
|
||||||
if (this.firstFormGroup.valid) {
|
if (this.firstFormGroup.valid) {
|
||||||
this.oidcAppRequest.name = this.name?.value;
|
this.oidcAppRequest.setName(this.name?.value);
|
||||||
this.apiAppRequest.name = this.name?.value;
|
this.apiAppRequest.setName(this.name?.value);
|
||||||
|
this.samlAppRequest.setName(this.name?.value);
|
||||||
|
|
||||||
if (this.isStepperOIDC) {
|
if (this.isStepperOIDC) {
|
||||||
const oidcAppType = (this.appType?.value as RadioItemAppType).oidcAppType;
|
const oidcAppType = (this.appType?.value as RadioItemAppType).oidcAppType;
|
||||||
if (oidcAppType !== undefined) {
|
if (oidcAppType !== undefined) {
|
||||||
this.oidcAppRequest.appType = oidcAppType;
|
this.oidcAppRequest.setAppType(oidcAppType);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (this.oidcAppRequest.appType) {
|
switch (this.appType?.value.oidcAppType) {
|
||||||
case OIDCAppType.OIDC_APP_TYPE_NATIVE:
|
case OIDCAppType.OIDC_APP_TYPE_NATIVE:
|
||||||
this.authMethods = [PKCE_METHOD];
|
this.authMethods = [PKCE_METHOD];
|
||||||
|
|
||||||
// automatically set to PKCE and skip step
|
// automatically set to PKCE and skip step
|
||||||
this.oidcAppRequest.responseTypesList = [OIDCResponseType.OIDC_RESPONSE_TYPE_CODE];
|
this.oidcAppRequest.setResponseTypesList([OIDCResponseType.OIDC_RESPONSE_TYPE_CODE]);
|
||||||
this.oidcAppRequest.grantTypesList = [OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE];
|
this.oidcAppRequest.setGrantTypesList([OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE]);
|
||||||
this.oidcAppRequest.authMethodType = OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE;
|
this.oidcAppRequest.setAuthMethodType(OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case OIDCAppType.OIDC_APP_TYPE_WEB:
|
case OIDCAppType.OIDC_APP_TYPE_WEB:
|
||||||
@ -179,19 +196,28 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
|||||||
this.secondFormGroup = this.fb.group({
|
this.secondFormGroup = this.fb.group({
|
||||||
authMethod: [this.authMethods[0].key, [Validators.required]],
|
authMethod: [this.authMethods[0].key, [Validators.required]],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.secondFormGroup.valueChanges.subscribe((form) => {
|
this.secondFormGroup.valueChanges.subscribe((form) => {
|
||||||
const partialConfig = getPartialConfigFromAuthMethod(form.authMethod);
|
const partialConfig = getPartialConfigFromAuthMethod(form.authMethod);
|
||||||
|
|
||||||
if (this.isStepperOIDC && partialConfig && partialConfig.oidc) {
|
if (this.isStepperOIDC && partialConfig && partialConfig.oidc) {
|
||||||
this.oidcAppRequest.responseTypesList = partialConfig.oidc?.responseTypesList ?? [];
|
this.oidcAppRequest.setResponseTypesList(partialConfig.oidc?.responseTypesList ?? []);
|
||||||
|
|
||||||
this.oidcAppRequest.grantTypesList = partialConfig.oidc?.grantTypesList ?? [];
|
this.oidcAppRequest.setGrantTypesList(partialConfig.oidc?.grantTypesList ?? []);
|
||||||
|
|
||||||
this.oidcAppRequest.authMethodType =
|
this.oidcAppRequest.setAuthMethodType(
|
||||||
partialConfig.oidc?.authMethodType ?? OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE;
|
partialConfig.oidc?.authMethodType ?? OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
|
||||||
|
);
|
||||||
} else if (this.isStepperAPI && partialConfig && partialConfig.api) {
|
} else if (this.isStepperAPI && partialConfig && partialConfig.api) {
|
||||||
this.apiAppRequest.authMethodType =
|
this.apiAppRequest.setAuthMethodType(
|
||||||
partialConfig.api?.authMethodType ?? APIAuthMethodType.API_AUTH_METHOD_TYPE_BASIC;
|
partialConfig.api?.authMethodType ?? APIAuthMethodType.API_AUTH_METHOD_TYPE_BASIC,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.samlConfigForm.valueChanges.subscribe((form) => {
|
||||||
|
if (form.metadataUrl && form.metadataUrl.length > 0) {
|
||||||
|
this.samlAppRequest.setMetadataUrl(form.metadataUrl);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -225,18 +251,23 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
public initForm(): void {
|
public initForm(): void {
|
||||||
this.form.valueChanges.pipe(takeUntil(this.destroyed$), debounceTime(150)).subscribe(() => {
|
this.form.valueChanges.pipe(takeUntil(this.destroyed$), debounceTime(150)).subscribe(() => {
|
||||||
this.oidcAppRequest.name = this.formname?.value;
|
this.oidcAppRequest.setName(this.formname?.value);
|
||||||
this.apiAppRequest.name = this.formname?.value;
|
this.apiAppRequest.setName(this.formname?.value);
|
||||||
|
this.samlAppRequest.setName(this.formname?.value);
|
||||||
|
|
||||||
this.oidcAppRequest.responseTypesList = this.formresponseTypesList?.value;
|
this.oidcAppRequest.setResponseTypesList(this.formresponseTypesList?.value);
|
||||||
this.oidcAppRequest.grantTypesList = this.formgrantTypesList?.value;
|
this.oidcAppRequest.setGrantTypesList(this.grantTypesList?.value);
|
||||||
|
|
||||||
this.oidcAppRequest.authMethodType = this.formauthMethodType?.value;
|
this.oidcAppRequest.setAuthMethodType(this.authMethodType?.value);
|
||||||
this.apiAppRequest.authMethodType = this.formauthMethodType?.value;
|
this.apiAppRequest.setAuthMethodType(this.authMethodType?.value);
|
||||||
|
|
||||||
|
if (this.formMetadataUrl?.value) {
|
||||||
|
this.samlAppRequest.setMetadataUrl(this.formMetadataUrl?.value);
|
||||||
|
}
|
||||||
|
|
||||||
const oidcAppType = (this.formappType?.value as RadioItemAppType).oidcAppType;
|
const oidcAppType = (this.formappType?.value as RadioItemAppType).oidcAppType;
|
||||||
if (oidcAppType !== undefined) {
|
if (oidcAppType !== undefined) {
|
||||||
this.oidcAppRequest.appType = oidcAppType;
|
this.oidcAppRequest.setAppType(oidcAppType);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -282,17 +313,41 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private async getData({ projectid }: Params): Promise<void> {
|
private async getData({ projectid }: Params): Promise<void> {
|
||||||
this.projectId = projectid;
|
this.projectId = projectid;
|
||||||
this.oidcAppRequest.projectId = projectid;
|
this.oidcAppRequest.setProjectId(projectid);
|
||||||
this.apiAppRequest.projectId = projectid;
|
this.apiAppRequest.setProjectId(projectid);
|
||||||
|
this.samlAppRequest.setProjectId(projectid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public close(): void {
|
public close(): void {
|
||||||
this._location.back();
|
this._location.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onDropXML(filelist: FileList): void {
|
||||||
|
const file = filelist.item(0);
|
||||||
|
this.metadataUrl?.setValue('');
|
||||||
|
if (file) {
|
||||||
|
if (file.size > MAX_ALLOWED_SIZE) {
|
||||||
|
this.toast.showInfo('POLICY.PRIVATELABELING.MAXSIZEEXCEEDED', true);
|
||||||
|
} else {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = ((aXML) => {
|
||||||
|
return (e) => {
|
||||||
|
const xmlBase64 = e.target?.result;
|
||||||
|
if (xmlBase64 && typeof xmlBase64 === 'string') {
|
||||||
|
const cropped = xmlBase64.replace('data:text/xml;base64,', '');
|
||||||
|
this.samlAppRequest.setMetadataXml(cropped);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})(file);
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public createApp(): void {
|
public createApp(): void {
|
||||||
const appOIDCCheck = this.devmode ? this.isDevOIDC : this.isStepperOIDC;
|
const appOIDCCheck = this.devmode ? this.isDevOIDC : this.isStepperOIDC;
|
||||||
const appAPICheck = this.devmode ? this.isDevAPI : this.isStepperAPI;
|
const appAPICheck = this.devmode ? this.isDevAPI : this.isStepperAPI;
|
||||||
|
const appSAMLCheck = this.devmode ? this.isDevSAML : this.isStepperSAML;
|
||||||
|
|
||||||
if (appOIDCCheck) {
|
if (appOIDCCheck) {
|
||||||
this.requestRedirectValuesSubject$.next();
|
this.requestRedirectValuesSubject$.next();
|
||||||
@ -331,6 +386,19 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
|||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.toast.showError(error);
|
this.toast.showError(error);
|
||||||
});
|
});
|
||||||
|
} else if (appSAMLCheck) {
|
||||||
|
this.loading = true;
|
||||||
|
this.toast.showInfo('APP.TOAST.CREATED', true);
|
||||||
|
this.mgmtService
|
||||||
|
.addSAMLApp(this.samlAppRequest)
|
||||||
|
.then((resp) => {
|
||||||
|
this.loading = false;
|
||||||
|
this.router.navigate(['projects', this.projectId, 'apps', resp.appId]);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.loading = false;
|
||||||
|
this.toast.showError(error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -381,19 +449,27 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
|||||||
get formname(): AbstractControl | null {
|
get formname(): AbstractControl | null {
|
||||||
return this.form.get('name');
|
return this.form.get('name');
|
||||||
}
|
}
|
||||||
|
|
||||||
get formresponseTypesList(): AbstractControl | null {
|
get formresponseTypesList(): AbstractControl | null {
|
||||||
return this.form.get('responseTypesList');
|
return this.form.get('responseTypesList');
|
||||||
}
|
}
|
||||||
get formgrantTypesList(): AbstractControl | null {
|
|
||||||
|
get grantTypesList(): AbstractControl | null {
|
||||||
return this.form.get('grantTypesList');
|
return this.form.get('grantTypesList');
|
||||||
}
|
}
|
||||||
|
|
||||||
get formappType(): AbstractControl | null {
|
get formappType(): AbstractControl | null {
|
||||||
return this.form.get('appType');
|
return this.form.get('appType');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get formMetadataUrl(): AbstractControl | null {
|
||||||
|
return this.form.get('metadataUrl');
|
||||||
|
}
|
||||||
// get formapplicationType(): AbstractControl | null {
|
// get formapplicationType(): AbstractControl | null {
|
||||||
// return this.form.get('applicationType');
|
// return this.form.get('applicationType');
|
||||||
// }
|
// }
|
||||||
get formauthMethodType(): AbstractControl | null {
|
|
||||||
|
get authMethodType(): AbstractControl | null {
|
||||||
return this.form.get('authMethodType');
|
return this.form.get('authMethodType');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -409,7 +485,35 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
|||||||
return (this.formappType?.value as RadioItemAppType).createType === AppCreateType.API;
|
return (this.formappType?.value as RadioItemAppType).createType === AppCreateType.API;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isDevSAML(): boolean {
|
||||||
|
return (this.formappType?.value as RadioItemAppType).createType === AppCreateType.SAML;
|
||||||
|
}
|
||||||
|
|
||||||
get isStepperAPI(): boolean {
|
get isStepperAPI(): boolean {
|
||||||
return (this.appType?.value as RadioItemAppType).createType === AppCreateType.API;
|
return (this.appType?.value as RadioItemAppType).createType === AppCreateType.API;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isStepperSAML(): boolean {
|
||||||
|
return (this.appType?.value as RadioItemAppType).createType === AppCreateType.SAML;
|
||||||
|
}
|
||||||
|
|
||||||
|
get decodedBase64(): string {
|
||||||
|
const samlReq = this.samlAppRequest.toObject();
|
||||||
|
if (samlReq && samlReq.metadataXml && typeof samlReq.metadataXml === 'string') {
|
||||||
|
return Buffer.from(samlReq.metadataXml, 'base64').toString('ascii');
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set decodedBase64(xmlString) {
|
||||||
|
if (this.samlAppRequest) {
|
||||||
|
const base64 = Buffer.from(xmlString, 'ascii').toString('base64');
|
||||||
|
this.samlAppRequest.setMetadataXml(base64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public get metadataUrl(): AbstractControl | null {
|
||||||
|
return this.samlConfigForm.get('metadataUrl');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
title="{{ app?.name }}"
|
title="{{ app?.name }}"
|
||||||
[hasActions]="isZitadel === false && (['project.app.write:' + projectId, 'project.app.write'] | hasRole | async)"
|
[hasActions]="isZitadel === false && (['project.app.write:' + projectId, 'project.app.write'] | hasRole | async)"
|
||||||
docLink="https://docs.zitadel.com/docs/guides/basics/projects"
|
docLink="https://docs.zitadel.com/docs/guides/basics/projects"
|
||||||
[sub]="app?.oidcConfig ? ('APP.OIDC.APPTYPE.' + app?.oidcConfig?.appType | translate) : 'API'"
|
[sub]="app?.oidcConfig ? ('APP.OIDC.APPTYPE.' + app?.oidcConfig?.appType | translate) : app?.apiConfig ? 'API' : 'SAML'"
|
||||||
[isActive]="app?.state === AppState.APP_STATE_ACTIVE"
|
[isActive]="app?.state === AppState.APP_STATE_ACTIVE"
|
||||||
[isInactive]="app?.state === AppState.APP_STATE_INACTIVE"
|
[isInactive]="app?.state === AppState.APP_STATE_INACTIVE"
|
||||||
stateTooltip="{{ 'APP.PAGES.DETAIL.STATE.' + app?.state | translate }}"
|
stateTooltip="{{ 'APP.PAGES.DETAIL.STATE.' + app?.state | translate }}"
|
||||||
@ -156,6 +156,60 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</cnsl-card>
|
</cnsl-card>
|
||||||
|
|
||||||
|
<cnsl-card *ngIf="samlForm && app?.samlConfig">
|
||||||
|
<form [formGroup]="samlForm">
|
||||||
|
<cnsl-form-field class="formfield">
|
||||||
|
<cnsl-label>{{ 'APP.SAML.URL' | translate }}</cnsl-label>
|
||||||
|
<input cnslInput formControlName="metadataUrl" placeholder="https://" />
|
||||||
|
</cnsl-form-field>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="cnsl-saml-config-line">
|
||||||
|
<span class="cnsl-app-or cnsl-secondary-text">{{ 'APP.SAML.OR' | translate }}</span>
|
||||||
|
|
||||||
|
<input
|
||||||
|
#xmlFileInput
|
||||||
|
style="display: none"
|
||||||
|
class="file-input"
|
||||||
|
type="file"
|
||||||
|
accept="text/xml,application/xml"
|
||||||
|
(change)="onDropXML($any($event.target).files)"
|
||||||
|
/>
|
||||||
|
<button mat-stroked-button (click)="$event.preventDefault(); xmlFileInput.click()">
|
||||||
|
{{ 'APP.SAML.XML' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="saml-xml"
|
||||||
|
[ngClass]="{ disabled: !!metadataUrl?.value }"
|
||||||
|
*ngIf="decodedBase64 && app && app.samlConfig && !app.samlConfig.metadataUrl"
|
||||||
|
>
|
||||||
|
<ngx-codemirror
|
||||||
|
[(ngModel)]="decodedBase64"
|
||||||
|
[disabled]="!!metadataUrl?.value"
|
||||||
|
[options]="{
|
||||||
|
lineNumbers: true,
|
||||||
|
theme: 'material',
|
||||||
|
mode: 'application/xml'
|
||||||
|
}"
|
||||||
|
></ngx-codemirror>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-container">
|
||||||
|
<button
|
||||||
|
class="submit-button"
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
(click)="saveSAMLApp()"
|
||||||
|
[disabled]="samlForm.invalid || !canWrite"
|
||||||
|
mat-raised-button
|
||||||
|
>
|
||||||
|
{{ 'ACTIONS.SAVE' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</cnsl-card>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="currentSetting === 'token'">
|
<ng-container *ngIf="currentSetting === 'token'">
|
||||||
|
@ -158,6 +158,24 @@
|
|||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cnsl-saml-config-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.cnsl-app-or {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.saml-xml {
|
||||||
|
margin-top: 2rem;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import { MatCheckboxChange } from '@angular/material/checkbox';
|
|||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { Buffer } from 'buffer';
|
||||||
import { Duration } from 'google-protobuf/google/protobuf/duration_pb';
|
import { Duration } from 'google-protobuf/google/protobuf/duration_pb';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
@ -27,11 +28,13 @@ import {
|
|||||||
OIDCGrantType,
|
OIDCGrantType,
|
||||||
OIDCResponseType,
|
OIDCResponseType,
|
||||||
OIDCTokenType,
|
OIDCTokenType,
|
||||||
|
SAMLConfig,
|
||||||
} from 'src/app/proto/generated/zitadel/app_pb';
|
} from 'src/app/proto/generated/zitadel/app_pb';
|
||||||
import {
|
import {
|
||||||
GetOIDCInformationResponse,
|
GetOIDCInformationResponse,
|
||||||
UpdateAPIAppConfigRequest,
|
UpdateAPIAppConfigRequest,
|
||||||
UpdateOIDCAppConfigRequest,
|
UpdateOIDCAppConfigRequest,
|
||||||
|
UpdateSAMLAppConfigRequest,
|
||||||
} from 'src/app/proto/generated/zitadel/management_pb';
|
} from 'src/app/proto/generated/zitadel/management_pb';
|
||||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||||
@ -52,6 +55,8 @@ import {
|
|||||||
} from '../authmethods';
|
} from '../authmethods';
|
||||||
import { AuthMethodDialogComponent } from './auth-method-dialog/auth-method-dialog.component';
|
import { AuthMethodDialogComponent } from './auth-method-dialog/auth-method-dialog.component';
|
||||||
|
|
||||||
|
const MAX_ALLOWED_SIZE = 1 * 1024 * 1024;
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cnsl-app-detail',
|
selector: 'cnsl-app-detail',
|
||||||
templateUrl: './app-detail.component.html',
|
templateUrl: './app-detail.component.html',
|
||||||
@ -104,6 +109,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
|||||||
public oidcForm!: UntypedFormGroup;
|
public oidcForm!: UntypedFormGroup;
|
||||||
public oidcTokenForm!: UntypedFormGroup;
|
public oidcTokenForm!: UntypedFormGroup;
|
||||||
public apiForm!: UntypedFormGroup;
|
public apiForm!: UntypedFormGroup;
|
||||||
|
public samlForm!: UntypedFormGroup;
|
||||||
|
|
||||||
public redirectUrisList: string[] = [];
|
public redirectUrisList: string[] = [];
|
||||||
public postLogoutRedirectUrisList: string[] = [];
|
public postLogoutRedirectUrisList: string[] = [];
|
||||||
@ -162,6 +168,11 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
|||||||
authMethodType: [{ value: '', disabled: true }],
|
authMethodType: [{ value: '', disabled: true }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.samlForm = this.fb.group({
|
||||||
|
metadataUrl: [{ value: '', disabled: true }],
|
||||||
|
metadataXml: [{ value: '', disabled: true }],
|
||||||
|
});
|
||||||
|
|
||||||
this.http.get('./assets/environment.json').subscribe((env: any) => {
|
this.http.get('./assets/environment.json').subscribe((env: any) => {
|
||||||
this.environmentMap = {
|
this.environmentMap = {
|
||||||
issuer: env.issuer,
|
issuer: env.issuer,
|
||||||
@ -290,12 +301,15 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
|||||||
} else {
|
} else {
|
||||||
this.authMethods = this.authMethods.filter((element) => element !== CUSTOM_METHOD);
|
this.authMethods = this.authMethods.filter((element) => element !== CUSTOM_METHOD);
|
||||||
}
|
}
|
||||||
|
} else if (this.app.samlConfig) {
|
||||||
|
this.settingsList = [{ id: 'configuration', i18nKey: 'APP.CONFIGURATION' }];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowed) {
|
if (allowed) {
|
||||||
this.oidcForm.enable();
|
this.oidcForm.enable();
|
||||||
this.oidcTokenForm.enable();
|
this.oidcTokenForm.enable();
|
||||||
this.apiForm.enable();
|
this.apiForm.enable();
|
||||||
|
this.samlForm.enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.app.oidcConfig?.redirectUrisList) {
|
if (this.app.oidcConfig?.redirectUrisList) {
|
||||||
@ -370,6 +384,30 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onDropXML(filelist: FileList): void {
|
||||||
|
const file = filelist.item(0);
|
||||||
|
if (file) {
|
||||||
|
if (file.size > MAX_ALLOWED_SIZE) {
|
||||||
|
this.toast.showInfo('POLICY.PRIVATELABELING.MAXSIZEEXCEEDED', true);
|
||||||
|
} else {
|
||||||
|
this.metadataUrl?.setValue('');
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = ((aXML) => {
|
||||||
|
return (e) => {
|
||||||
|
const xmlBase64 = e.target?.result;
|
||||||
|
if (xmlBase64 && typeof xmlBase64 === 'string' && this.app?.samlConfig) {
|
||||||
|
const samlConfig = new SAMLConfig();
|
||||||
|
const cropped = xmlBase64.replace('data:text/xml;base64,', '');
|
||||||
|
samlConfig.setMetadataXml(cropped);
|
||||||
|
this.app.samlConfig.metadataXml = cropped;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})(file);
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public authMethodFromPartialConfig(config: { oidc?: OIDCConfig.AsObject; api?: APIConfig.AsObject }): string {
|
public authMethodFromPartialConfig(config: { oidc?: OIDCConfig.AsObject; api?: APIConfig.AsObject }): string {
|
||||||
const key = getAuthMethodFromPartialConfig(config);
|
const key = getAuthMethodFromPartialConfig(config);
|
||||||
return key;
|
return key;
|
||||||
@ -581,6 +619,28 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public saveSAMLApp(): void {
|
||||||
|
if (this.samlForm.valid && this.app?.samlConfig) {
|
||||||
|
const req = new UpdateSAMLAppConfigRequest();
|
||||||
|
req.setProjectId(this.projectId);
|
||||||
|
req.setAppId(this.app.id);
|
||||||
|
|
||||||
|
if (this.app.samlConfig) {
|
||||||
|
req.setMetadataUrl(this.app.samlConfig?.metadataUrl);
|
||||||
|
req.setMetadataXml(this.app.samlConfig?.metadataXml);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mgmtService
|
||||||
|
.updateSAMLAppConfig(req)
|
||||||
|
.then(() => {
|
||||||
|
this.toast.showInfo('APP.TOAST.APIUPDATED', true);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.toast.showError(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public regenerateOIDCClientSecret(): void {
|
public regenerateOIDCClientSecret(): void {
|
||||||
if (this.app) {
|
if (this.app) {
|
||||||
this.mgmtService
|
this.mgmtService
|
||||||
@ -693,4 +753,31 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
|||||||
public get clockSkewSeconds(): AbstractControl | null {
|
public get clockSkewSeconds(): AbstractControl | null {
|
||||||
return this.oidcTokenForm.get('clockSkewSeconds');
|
return this.oidcTokenForm.get('clockSkewSeconds');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get metadataUrl(): AbstractControl | null {
|
||||||
|
return this.samlForm.get('metadataUrl');
|
||||||
|
}
|
||||||
|
|
||||||
|
get decodedBase64(): string {
|
||||||
|
if (
|
||||||
|
this.app &&
|
||||||
|
this.app.samlConfig &&
|
||||||
|
this.app.samlConfig.metadataXml &&
|
||||||
|
typeof this.app.samlConfig.metadataXml === 'string'
|
||||||
|
) {
|
||||||
|
return Buffer.from(this.app?.samlConfig.metadataXml, 'base64').toString('ascii');
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set decodedBase64(xmlString: string) {
|
||||||
|
if (this.app && this.app.samlConfig && this.app.samlConfig.metadataXml) {
|
||||||
|
const base64 = Buffer.from(xmlString, 'ascii').toString('base64');
|
||||||
|
|
||||||
|
if (this.app.samlConfig) {
|
||||||
|
this.app.samlConfig.metadataXml = base64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
|||||||
import { MatSliderModule } from '@angular/material/slider';
|
import { MatSliderModule } from '@angular/material/slider';
|
||||||
import { MatStepperModule } from '@angular/material/stepper';
|
import { MatStepperModule } from '@angular/material/stepper';
|
||||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
import { CodemirrorModule } from '@ctrl/ngx-codemirror';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module';
|
import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module';
|
||||||
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
|
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
|
||||||
@ -89,6 +90,7 @@ import { RedirectUrisComponent } from './redirect-uris/redirect-uris.component';
|
|||||||
InputModule,
|
InputModule,
|
||||||
MetaLayoutModule,
|
MetaLayoutModule,
|
||||||
MatSliderModule,
|
MatSliderModule,
|
||||||
|
CodemirrorModule,
|
||||||
ChangesModule,
|
ChangesModule,
|
||||||
InfoSectionModule,
|
InfoSectionModule,
|
||||||
],
|
],
|
||||||
|
@ -10,6 +10,7 @@ import { OIDCAppType } from 'src/app/proto/generated/zitadel/app_pb';
|
|||||||
export enum AppCreateType {
|
export enum AppCreateType {
|
||||||
API = 'API',
|
API = 'API',
|
||||||
OIDC = 'OIDC',
|
OIDC = 'OIDC',
|
||||||
|
SAML = 'SAML',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RadioItemAppType {
|
export interface RadioItemAppType {
|
||||||
@ -20,6 +21,7 @@ export interface RadioItemAppType {
|
|||||||
descI18nKey: string;
|
descI18nKey: string;
|
||||||
prefix: string;
|
prefix: string;
|
||||||
background: string;
|
background: string;
|
||||||
|
protocol: 'OIDC' | 'SAML';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WEB_TYPE: RadioItemAppType = {
|
export const WEB_TYPE: RadioItemAppType = {
|
||||||
@ -30,6 +32,7 @@ export const WEB_TYPE: RadioItemAppType = {
|
|||||||
oidcAppType: OIDCAppType.OIDC_APP_TYPE_WEB,
|
oidcAppType: OIDCAppType.OIDC_APP_TYPE_WEB,
|
||||||
prefix: 'WEB',
|
prefix: 'WEB',
|
||||||
background: 'linear-gradient(40deg, #059669 30%, #047857)',
|
background: 'linear-gradient(40deg, #059669 30%, #047857)',
|
||||||
|
protocol: 'OIDC',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const USER_AGENT_TYPE: RadioItemAppType = {
|
export const USER_AGENT_TYPE: RadioItemAppType = {
|
||||||
@ -40,6 +43,7 @@ export const USER_AGENT_TYPE: RadioItemAppType = {
|
|||||||
oidcAppType: OIDCAppType.OIDC_APP_TYPE_USER_AGENT,
|
oidcAppType: OIDCAppType.OIDC_APP_TYPE_USER_AGENT,
|
||||||
prefix: 'UA',
|
prefix: 'UA',
|
||||||
background: 'linear-gradient(40deg, #dc2626 30%, #db2777)',
|
background: 'linear-gradient(40deg, #dc2626 30%, #db2777)',
|
||||||
|
protocol: 'OIDC',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NATIVE_TYPE: RadioItemAppType = {
|
export const NATIVE_TYPE: RadioItemAppType = {
|
||||||
@ -50,6 +54,7 @@ export const NATIVE_TYPE: RadioItemAppType = {
|
|||||||
oidcAppType: OIDCAppType.OIDC_APP_TYPE_NATIVE,
|
oidcAppType: OIDCAppType.OIDC_APP_TYPE_NATIVE,
|
||||||
prefix: 'N',
|
prefix: 'N',
|
||||||
background: 'linear-gradient(40deg, #306ccc 30%, #4f46e5)',
|
background: 'linear-gradient(40deg, #306ccc 30%, #4f46e5)',
|
||||||
|
protocol: 'OIDC',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const API_TYPE: RadioItemAppType = {
|
export const API_TYPE: RadioItemAppType = {
|
||||||
@ -59,4 +64,14 @@ export const API_TYPE: RadioItemAppType = {
|
|||||||
createType: AppCreateType.API,
|
createType: AppCreateType.API,
|
||||||
prefix: 'API',
|
prefix: 'API',
|
||||||
background: 'linear-gradient(40deg, #1f2937, #111827)',
|
background: 'linear-gradient(40deg, #1f2937, #111827)',
|
||||||
|
protocol: 'OIDC',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SAML_TYPE: RadioItemAppType = {
|
||||||
|
titleI18nKey: 'APP.SAML.SELECTION.TITLE',
|
||||||
|
descI18nKey: 'APP.SAML.SELECTION.DESCRIPTION',
|
||||||
|
createType: AppCreateType.SAML,
|
||||||
|
prefix: 'SAML',
|
||||||
|
background: 'linear-gradient(40deg,rgb(110, 56, 124), rgb(88, 37, 103))',
|
||||||
|
protocol: 'SAML',
|
||||||
};
|
};
|
||||||
|
@ -17,20 +17,27 @@
|
|||||||
*ngFor="let app of appsSubject | async"
|
*ngFor="let app of appsSubject | async"
|
||||||
matTooltip="{{ 'ACTIONS.EDIT' | translate }}"
|
matTooltip="{{ 'ACTIONS.EDIT' | translate }}"
|
||||||
>
|
>
|
||||||
<cnsl-app-card class="grid-card" matRipple [type]="app.oidcConfig?.appType" [isApiApp]="app.apiConfig !== undefined">
|
<cnsl-app-card
|
||||||
|
class="grid-card"
|
||||||
|
matRipple
|
||||||
|
[type]="app.samlConfig ? 'SAML' : app.oidcConfig?.appType"
|
||||||
|
[isApiApp]="app.apiConfig !== undefined"
|
||||||
|
>
|
||||||
{{ app.name.charAt(0) }}
|
{{ app.name.charAt(0) }}
|
||||||
<ng-container *ngIf="app.oidcConfig?.appType !== undefined">
|
<ng-container *ngIf="app.oidcConfig?.appType !== undefined">
|
||||||
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE" class="las la-mobile"></i>
|
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE" class="lab la-openid"></i>
|
||||||
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_WEB" class="las la-code"></i>
|
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_WEB" class="lab la-openid"></i>
|
||||||
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_USER_AGENT" class="las la-code"></i>
|
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_USER_AGENT" class="lab la-openid"></i>
|
||||||
<i *ngIf="app.apiConfig" class="las la-robot"></i>
|
<i *ngIf="app.apiConfig" class="las la-robot"></i>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<span *ngIf="app.samlConfig" class="samlspan">SAML</span>
|
||||||
</cnsl-app-card>
|
</cnsl-app-card>
|
||||||
<span class="name">{{ app.name }}</span>
|
<span class="name">{{ app.name }}</span>
|
||||||
<span *ngIf="app.oidcConfig?.appType !== undefined && app.oidcConfig?.appType !== null" class="type">
|
<span *ngIf="app.oidcConfig?.appType !== undefined && app.oidcConfig?.appType !== null" class="type">
|
||||||
{{ 'APP.OIDC.APPTYPE.' + app.oidcConfig?.appType | translate }}</span
|
{{ 'APP.OIDC.APPTYPE.' + app.oidcConfig?.appType | translate }}</span
|
||||||
>
|
>
|
||||||
<span *ngIf="app.apiConfig !== undefined" class="type"> API</span>
|
<span *ngIf="app.apiConfig !== undefined" class="type"> API</span>
|
||||||
|
<span *ngIf="app.samlConfig !== undefined" class="type"> SAML</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-template cnslHasRole [hasRole]="['project.app.write']">
|
<ng-template cnslHasRole [hasRole]="['project.app.write']">
|
||||||
|
@ -49,6 +49,17 @@
|
|||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.samlspan {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
line-height: 1.2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
|
@ -63,6 +63,8 @@ import {
|
|||||||
AddProjectResponse,
|
AddProjectResponse,
|
||||||
AddProjectRoleRequest,
|
AddProjectRoleRequest,
|
||||||
AddProjectRoleResponse,
|
AddProjectRoleResponse,
|
||||||
|
AddSAMLAppRequest,
|
||||||
|
AddSAMLAppResponse,
|
||||||
AddSecondFactorToLoginPolicyRequest,
|
AddSecondFactorToLoginPolicyRequest,
|
||||||
AddSecondFactorToLoginPolicyResponse,
|
AddSecondFactorToLoginPolicyResponse,
|
||||||
AddUserGrantRequest,
|
AddUserGrantRequest,
|
||||||
@ -423,6 +425,8 @@ import {
|
|||||||
UpdateProjectResponse,
|
UpdateProjectResponse,
|
||||||
UpdateProjectRoleRequest,
|
UpdateProjectRoleRequest,
|
||||||
UpdateProjectRoleResponse,
|
UpdateProjectRoleResponse,
|
||||||
|
UpdateSAMLAppConfigRequest,
|
||||||
|
UpdateSAMLAppConfigResponse,
|
||||||
UpdateUserGrantRequest,
|
UpdateUserGrantRequest,
|
||||||
UpdateUserGrantResponse,
|
UpdateUserGrantResponse,
|
||||||
UpdateUserNameRequest,
|
UpdateUserNameRequest,
|
||||||
@ -2268,27 +2272,18 @@ export class ManagementService {
|
|||||||
return this.grpcService.mgmt.reactivateProjectGrant(req, null).then((resp) => resp.toObject());
|
return this.grpcService.mgmt.reactivateProjectGrant(req, null).then((resp) => resp.toObject());
|
||||||
}
|
}
|
||||||
|
|
||||||
public addOIDCApp(app: AddOIDCAppRequest.AsObject): Promise<AddOIDCAppResponse.AsObject> {
|
public addOIDCApp(req: AddOIDCAppRequest): Promise<AddOIDCAppResponse.AsObject> {
|
||||||
const req: AddOIDCAppRequest = new AddOIDCAppRequest();
|
|
||||||
req.setAuthMethodType(app.authMethodType);
|
|
||||||
req.setName(app.name);
|
|
||||||
req.setProjectId(app.projectId);
|
|
||||||
req.setResponseTypesList(app.responseTypesList);
|
|
||||||
req.setGrantTypesList(app.grantTypesList);
|
|
||||||
req.setAppType(app.appType);
|
|
||||||
req.setPostLogoutRedirectUrisList(app.postLogoutRedirectUrisList);
|
|
||||||
req.setRedirectUrisList(app.redirectUrisList);
|
|
||||||
return this.grpcService.mgmt.addOIDCApp(req, null).then((resp) => resp.toObject());
|
return this.grpcService.mgmt.addOIDCApp(req, null).then((resp) => resp.toObject());
|
||||||
}
|
}
|
||||||
|
|
||||||
public addAPIApp(app: AddAPIAppRequest.AsObject): Promise<AddAPIAppResponse.AsObject> {
|
public addAPIApp(req: AddAPIAppRequest): Promise<AddAPIAppResponse.AsObject> {
|
||||||
const req: AddAPIAppRequest = new AddAPIAppRequest();
|
|
||||||
req.setAuthMethodType(app.authMethodType);
|
|
||||||
req.setName(app.name);
|
|
||||||
req.setProjectId(app.projectId);
|
|
||||||
return this.grpcService.mgmt.addAPIApp(req, null).then((resp) => resp.toObject());
|
return this.grpcService.mgmt.addAPIApp(req, null).then((resp) => resp.toObject());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addSAMLApp(req: AddSAMLAppRequest): Promise<AddSAMLAppResponse.AsObject> {
|
||||||
|
return this.grpcService.mgmt.addSAMLApp(req, null).then((resp) => resp.toObject());
|
||||||
|
}
|
||||||
|
|
||||||
public regenerateAPIClientSecret(appId: string, projectId: string): Promise<RegenerateAPIClientSecretResponse.AsObject> {
|
public regenerateAPIClientSecret(appId: string, projectId: string): Promise<RegenerateAPIClientSecretResponse.AsObject> {
|
||||||
const req = new RegenerateAPIClientSecretRequest();
|
const req = new RegenerateAPIClientSecretRequest();
|
||||||
req.setAppId(appId);
|
req.setAppId(appId);
|
||||||
@ -2312,6 +2307,10 @@ export class ManagementService {
|
|||||||
return this.grpcService.mgmt.updateAPIAppConfig(req, null).then((resp) => resp.toObject());
|
return this.grpcService.mgmt.updateAPIAppConfig(req, null).then((resp) => resp.toObject());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public updateSAMLAppConfig(req: UpdateSAMLAppConfigRequest): Promise<UpdateSAMLAppConfigResponse.AsObject> {
|
||||||
|
return this.grpcService.mgmt.updateSAMLAppConfig(req, null).then((resp) => resp.toObject());
|
||||||
|
}
|
||||||
|
|
||||||
public removeApp(projectId: string, appId: string): Promise<RemoveAppResponse.AsObject> {
|
public removeApp(projectId: string, appId: string): Promise<RemoveAppResponse.AsObject> {
|
||||||
const req = new RemoveAppRequest();
|
const req = new RemoveAppRequest();
|
||||||
req.setAppId(appId);
|
req.setAppId(appId);
|
||||||
|
@ -1609,9 +1609,9 @@
|
|||||||
"TITLE": "Anwendung",
|
"TITLE": "Anwendung",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"DESCRIPTION": "Hier kannst Du Deine Applikationen bearbeiten und deren Konfiguration anpassen.",
|
"DESCRIPTION": "Hier kannst Du Deine Applikationen bearbeiten und deren Konfiguration anpassen.",
|
||||||
"CREATE_OIDC": "OIDC-Anwendung",
|
"CREATE": "Applikation erstellen",
|
||||||
"CREATE_OIDC_DESC_TITLE": "Gebe die Daten der Anwendung Schritt für Schritt ein.",
|
"CREATE_DESC_TITLE": "Gebe die Daten der Anwendung Schritt für Schritt ein.",
|
||||||
"CREATE_OIDC_DESC_SUB": "Es wird automatisch eine empfohlene Konfiguration generiert.",
|
"CREATE_DESC_SUB": "Es wird automatisch eine empfohlene Konfiguration generiert.",
|
||||||
"STATE": "Status",
|
"STATE": "Status",
|
||||||
"DATECREATED": "Erstellt",
|
"DATECREATED": "Erstellt",
|
||||||
"DATECHANGED": "Geändert",
|
"DATECHANGED": "Geändert",
|
||||||
@ -1664,6 +1664,10 @@
|
|||||||
"ADDITIONALORIGINSDESC": "Wenn sie zusätzliche Origins definieren wollen, die nicht den Redirect URIs gleichzusetzen sind, können Sie dies hier tun.",
|
"ADDITIONALORIGINSDESC": "Wenn sie zusätzliche Origins definieren wollen, die nicht den Redirect URIs gleichzusetzen sind, können Sie dies hier tun.",
|
||||||
"ORIGINS": "Origins",
|
"ORIGINS": "Origins",
|
||||||
"NOTANORIGIN": "Der Angegebene Wert ist kein Origin.",
|
"NOTANORIGIN": "Der Angegebene Wert ist kein Origin.",
|
||||||
|
"PROSWITCH": "Konfigurator überspringen",
|
||||||
|
"NAMEANDTYPESECTION": "Name und Typ",
|
||||||
|
"TITLEFIRST": "Name der Applikation.",
|
||||||
|
"TYPETITLE": "Art der Anwendung",
|
||||||
"OIDC": {
|
"OIDC": {
|
||||||
"INFO": {
|
"INFO": {
|
||||||
"ISSUER": "Issuer",
|
"ISSUER": "Issuer",
|
||||||
@ -1672,10 +1676,6 @@
|
|||||||
"CURRENT": "Aktuelle Konfiguration",
|
"CURRENT": "Aktuelle Konfiguration",
|
||||||
"TOKENSECTIONTITLE": "AuthToken Optionen",
|
"TOKENSECTIONTITLE": "AuthToken Optionen",
|
||||||
"REDIRECTSECTIONTITLE": "Weiterleitungseinstellungen",
|
"REDIRECTSECTIONTITLE": "Weiterleitungseinstellungen",
|
||||||
"PROSWITCH": "Konfigurator überspringen",
|
|
||||||
"NAMEANDTYPESECTION": "Name und Typ",
|
|
||||||
"TITLEFIRST": "Gebe zuerst einen Namen ein.",
|
|
||||||
"TYPETITLE": "Welche Art von Anwendung möchtest Du erstellen?",
|
|
||||||
"REDIRECTTITLE": "Wohin soll nach dem Log-in weitergeleitet werden?",
|
"REDIRECTTITLE": "Wohin soll nach dem Log-in weitergeleitet werden?",
|
||||||
"REDIRECTDESCRIPTIONWEB": "Die Weiterleitung muss mit https:// beginnen. http:// ist nur im Entwicklermodus zulässig.",
|
"REDIRECTDESCRIPTIONWEB": "Die Weiterleitung muss mit https:// beginnen. http:// ist nur im Entwicklermodus zulässig.",
|
||||||
"REDIRECTDESCRIPTIONNATIVE": "Die Weiterleitung muss mit einem eigenen Protokoll, http://127.0.0.1, http://[::1] oder http://localhost beginnen.",
|
"REDIRECTDESCRIPTIONNATIVE": "Die Weiterleitung muss mit einem eigenen Protokoll, http://127.0.0.1, http://[::1] oder http://localhost beginnen.",
|
||||||
@ -1767,6 +1767,18 @@
|
|||||||
"1": "Private Key JWT"
|
"1": "Private Key JWT"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"SAML": {
|
||||||
|
"SELECTION": {
|
||||||
|
"TITLE": "SAML",
|
||||||
|
"DESCRIPTION": "SAML Applikationen"
|
||||||
|
},
|
||||||
|
"CONFIGSECTION": "SAML Konfiguration",
|
||||||
|
"URL": "Url des Metadata Files",
|
||||||
|
"OR": "oder",
|
||||||
|
"XML": "Metadata XML hochladen",
|
||||||
|
"METADATA": "Metadata",
|
||||||
|
"METADATAFROMFILE": "Metadata aus Datei"
|
||||||
|
},
|
||||||
"AUTHMETHODS": {
|
"AUTHMETHODS": {
|
||||||
"CODE": {
|
"CODE": {
|
||||||
"TITLE": "Code",
|
"TITLE": "Code",
|
||||||
|
@ -1609,9 +1609,9 @@
|
|||||||
"TITLE": "Application",
|
"TITLE": "Application",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"DESCRIPTION": "Here you can edit your application data and it's configuration.",
|
"DESCRIPTION": "Here you can edit your application data and it's configuration.",
|
||||||
"CREATE_OIDC": "OIDC Application",
|
"CREATE": "Create application",
|
||||||
"CREATE_OIDC_DESC_TITLE": "Enter Your Application Details Step by Step",
|
"CREATE_DESC_TITLE": "Enter Your Application Details Step by Step",
|
||||||
"CREATE_OIDC_DESC_SUB": "A recommended configuration will be automatically generated.",
|
"CREATE_DESC_SUB": "A recommended configuration will be automatically generated.",
|
||||||
"STATE": "Status",
|
"STATE": "Status",
|
||||||
"DATECREATED": "Created",
|
"DATECREATED": "Created",
|
||||||
"DATECHANGED": "Changed",
|
"DATECHANGED": "Changed",
|
||||||
@ -1664,6 +1664,10 @@
|
|||||||
"ADDITIONALORIGINSDESC": "If you want to add additional Origins to your app which is not used as a redirect you can do that here.",
|
"ADDITIONALORIGINSDESC": "If you want to add additional Origins to your app which is not used as a redirect you can do that here.",
|
||||||
"ORIGINS": "Origins",
|
"ORIGINS": "Origins",
|
||||||
"NOTANORIGIN": "The entered value is not an origin",
|
"NOTANORIGIN": "The entered value is not an origin",
|
||||||
|
"PROSWITCH": "I'm a pro. Skip this wizard.",
|
||||||
|
"NAMEANDTYPESECTION": "Name and Type",
|
||||||
|
"TITLEFIRST": "Name of the application",
|
||||||
|
"TYPETITLE": "Type of application",
|
||||||
"OIDC": {
|
"OIDC": {
|
||||||
"INFO": {
|
"INFO": {
|
||||||
"ISSUER": "Issuer",
|
"ISSUER": "Issuer",
|
||||||
@ -1672,10 +1676,6 @@
|
|||||||
"CURRENT": "Current Config",
|
"CURRENT": "Current Config",
|
||||||
"TOKENSECTIONTITLE": "AuthToken Options",
|
"TOKENSECTIONTITLE": "AuthToken Options",
|
||||||
"REDIRECTSECTIONTITLE": "Redirect Settings",
|
"REDIRECTSECTIONTITLE": "Redirect Settings",
|
||||||
"PROSWITCH": "I'm a pro. Skip this wizard.",
|
|
||||||
"NAMEANDTYPESECTION": "Name and Type",
|
|
||||||
"TITLEFIRST": "Insert a name first.",
|
|
||||||
"TYPETITLE": "What type of application do you want to create?",
|
|
||||||
"REDIRECTTITLE": "Specify the URIs where the login will redirect to.",
|
"REDIRECTTITLE": "Specify the URIs where the login will redirect to.",
|
||||||
"POSTREDIRECTTITLE": "This is the redirect URI after logout.",
|
"POSTREDIRECTTITLE": "This is the redirect URI after logout.",
|
||||||
"REDIRECTDESCRIPTIONWEB": "Redirect URIs must begin with https://. http:// is only valid with enabled development mode.",
|
"REDIRECTDESCRIPTIONWEB": "Redirect URIs must begin with https://. http:// is only valid with enabled development mode.",
|
||||||
@ -1767,6 +1767,18 @@
|
|||||||
"1": "Private Key JWT"
|
"1": "Private Key JWT"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"SAML": {
|
||||||
|
"SELECTION": {
|
||||||
|
"TITLE": "SAML",
|
||||||
|
"DESCRIPTION": "SAML Applications"
|
||||||
|
},
|
||||||
|
"CONFIGSECTION": "SAML Configuration",
|
||||||
|
"URL": "Url where Metadata file is located",
|
||||||
|
"OR": "or",
|
||||||
|
"XML": "Upload Metadata XML",
|
||||||
|
"METADATA": "Metadata",
|
||||||
|
"METADATAFROMFILE": "Metadata from File"
|
||||||
|
},
|
||||||
"AUTHMETHODS": {
|
"AUTHMETHODS": {
|
||||||
"CODE": {
|
"CODE": {
|
||||||
"TITLE": "Code",
|
"TITLE": "Code",
|
||||||
|
@ -1609,9 +1609,9 @@
|
|||||||
"TITLE": "Applicazione",
|
"TITLE": "Applicazione",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"DESCRIPTION": "Qui puoi modificare i dati della tua applicazione e la sua configurazione.",
|
"DESCRIPTION": "Qui puoi modificare i dati della tua applicazione e la sua configurazione.",
|
||||||
"CREATE_OIDC": "Applicazione OIDC",
|
"CREATE": "Crea Applicazione",
|
||||||
"CREATE_OIDC_DESC_TITLE": "Inserisci i dettagli della tua applicazione passo dopo passo",
|
"CREATE_DESC_TITLE": "Inserisci i dettagli della tua applicazione passo dopo passo",
|
||||||
"CREATE_OIDC_DESC_SUB": "Una configurazione raccomandata sar\u00e0 generata automaticamente.",
|
"CREATE_DESC_SUB": "Una configurazione raccomandata sar\u00e0 generata automaticamente.",
|
||||||
"STATE": "Stato",
|
"STATE": "Stato",
|
||||||
"DATECREATED": "Creato",
|
"DATECREATED": "Creato",
|
||||||
"DATECHANGED": "Cambiato",
|
"DATECHANGED": "Cambiato",
|
||||||
@ -1664,6 +1664,10 @@
|
|||||||
"ADDITIONALORIGINSDESC": "Se vuoi aggiungere ulteriori Origini alla tua app che non \u00e8 usata come reindirizzamento puoi farlo qui.",
|
"ADDITIONALORIGINSDESC": "Se vuoi aggiungere ulteriori Origini alla tua app che non \u00e8 usata come reindirizzamento puoi farlo qui.",
|
||||||
"ORIGINS": "Origini",
|
"ORIGINS": "Origini",
|
||||||
"NOTANORIGIN": "Il valore inserito non \u00e8 un'origine",
|
"NOTANORIGIN": "Il valore inserito non \u00e8 un'origine",
|
||||||
|
"PROSWITCH": "Sono un professionista. Salta questo passo.",
|
||||||
|
"NAMEANDTYPESECTION": "Nome e tipo",
|
||||||
|
"TITLEFIRST": "Nome dell' applicazione",
|
||||||
|
"TYPETITLE": "Che tipo di applicazione vuoi creare?",
|
||||||
"OIDC": {
|
"OIDC": {
|
||||||
"INFO": {
|
"INFO": {
|
||||||
"ISSUER": "Issuer",
|
"ISSUER": "Issuer",
|
||||||
@ -1672,10 +1676,6 @@
|
|||||||
"CURRENT": "Configurazione attuale",
|
"CURRENT": "Configurazione attuale",
|
||||||
"TOKENSECTIONTITLE": "Opzioni AuthToken",
|
"TOKENSECTIONTITLE": "Opzioni AuthToken",
|
||||||
"REDIRECTSECTIONTITLE": "Impostazioni di reindirizzamento",
|
"REDIRECTSECTIONTITLE": "Impostazioni di reindirizzamento",
|
||||||
"PROSWITCH": "Sono un professionista. Salta questo passo.",
|
|
||||||
"NAMEANDTYPESECTION": "Nome e tipo",
|
|
||||||
"TITLEFIRST": "Inserisci un nome.",
|
|
||||||
"TYPETITLE": "Che tipo di applicazione vuoi creare?",
|
|
||||||
"REDIRECTTITLE": "Specifica gli URI a cui il login sar\u00e0 reindirizzato.",
|
"REDIRECTTITLE": "Specifica gli URI a cui il login sar\u00e0 reindirizzato.",
|
||||||
"POSTREDIRECTTITLE": "Questo \u00e8 l'URI di reindirizzamento dopo il logout.",
|
"POSTREDIRECTTITLE": "Questo \u00e8 l'URI di reindirizzamento dopo il logout.",
|
||||||
"REDIRECTDESCRIPTIONWEB": "Gli URI di reindirizzamento devono iniziare con https://. http:// \u00e8 valido solo con la modalit\u00e0 di sviluppo abilitata (DEV Mode).",
|
"REDIRECTDESCRIPTIONWEB": "Gli URI di reindirizzamento devono iniziare con https://. http:// \u00e8 valido solo con la modalit\u00e0 di sviluppo abilitata (DEV Mode).",
|
||||||
@ -1767,6 +1767,18 @@
|
|||||||
"1": "Private Key JWT"
|
"1": "Private Key JWT"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"SAML": {
|
||||||
|
"SELECTION": {
|
||||||
|
"TITLE": "SAML",
|
||||||
|
"DESCRIPTION": "Applicazioni SAMML"
|
||||||
|
},
|
||||||
|
"CONFIGSECTION": "Configurazione SAML",
|
||||||
|
"URL": "URL in cui si trova il file di metadati",
|
||||||
|
"OR": "o",
|
||||||
|
"XML": "Carica Metadata XML",
|
||||||
|
"METADATA": "Metadata",
|
||||||
|
"METADATAFROMFILE": "Metadati dal file"
|
||||||
|
},
|
||||||
"AUTHMETHODS": {
|
"AUTHMETHODS": {
|
||||||
"CODE": {
|
"CODE": {
|
||||||
"TITLE": "Code",
|
"TITLE": "Code",
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'codemirror/mode/javascript/javascript';
|
import 'codemirror/mode/javascript/javascript';
|
||||||
|
import 'codemirror/mode/xml/xml';
|
||||||
|
|
||||||
import { enableProdMode } from '@angular/core';
|
import { enableProdMode } from '@angular/core';
|
||||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||||
|
@ -33,6 +33,7 @@ title: zitadel/app.proto
|
|||||||
| name | string | - | |
|
| name | string | - | |
|
||||||
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) config.oidc_config | OIDCConfig | - | |
|
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) config.oidc_config | OIDCConfig | - | |
|
||||||
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) config.api_config | APIConfig | - | |
|
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) config.api_config | APIConfig | - | |
|
||||||
|
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) config.saml_config | SAMLConfig | - | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -88,6 +89,18 @@ title: zitadel/app.proto
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### SAMLConfig
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Type | Description | Validation |
|
||||||
|
| ----- | ---- | ----------- | ----------- |
|
||||||
|
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) metadata.metadata_xml | bytes | - | |
|
||||||
|
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) metadata.metadata_url | string | - | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Enums
|
## Enums
|
||||||
|
@ -1254,6 +1254,19 @@ Returns a new generated secret if needed (Depending on the configuration)
|
|||||||
POST: /projects/{project_id}/apps/oidc
|
POST: /projects/{project_id}/apps/oidc
|
||||||
|
|
||||||
|
|
||||||
|
### AddSAMLApp
|
||||||
|
|
||||||
|
> **rpc** AddSAMLApp([AddSAMLAppRequest](#addsamlapprequest))
|
||||||
|
[AddSAMLAppResponse](#addsamlappresponse)
|
||||||
|
|
||||||
|
Adds a new saml service provider
|
||||||
|
Returns a entityID
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
POST: /projects/{project_id}/apps/saml
|
||||||
|
|
||||||
|
|
||||||
### AddAPIApp
|
### AddAPIApp
|
||||||
|
|
||||||
> **rpc** AddAPIApp([AddAPIAppRequest](#addapiapprequest))
|
> **rpc** AddAPIApp([AddAPIAppRequest](#addapiapprequest))
|
||||||
@ -1292,6 +1305,18 @@ Changes the configuration of the oidc client
|
|||||||
PUT: /projects/{project_id}/apps/{app_id}/oidc_config
|
PUT: /projects/{project_id}/apps/{app_id}/oidc_config
|
||||||
|
|
||||||
|
|
||||||
|
### UpdateSAMLAppConfig
|
||||||
|
|
||||||
|
> **rpc** UpdateSAMLAppConfig([UpdateSAMLAppConfigRequest](#updatesamlappconfigrequest))
|
||||||
|
[UpdateSAMLAppConfigResponse](#updatesamlappconfigresponse)
|
||||||
|
|
||||||
|
Changes the configuration of the saml application
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
PUT: /projects/{project_id}/apps/{app_id}/saml_config
|
||||||
|
|
||||||
|
|
||||||
### UpdateAPIAppConfig
|
### UpdateAPIAppConfig
|
||||||
|
|
||||||
> **rpc** UpdateAPIAppConfig([UpdateAPIAppConfigRequest](#updateapiappconfigrequest))
|
> **rpc** UpdateAPIAppConfig([UpdateAPIAppConfigRequest](#updateapiappconfigrequest))
|
||||||
@ -2994,7 +3019,7 @@ This is an empty request
|
|||||||
| Field | Type | Description | Validation |
|
| Field | Type | Description | Validation |
|
||||||
| ----- | ---- | ----------- | ----------- |
|
| ----- | ---- | ----------- | ----------- |
|
||||||
| primary_color | string | - | string.max_len: 50<br /> |
|
| primary_color | string | - | string.max_len: 50<br /> |
|
||||||
| hide_login_name_suffix | bool | hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set. Details about this [scope in](../openidoauth/scopes) | |
|
| hide_login_name_suffix | bool | hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set. Details about this scope in https://docs.zitadel.com/concepts#Reserved_Scopes | |
|
||||||
| warn_color | string | - | string.max_len: 50<br /> |
|
| warn_color | string | - | string.max_len: 50<br /> |
|
||||||
| background_color | string | - | string.max_len: 50<br /> |
|
| background_color | string | - | string.max_len: 50<br /> |
|
||||||
| font_color | string | - | string.max_len: 50<br /> |
|
| font_color | string | - | string.max_len: 50<br /> |
|
||||||
@ -3670,6 +3695,32 @@ This is an empty request
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### AddSAMLAppRequest
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Type | Description | Validation |
|
||||||
|
| ----- | ---- | ----------- | ----------- |
|
||||||
|
| project_id | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
|
||||||
|
| name | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
|
||||||
|
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) metadata.metadata_xml | bytes | - | bytes.max_len: 500000<br /> |
|
||||||
|
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) metadata.metadata_url | string | - | string.max_len: 200<br /> |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### AddSAMLAppResponse
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Type | Description | Validation |
|
||||||
|
| ----- | ---- | ----------- | ----------- |
|
||||||
|
| app_id | string | - | |
|
||||||
|
| details | zitadel.v1.ObjectDetails | - | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### AddSecondFactorToLoginPolicyRequest
|
### AddSecondFactorToLoginPolicyRequest
|
||||||
|
|
||||||
|
|
||||||
@ -8309,6 +8360,31 @@ This is an empty request
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### UpdateSAMLAppConfigRequest
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Type | Description | Validation |
|
||||||
|
| ----- | ---- | ----------- | ----------- |
|
||||||
|
| project_id | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
|
||||||
|
| app_id | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
|
||||||
|
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) metadata.metadata_xml | bytes | - | bytes.max_len: 500000<br /> |
|
||||||
|
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) metadata.metadata_url | string | - | string.max_len: 200<br /> |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### UpdateSAMLAppConfigResponse
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Type | Description | Validation |
|
||||||
|
| ----- | ---- | ----------- | ----------- |
|
||||||
|
| details | zitadel.v1.ObjectDetails | - | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### UpdateUserGrantRequest
|
### UpdateUserGrantRequest
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ title: zitadel/policy.proto
|
|||||||
| details | zitadel.v1.ObjectDetails | - | |
|
| details | zitadel.v1.ObjectDetails | - | |
|
||||||
| primary_color | string | hex value for primary color | |
|
| primary_color | string | hex value for primary color | |
|
||||||
| is_default | bool | defines if the organisation's admin changed the policy | |
|
| is_default | bool | defines if the organisation's admin changed the policy | |
|
||||||
| hide_login_name_suffix | bool | hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set. Details about this [scope in](../openidoauth/scopes) | |
|
| hide_login_name_suffix | bool | hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set. Details about this scope in https://docs.zitadel.com/concepts#Reserved_Scopes | |
|
||||||
| warn_color | string | hex value for secondary color | |
|
| warn_color | string | hex value for secondary color | |
|
||||||
| background_color | string | hex value for background color | |
|
| background_color | string | hex value for background color | |
|
||||||
| font_color | string | hex value for font color | |
|
| font_color | string | hex value for font color | |
|
||||||
|
60
docs/docs/apis/saml/endpoints.md
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
title: Endpoints
|
||||||
|
---
|
||||||
|
|
||||||
|
## SAML 2.0 Metadata
|
||||||
|
|
||||||
|
The SAML Metadata is located within the issuer domain. This would give us {your_domain}/saml/v2/metadata.
|
||||||
|
|
||||||
|
This metadata contains all the information defined in the spec.
|
||||||
|
|
||||||
|
**Link to
|
||||||
|
spec.** [Metadata for the OASIS Security Assertion Markup Language (SAML) V2.0 – Errata Composite](https://www.oasis-open.org/committees/download.php/35391/sstc-saml-metadata-errata-2.0-wd-04-diff.pdf)
|
||||||
|
|
||||||
|
## Certificate Endpoint
|
||||||
|
|
||||||
|
{your_domain}/saml/v2/certificate
|
||||||
|
|
||||||
|
The certificate endpoint provides the certificate which is used to sign the responses for download, for easier use with
|
||||||
|
different service providers which want the certificate separately instead of inside the metadata.
|
||||||
|
|
||||||
|
## SSO Endpoint
|
||||||
|
|
||||||
|
{your_domain}/saml/v2/SSO
|
||||||
|
|
||||||
|
The SSO endpoint is the starting point for all initial user authentications. The user agent (browser) will be redirected
|
||||||
|
to this endpoint to authenticate the user.
|
||||||
|
|
||||||
|
Supported on this endpoint or currently `urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect`
|
||||||
|
or `urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST` bindings.
|
||||||
|
|
||||||
|
**Link to
|
||||||
|
spec.** [Bindings for the OASIS Security Assertion Markup Language (SAML) V2.0 – Errata Composite](https://www.oasis-open.org/committees/download.php/35387/sstc-saml-bindings-errata-2.0-wd-05-diff.pdf)
|
||||||
|
|
||||||
|
### Required request parameters
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| RelayState | ID to associate the exchange with the original request. |
|
||||||
|
| SAMLRequest | The request made to the SAML IDP. (base64 encoded) |
|
||||||
|
| SigAlg | Algorithm used to sign the request, only if binding is 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' as signature has to be provided es separate parameter. (base64 encoded) |
|
||||||
|
| Signature | Signature of the request as parameter with 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' binding. (base64 encoded) |
|
||||||
|
|
||||||
|
### Successful Response
|
||||||
|
|
||||||
|
Depending on the content of the request the response comes back in the requested binding, but the content is the same.
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| RelayState | ID to associate the exchange with the original request. |
|
||||||
|
| SAMLResponse | The response form the SAML IDP. (base64 encoded) |
|
||||||
|
| SigAlg | Algorithm used to sign the response, only if binding is 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' as signature has to be provided es separate parameter. (base64 encoded) |
|
||||||
|
| Signature | Signature of the response as parameter with 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' binding. (base64 encoded) |
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
|
||||||
|
Regardless of the error, the used http error code will be '200', which represents a successful request. Whereas the
|
||||||
|
response will contain a StatusCode include a message which provides more information if an error occurred.
|
||||||
|
|
||||||
|
**Link to
|
||||||
|
spec** [Assertions and Protocols for the OASIS Security Assertion Markup Language (SAML) V2.0 – Errata Composite](https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf)
|
@ -40,6 +40,7 @@ All APIs of ZITADEL are always available as gRCP, gRPC-web and REST service.
|
|||||||
The only exception is the [OpenID Connect & OAuth](/docs/apis/openidoauth/endpoints) and [Asset API](/docs/apis/introduction#assets) due their unique nature.
|
The only exception is the [OpenID Connect & OAuth](/docs/apis/openidoauth/endpoints) and [Asset API](/docs/apis/introduction#assets) due their unique nature.
|
||||||
|
|
||||||
- [OpenID Connect & OAuth](/docs/apis/openidoauth/endpoints) - allows to request authentication and authorization of ZITADEL
|
- [OpenID Connect & OAuth](/docs/apis/openidoauth/endpoints) - allows to request authentication and authorization of ZITADEL
|
||||||
|
- [SAML](/docs/apis/saml/endpoints) - allows to request authentication and authorization of ZITADEL through the SAML standard
|
||||||
- [Authentication API](/docs/apis/introduction#authentication) - allow a user to do operation in its own context
|
- [Authentication API](/docs/apis/introduction#authentication) - allow a user to do operation in its own context
|
||||||
- [Management API](/docs/apis/introduction#management) - allows an admin or machine to manage the ZITADEL resources on an organization level
|
- [Management API](/docs/apis/introduction#management) - allows an admin or machine to manage the ZITADEL resources on an organization level
|
||||||
- [Administration API](/docs/apis/introduction#administration) - allows an admin or machine to manage the ZITADEL resources on an instance level
|
- [Administration API](/docs/apis/introduction#administration) - allows an admin or machine to manage the ZITADEL resources on an instance level
|
||||||
|
96
docs/docs/guides/integrate/atlassian-saml.md
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
---
|
||||||
|
title: Connect with Atlassian through SAML 2.0
|
||||||
|
---
|
||||||
|
|
||||||
|
This guide shows how to enable login with ZITADEL on Atlassian.
|
||||||
|
|
||||||
|
It covers how to:
|
||||||
|
|
||||||
|
- create and configure the application in your project
|
||||||
|
- create and configure the connection in Atlassian Access
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- existing ZITADEL Instance, if not present follow [this guide](../../guides/start/quickstart)
|
||||||
|
- existing ZITADEL Organization, if not present follow [this guide](../../guides/manage/console/organizations)
|
||||||
|
- existing ZITADEL project, if not present follow the first 3 steps [here](../../guides/manage/console/projects)
|
||||||
|
- existing Atlassian Access setup, including verified domain
|
||||||
|
|
||||||
|
> We have to switch between ZITADEL and Atlassian. If the headings begin with "ZITADEL" switch to the ZITADEL
|
||||||
|
> Console and
|
||||||
|
> if the headings start with "Atlassian" please switch to the Atlassian Admin GUI.
|
||||||
|
|
||||||
|
## **Atlassian**: Create a new external identity provider
|
||||||
|
|
||||||
|
Please follow the instructions on [Atlassian's support page](https://support.atlassian.com/security-and-access-policies/docs/configure-saml-single-sign-on-with-an-identity-provider/) to configure a SAML identity provider for SSO.
|
||||||
|
The following instructions give you a quick overview of the most important steps.
|
||||||
|
|
||||||
|
Login to Atlassian's security center and select Identity providers.
|
||||||
|
Select the option to Set up SAML single sign-on.
|
||||||
|

|
||||||
|
|
||||||
|
For Identity Provider select "Other provider" and enter a Directory Name.
|
||||||
|

|
||||||
|
|
||||||
|
Follow the wizard.
|
||||||
|
Fill in the following information:
|
||||||
|
|
||||||
|
- `Identity provider Entity ID`: {your_instance_domain}/saml/v2/metadata
|
||||||
|
- `Identity provider SSO URL`: {your_instance_domain}/saml/v2/SSO
|
||||||
|
- `Public x509 certificate`: You need to download and paste the value of the certificate from {your_instance_domain}/saml/v2/certificate
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Create a new .xml file with the following minimal SAML metadata contents:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="${ENTITYID}">
|
||||||
|
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol">
|
||||||
|
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="${ACSURL}" index="0"/>
|
||||||
|
</md:SPSSODescriptor>
|
||||||
|
</md:EntityDescriptor>
|
||||||
|
```
|
||||||
|
|
||||||
|
Set or replace the variables with the values from the next screen as follows:
|
||||||
|
|
||||||
|
- `${ENTITYID}`: Copy the value from "Service provider entity URL"
|
||||||
|
- `${ACSURL}`: Copy the value from "Service provider assertion consumer service URL"
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## **ZITADEL**: Create the application
|
||||||
|
|
||||||
|
In your existing project:
|
||||||
|
|
||||||
|
Press the "+"-button to add an application
|
||||||
|

|
||||||
|
|
||||||
|
Fill in a name for the application and chose the SAML type, then click "Continue".
|
||||||
|

|
||||||
|
|
||||||
|
Either fill in the URL where ZITADEL can read the metadata from, or upload the metadata XML directly, then click "Continue".
|
||||||
|

|
||||||
|
|
||||||
|
Check your application, if everything is correct, press "Create".
|
||||||
|

|
||||||
|
|
||||||
|
## **Atlassian**: Setup authentication policies
|
||||||
|
|
||||||
|
Under Authentication policies, select "Edit" on the directory that you have created.
|
||||||
|
Then check the box "Enforce single sign-on" and confirm by clicking "Update".
|
||||||
|

|
||||||
|
|
||||||
|
Add members to your policy.
|
||||||
|

|
||||||
|
|
||||||
|
## Verify configuration
|
||||||
|
|
||||||
|
Now you should be all set to verify your setup:
|
||||||
|
|
||||||
|
- Create an user in ZITADEL with the same email address as a member in your authentication policy.
|
||||||
|
- In a new browser session go to https://id.atlassian.com
|
||||||
|
- Enter the user's email address
|
||||||
|
- You should be redirected to ZITADEL's Login screen
|
||||||
|
- Enter the email address and password
|
||||||
|
- Continue and you should be redirected back to Atlassian
|
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: Connect with Auth0
|
title: Connect with Auth0 through OIDC
|
||||||
---
|
---
|
||||||
|
|
||||||
This guide shows how to enable login with ZITADEL on Auth0.
|
This guide shows how to enable login with ZITADEL on Auth0.
|
||||||
@ -16,7 +16,7 @@ Prerequisites:
|
|||||||
- existing ZITADEL project, if not present follow the first 3 steps [here](../../guides/manage/console/projects)
|
- existing ZITADEL project, if not present follow the first 3 steps [here](../../guides/manage/console/projects)
|
||||||
- existing Auth0 tenant as described [here](https://auth0.com/docs/get-started/auth0-overview/create-tenants)
|
- existing Auth0 tenant as described [here](https://auth0.com/docs/get-started/auth0-overview/create-tenants)
|
||||||
|
|
||||||
> We have to switch between ZITADEL and a Auth0. If the headings begin with "ZITADEL" switch to the ZITADEL Console and if the headings start with Auth0 please switch to the Auth0 GUI.
|
> We have to switch between ZITADEL and a Auth0. If the headings begin with "ZITADEL" switch to the ZITADEL Console and if the headings start with "Auth0" please switch to the Auth0 GUI.
|
||||||
|
|
||||||
## **Auth0**: Create a new connection
|
## **Auth0**: Create a new connection
|
||||||
|
|
78
docs/docs/guides/integrate/auth0-saml.md
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
title: Connect with Auth0 through SAML 2.0
|
||||||
|
---
|
||||||
|
|
||||||
|
This guide shows how to enable login with ZITADEL on Auth0.
|
||||||
|
|
||||||
|
It covers how to:
|
||||||
|
|
||||||
|
- create and configure the application in your project
|
||||||
|
- create and configure the connection in your Auth0 tenant
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- existing ZITADEL Instance, if not present follow [this guide](../../guides/start/quickstart)
|
||||||
|
- existing ZITADEL Organization, if not present follow [this guide](../../guides/manage/console/organizations)
|
||||||
|
- existing ZITADEL project, if not present follow the first 3 steps [here](../../guides/manage/console/projects)
|
||||||
|
- existing Auth0 tenant as described [here](https://auth0.com/docs/get-started/auth0-overview/create-tenants)
|
||||||
|
|
||||||
|
> We have to switch between ZITADEL and a Auth0. If the headings begin with "ZITADEL" switch to the ZITADEL Console and
|
||||||
|
> if the headings start with "Auth0" please switch to the Auth0 GUI.
|
||||||
|
|
||||||
|
## **Auth0**: Create a new connection
|
||||||
|
|
||||||
|
In Authentication -> Enterprise
|
||||||
|

|
||||||
|
|
||||||
|
1. Press the "+" button right to "SAML"
|
||||||
|

|
||||||
|
2. Fill out the fields as follows in the SAML Connection:
|
||||||
|

|
||||||
|
|
||||||
|
This includes:
|
||||||
|
|
||||||
|
- a unique "Connection name"
|
||||||
|
- the "Sign In URL"
|
||||||
|
- the "Sign Out URL"
|
||||||
|
- used "User ID Attribute"
|
||||||
|
- the definition how the request should be signed
|
||||||
|
- which binding should be used to call ZITADEL
|
||||||
|
|
||||||
|
All the information is filled out as an example, and to connect with any other environment you only have to change the
|
||||||
|
used domain, for example "example.com" with "zitadel.cloud".
|
||||||
|
Lastly, upload the certificate used to sign the reponses, provided for you under the
|
||||||
|
URL {your_instance_domain}/saml/v2/certificate.
|
||||||
|
|
||||||
|
Then just press the button "Create" and the connection on Auth0 is configured.
|
||||||
|
|
||||||
|
## **ZITADEL**: Create the application
|
||||||
|
|
||||||
|
You need to upload the SAML metadata to ZITADEL for it to recognize this newly created connection.
|
||||||
|
[Under this link](https://auth0.com/docs/authenticate/protocols/saml/saml-identity-provider-configuration-settings) are
|
||||||
|
all necessary information to correctly fill out the metadata or download the metadata-file directly under the
|
||||||
|
URL https://YOUR_AUTH0_DOMAIN/samlp/metadata?connection=YOUR_CONNECTION_NAME, which in this example would
|
||||||
|
be https://example.auth0.com/samlp/metadata?connection=SAML-ZITADEL.
|
||||||
|
|
||||||
|
In your existing project:
|
||||||
|
|
||||||
|
1. Press the "+"-button to add an application
|
||||||
|

|
||||||
|
2. Fill in a name for the application and chose the SAML type, then click "Continue".
|
||||||
|

|
||||||
|
3. Either fill in the URL where ZITADEL can read the metadata from, or upload the metadata XML directly, then click "
|
||||||
|
Continue".
|
||||||
|

|
||||||
|
4. Check your application, if everything is correct, press "Create".
|
||||||
|

|
||||||
|
|
||||||
|
Everything on the side of ZITADEL is done if the application is correctly created.
|
||||||
|
|
||||||
|
## **Auth0**: Try the connection
|
||||||
|
|
||||||
|
To then test the connection you only have to press "Try" on the created connection in the Authentication -> Enterprise
|
||||||
|
screen.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
To further customize the requests you can also customize the SAML communication as
|
||||||
|
described [here](https://auth0.com/docs/authenticate/protocols/saml/saml-configuration/customize-saml-assertions)
|
78
docs/docs/guides/integrate/aws-saml.md
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
title: Connect with AWS through SAML 2.0
|
||||||
|
---
|
||||||
|
|
||||||
|
This guide shows how to enable login with ZITADEL on AWS SSO.
|
||||||
|
|
||||||
|
It covers how to:
|
||||||
|
|
||||||
|
- create and configure the application in your project
|
||||||
|
- create and configure the connection in your AWS SSO external IDP
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- existing ZITADEL Instance, if not present follow [this guide](../../guides/start/quickstart)
|
||||||
|
- existing ZITADEL Organization, if not present follow [this guide](../../guides/manage/console/organizations)
|
||||||
|
- existing ZITADEL project, if not present follow the first 3 steps [here](../../guides/manage/console/projects)
|
||||||
|
- prerequisites on AWS side [here](https://docs.aws.amazon.com/singlesignon/latest/userguide/prereqs.html).
|
||||||
|
- enabled AWS SSO [here](https://docs.aws.amazon.com/singlesignon/latest/userguide/step1.html?icmpid=docs_sso_console)
|
||||||
|
|
||||||
|
> We have to switch between ZITADEL and a AWS. If the headings begin with "ZITADEL" switch to the ZITADEL Console and if
|
||||||
|
> the headings start with "AWS" please switch to the AWS GUI.
|
||||||
|
|
||||||
|
## **AWS**: Change to external identity provider ZITADEL
|
||||||
|
|
||||||
|
As you have activated SSO you still have the possibility to use AWS itself to manage the users, but you can also use a
|
||||||
|
Microsoft AD or an external IDP.
|
||||||
|
|
||||||
|
Described [here](https://docs.aws.amazon.com/singlesignon/latest/userguide/manage-your-identity-source-idp.html) how you
|
||||||
|
can connect to ZITADEL as a SAML2 IDP.
|
||||||
|
|
||||||
|
1. Chose the External identity provider:
|
||||||
|

|
||||||
|
|
||||||
|
2. Download the metadata file, to provide ZITADEL with all the information it needs, and save the AWS SSO Sign-in URL,
|
||||||
|
which you use to login afterwards.
|
||||||
|
|
||||||
|
3. Fill out the fields as follows, to provide AWS with all the information it needs:
|
||||||
|

|
||||||
|
|
||||||
|
To connect to another environment, change the domains, for example if you would use ZITADEL under the domain "
|
||||||
|
example.com" you would have the URLs "https://accounts.example.com/saml/SSO"
|
||||||
|
and "https://accounts.exmaple.com/saml/metadata".
|
||||||
|
|
||||||
|
4. Download the ZITADEL-used certificate to sign the responses, so that AWS can validation the signature.
|
||||||
|
|
||||||
|
You can download the certificate from following
|
||||||
|
URL: {your_instance_domain}/saml/v2/certificate
|
||||||
|
|
||||||
|
5. Then upload the ".crt"-file to AWS and click "next".
|
||||||
|
|
||||||
|
6. Lastly, accept to confirm the change and ZITADEL is used as the external identity provider for AWS SSO to provide
|
||||||
|
connectivity to your AWS Accounts.
|
||||||
|
|
||||||
|
As for how the SSO users are then connected to the AWS accounts, you can find more information in the AWS documentation,
|
||||||
|
for example [here](https://docs.aws.amazon.com/singlesignon/latest/userguide/useraccess.html).
|
||||||
|
|
||||||
|
## **ZITADEL**: Create the application
|
||||||
|
|
||||||
|
The metadata used in this part is from "Change to external identity provider ZITADEL" step 2.
|
||||||
|
|
||||||
|
In your existing project:
|
||||||
|
|
||||||
|
1. Press the "+"-button to add an application
|
||||||
|

|
||||||
|
2. Fill in a name for the application and chose the SAML type, then click "Continue".
|
||||||
|

|
||||||
|
3. Either fill in the URL where ZITADEL can read the metadata from, or upload the metadata XML directly, then click "
|
||||||
|
Continue".
|
||||||
|

|
||||||
|
4. Check your application, if everything is correct, press "Create".
|
||||||
|

|
||||||
|
|
||||||
|
Everything on the side of ZITADEL is done if the application is correctly created.
|
||||||
|
|
||||||
|
## **AWS**: Test the connection
|
||||||
|
|
||||||
|
The result, you can now login to you AWS account through your ZITADEL-login with the AWS SSO Sign-in URL, which you
|
||||||
|
should have saved in "Change to external identity provider ZITADEL" step 2.
|
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: Connect with AzureAD
|
title: Connect with AzureAD through OIDC
|
||||||
---
|
---
|
||||||
|
|
||||||
## AzureAD Tenant as Identity Provider for ZITADEL
|
## AzureAD Tenant as Identity Provider for ZITADEL
|
67
docs/docs/guides/integrate/gitlab-saml.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
title: Connect with Gitlab through SAML 2.0
|
||||||
|
---
|
||||||
|
|
||||||
|
This guide shows how to enable login with ZITADEL on Gitlab.
|
||||||
|
|
||||||
|
It covers how to:
|
||||||
|
|
||||||
|
- create and configure the application in your project
|
||||||
|
- create and configure the connection in Gitlab SaaS
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- existing ZITADEL Instance, if not present follow [this guide](../../guides/start/quickstart)
|
||||||
|
- existing ZITADEL Organization, if not present follow [this guide](../../guides/manage/console/organizations)
|
||||||
|
- existing ZITADEL project, if not present follow the first 3 steps [here](../../guides/manage/console/projects)
|
||||||
|
- existing Gitlab SaaS Setup in the premium tier
|
||||||
|
|
||||||
|
> We have to switch between ZITADEL and Gitlab. If the headings begin with "ZITADEL" switch to the ZITADEL
|
||||||
|
> Console and
|
||||||
|
> if the headings start with "Gitlab" please switch to the Gitlab GUI.
|
||||||
|
|
||||||
|
## **Gitlab**: Create a new external identity provider
|
||||||
|
|
||||||
|
Please follow the instructions on [Gitlab docs](https://docs.gitlab.com/ee/user/group/saml_sso/index.html) to configure a SAML identity provider for SSO.
|
||||||
|
The following instructions give you a quick overview of the most important steps.
|
||||||
|
|
||||||
|
[Open the group](https://gitlab.com/dashboard/groups) to which you want to add the SSO configuration.
|
||||||
|
Select on the menu Settings and then SAML SSO.
|
||||||
|
Copy `GitLab metadata URL` for the next step.
|
||||||
|

|
||||||
|
|
||||||
|
## **ZITADEL**: Create the application
|
||||||
|
|
||||||
|
In your existing project:
|
||||||
|
|
||||||
|
Press the "+"-button to add an application
|
||||||
|

|
||||||
|
|
||||||
|
Fill in a name for the application and chose the SAML type, then click "Continue".
|
||||||
|

|
||||||
|
|
||||||
|
Enter the URL from before, then click "Continue".
|
||||||
|

|
||||||
|
|
||||||
|
Check your application, if everything is correct, press "Create".
|
||||||
|

|
||||||
|
|
||||||
|
## **Gitlab**: Configuration
|
||||||
|
|
||||||
|
Complete the configuration as follows:
|
||||||
|
|
||||||
|
- `Identity provider single sign-on URL`: {your_instance_domain}/saml/v2/SSO
|
||||||
|
- `Certificate fingerprint`: You need to download the certificate from {your_instance_domain}/saml/v2/certificate and create a SHA1 fingerprint
|
||||||
|
|
||||||
|
Save the changes.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## **Gitlab**: Verify SAML configuration
|
||||||
|
|
||||||
|
Once you saved the changes, click on the button "Verify SAML configuration".
|
||||||
|
|
||||||
|
You should be redirected to ZITADEL.
|
||||||
|
Login with your user.
|
||||||
|
After that you should be redirected back to GitLab and you can inspect the Response Output.
|
||||||
|

|
84
docs/docs/guides/integrate/pingidentity-saml.md
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
---
|
||||||
|
title: Connect with Ping Identity through SAML 2.0
|
||||||
|
---
|
||||||
|
|
||||||
|
This guide shows how to enable login with ZITADEL on Auth0.
|
||||||
|
|
||||||
|
It covers how to:
|
||||||
|
|
||||||
|
- create and configure the application in your project
|
||||||
|
- create and configure the connection in your Ping Identity tenant
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- existing ZITADEL Instance, if not present follow [this guide](../../guides/start/quickstart)
|
||||||
|
- existing ZITADEL Organization, if not present follow [this guide](../../guides/manage/console/organizations)
|
||||||
|
- existing ZITADEL project, if not present follow the first 3 steps [here](../../guides/manage/console/projects)
|
||||||
|
- existing Pingidentity environment [here](https://docs.pingidentity.com/bundle/pingone/page/wqe1564020490538.html)
|
||||||
|
|
||||||
|
> We have to switch between ZITADEL and Ping Identity. If the headings begin with "ZITADEL" switch to the ZITADEL
|
||||||
|
> Console and
|
||||||
|
> if the headings start with "Ping" please switch to the PingIdentity GUI.
|
||||||
|
|
||||||
|
## **Ping**: Create a new external identity provider
|
||||||
|
|
||||||
|
To add an
|
||||||
|
additional [external identity provider](https://docs.pingidentity.com/bundle/pingone/page/jvz1567784210191.html), you
|
||||||
|
can follow the instructions [here](https://docs.pingidentity.com/bundle/pingone/page/ovy1567784211297.html)
|
||||||
|
|
||||||
|
1. As described you have to create a new provider, with a unique identifier:
|
||||||
|

|
||||||
|
|
||||||
|
We recommend activating signing the auth request whenever possible:
|
||||||
|

|
||||||
|
|
||||||
|
2. Manually enter the necessary information:
|
||||||
|
|
||||||
|
- SSO Endpoint, for example https://accounts.example.com/saml/SSO
|
||||||
|
- IDP EntityID, for example https://accounts.example.com/saml/metadata
|
||||||
|
- Binding, which is a decision which you can take yourself, we recommend HTTP POST as it has fewer restrictions
|
||||||
|
- Import certificate, provided from the certificate endpoint
|
||||||
|

|
||||||
|
|
||||||
|
Everything you need to know about the attribute mapping you can find
|
||||||
|
in [Ping Identity's documentation](https://docs.pingidentity.com/bundle/pingone/page/pwv1567784207915.html)
|
||||||
|
|
||||||
|
3. With this you have defined to connection to ZITADEL as an external IDP, next is the policy to use ZITADEL as an IDP
|
||||||
|
to
|
||||||
|
connect to an application. The "How to" for that can be
|
||||||
|
found [here](https://docs.pingidentity.com/bundle/pingone/page/zqd1616600404402.html).
|
||||||
|
|
||||||
|
## **ZITADEL**: Create the application
|
||||||
|
|
||||||
|
To add the connection to ZITADEL you have to build the metadata, which should minimalistic look like this, the necessary
|
||||||
|
information can be found on the External IDPs page under "P1Connection" and "IDP Configuration" :
|
||||||
|
|
||||||
|
```xml
|
||||||
|
ENTITYID="PINGONE (SP) ENTITY ID"
|
||||||
|
ACSURL="ACS ENDPOINT"
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="${ENTITYID}">
|
||||||
|
<md:SPSSODescriptor
|
||||||
|
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol">
|
||||||
|
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="${ACSURL}"
|
||||||
|
index="0"/>
|
||||||
|
</md:SPSSODescriptor>
|
||||||
|
</md:EntityDescriptor>
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
In your existing project:
|
||||||
|
|
||||||
|
1. Press the "+"-button to add an application
|
||||||
|

|
||||||
|
2. Fill in a name for the application and chose the SAML type, then click "Continue".
|
||||||
|

|
||||||
|
3. Either fill in the URL where ZITADEL can read the metadata from, or upload the metadata XML directly, then click "
|
||||||
|
Continue".
|
||||||
|

|
||||||
|
4. Check your application, if everything is correct, press "Create".
|
||||||
|

|
||||||
|
|
||||||
|
Everything on the side of ZITADEL is done if the application is correctly created.
|
@ -115,17 +115,43 @@ module.exports = {
|
|||||||
label: "Integrate",
|
label: "Integrate",
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
items: [
|
items: [
|
||||||
|
"guides/integrate/login-users",
|
||||||
"guides/integrate/identity-brokering",
|
"guides/integrate/identity-brokering",
|
||||||
|
{
|
||||||
|
type: "category",
|
||||||
|
label: "Access ZITADEL APIs",
|
||||||
|
collapsed: false,
|
||||||
|
items: [
|
||||||
|
"guides/integrate/serviceusers",
|
||||||
"guides/integrate/access-zitadel-apis",
|
"guides/integrate/access-zitadel-apis",
|
||||||
"guides/integrate/access-zitadel-system-api",
|
"guides/integrate/access-zitadel-system-api",
|
||||||
"guides/integrate/authenticated-mongodb-charts",
|
"guides/integrate/export-and-import",
|
||||||
"guides/integrate/auth0",
|
],
|
||||||
"guides/integrate/azuread",
|
},
|
||||||
"guides/integrate/gitlab-self-hosted",
|
{
|
||||||
"guides/integrate/login-users",
|
type: "category",
|
||||||
"guides/integrate/serviceusers",
|
label: "OpenID Connect 1.0 Clients",
|
||||||
|
collapsed: false,
|
||||||
|
items: [
|
||||||
"guides/integrate/oauth-recommended-flows",
|
"guides/integrate/oauth-recommended-flows",
|
||||||
"guides/integrate/export-and-import"
|
"guides/integrate/auth0-oidc",
|
||||||
|
"guides/integrate/azuread-oidc",
|
||||||
|
"guides/integrate/authenticated-mongodb-charts",
|
||||||
|
"guides/integrate/gitlab-self-hosted",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "category",
|
||||||
|
label: "SAML 2.0 Clients",
|
||||||
|
collapsed: false,
|
||||||
|
items: [
|
||||||
|
"guides/integrate/auth0-saml",
|
||||||
|
"guides/integrate/aws-saml",
|
||||||
|
"guides/integrate/pingidentity-saml",
|
||||||
|
"guides/integrate/atlassian-saml",
|
||||||
|
"guides/integrate/gitlab-saml",
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -205,6 +231,14 @@ module.exports = {
|
|||||||
"apis/openidoauth/grant-types",
|
"apis/openidoauth/grant-types",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "category",
|
||||||
|
label: "SAML",
|
||||||
|
collapsed: false,
|
||||||
|
items: [
|
||||||
|
"apis/saml/endpoints",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "category",
|
type: "category",
|
||||||
label: "Rate Limits",
|
label: "Rate Limits",
|
||||||
|
BIN
docs/static/img/saml/atlassian/atlassian-01.png
vendored
Normal file
After Width: | Height: | Size: 182 KiB |
BIN
docs/static/img/saml/atlassian/atlassian-02.png
vendored
Normal file
After Width: | Height: | Size: 87 KiB |
BIN
docs/static/img/saml/atlassian/atlassian-03.png
vendored
Normal file
After Width: | Height: | Size: 127 KiB |
BIN
docs/static/img/saml/atlassian/atlassian-04.png
vendored
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
docs/static/img/saml/atlassian/atlassian-05.png
vendored
Normal file
After Width: | Height: | Size: 185 KiB |
BIN
docs/static/img/saml/atlassian/atlassian-06.png
vendored
Normal file
After Width: | Height: | Size: 95 KiB |
BIN
docs/static/img/saml/auth0/auth_enterprise.png
vendored
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
docs/static/img/saml/auth0/auth_enterprise_try.png
vendored
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
docs/static/img/saml/auth0/connection.png
vendored
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
docs/static/img/saml/auth0/enterprise_connections.png
vendored
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
docs/static/img/saml/aws/change_idp.png
vendored
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
docs/static/img/saml/aws/configure_idp.png
vendored
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
docs/static/img/saml/gitlab/gitlab-01.png
vendored
Normal file
After Width: | Height: | Size: 207 KiB |
BIN
docs/static/img/saml/gitlab/gitlab-02.png
vendored
Normal file
After Width: | Height: | Size: 207 KiB |
BIN
docs/static/img/saml/gitlab/gitlab-03.png
vendored
Normal file
After Width: | Height: | Size: 160 KiB |
BIN
docs/static/img/saml/pingidentity/conf_connection.png
vendored
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
docs/static/img/saml/pingidentity/conf_idp_connection.png
vendored
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
docs/static/img/saml/pingidentity/create_idp_profile.png
vendored
Normal file
After Width: | Height: | Size: 67 KiB |
BIN
docs/static/img/saml/pingidentity/idp_idp_configuration.png
vendored
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
docs/static/img/saml/pingidentity/idp_p1_connection.png
vendored
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
docs/static/img/saml/zitadel/application_saml.png
vendored
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
docs/static/img/saml/zitadel/application_saml_create.png
vendored
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
docs/static/img/saml/zitadel/application_saml_metadata.png
vendored
Normal file
After Width: | Height: | Size: 156 KiB |
BIN
docs/static/img/saml/zitadel/project.png
vendored
Normal file
After Width: | Height: | Size: 37 KiB |
@ -26,7 +26,7 @@ export function ensureSomethingExists(api: apiCallProperties, searchPath: string
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}).then((data) => {
|
}).then((data) => {
|
||||||
awaitDesired(30, (entity) => !!entity, data.initialSequence, api, searchPath, find)
|
awaitDesired(90, (entity) => !!entity, data.initialSequence, api, searchPath, find)
|
||||||
return cy.wrap<number>(data.id)
|
return cy.wrap<number>(data.id)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -49,7 +49,7 @@ export function ensureSomethingDoesntExist(api: apiCallProperties, searchPath: s
|
|||||||
return sRes.sequence
|
return sRes.sequence
|
||||||
})
|
})
|
||||||
}).then((initialSequence) => {
|
}).then((initialSequence) => {
|
||||||
awaitDesired(30, (entity) => !entity , initialSequence, api, searchPath, find)
|
awaitDesired(90, (entity) => !entity , initialSequence, api, searchPath, find)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -77,7 +77,10 @@ function searchSomething(api: apiCallProperties, searchPath: string, find: (enti
|
|||||||
|
|
||||||
function awaitDesired(trials: number, expectEntity: (entity: any) => boolean, initialSequence: number, api: apiCallProperties, searchPath: string, find: (entity: any) => boolean) {
|
function awaitDesired(trials: number, expectEntity: (entity: any) => boolean, initialSequence: number, api: apiCallProperties, searchPath: string, find: (entity: any) => boolean) {
|
||||||
searchSomething(api, searchPath, find).then(resp => {
|
searchSomething(api, searchPath, find).then(resp => {
|
||||||
if (!expectEntity(resp.entity) || resp.sequence <= initialSequence) {
|
const foundExpectedEntity = expectEntity(resp.entity)
|
||||||
|
const foundExpectedSequence = resp.sequence > initialSequence
|
||||||
|
|
||||||
|
if (!foundExpectedEntity || !foundExpectedSequence) {
|
||||||
expect(trials, `trying ${trials} more times`).to.be.greaterThan(0);
|
expect(trials, `trying ${trials} more times`).to.be.greaterThan(0);
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
awaitDesired(trials - 1, expectEntity, initialSequence, api, searchPath, find)
|
awaitDesired(trials - 1, expectEntity, initialSequence, api, searchPath, find)
|
||||||
|
9
go.mod
@ -48,11 +48,12 @@ require (
|
|||||||
github.com/sony/sonyflake v1.0.0
|
github.com/sony/sonyflake v1.0.0
|
||||||
github.com/spf13/cobra v1.3.0
|
github.com/spf13/cobra v1.3.0
|
||||||
github.com/spf13/viper v1.10.1
|
github.com/spf13/viper v1.10.1
|
||||||
github.com/stretchr/testify v1.7.1
|
github.com/stretchr/testify v1.8.0
|
||||||
github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203
|
github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203
|
||||||
github.com/ttacon/libphonenumber v1.2.1
|
github.com/ttacon/libphonenumber v1.2.1
|
||||||
github.com/zitadel/logging v0.3.4
|
github.com/zitadel/logging v0.3.4
|
||||||
github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.5
|
github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.5
|
||||||
|
github.com/zitadel/saml v0.0.6
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.27.0
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.27.0
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.27.0
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.27.0
|
||||||
go.opentelemetry.io/otel v1.2.0
|
go.opentelemetry.io/otel v1.2.0
|
||||||
@ -82,6 +83,8 @@ require (
|
|||||||
cloud.google.com/go/trace v1.0.0 // indirect
|
cloud.google.com/go/trace v1.0.0 // indirect
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver v1.5.0 // indirect
|
github.com/Masterminds/semver v1.5.0 // indirect
|
||||||
|
github.com/amdonov/xmlsig v0.1.0 // indirect
|
||||||
|
github.com/beevik/etree v1.1.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
|
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
|
||||||
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
|
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
|
||||||
@ -135,6 +138,7 @@ require (
|
|||||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||||
github.com/jarcoal/jpath v0.0.0-20140328210829-f76b8b2dbf52
|
github.com/jarcoal/jpath v0.0.0-20140328210829-f76b8b2dbf52
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/kevinburke/go-types v0.0.0-20210723172823-2deba1f80ba7 // indirect
|
github.com/kevinburke/go-types v0.0.0-20210723172823-2deba1f80ba7 // indirect
|
||||||
github.com/kevinburke/rest v0.0.0-20210506044642-5611499aa33c // indirect
|
github.com/kevinburke/rest v0.0.0-20210506044642-5611499aa33c // indirect
|
||||||
@ -161,6 +165,7 @@ require (
|
|||||||
github.com/prometheus/common v0.26.0 // indirect
|
github.com/prometheus/common v0.26.0 // indirect
|
||||||
github.com/prometheus/procfs v0.6.0 // indirect
|
github.com/prometheus/procfs v0.6.0 // indirect
|
||||||
github.com/rs/xid v1.2.1 // indirect
|
github.com/rs/xid v1.2.1 // indirect
|
||||||
|
github.com/russellhaering/goxmldsig v1.2.0 // indirect
|
||||||
github.com/satori/go.uuid v1.2.0 // indirect
|
github.com/satori/go.uuid v1.2.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||||
github.com/spf13/afero v1.8.1 // indirect
|
github.com/spf13/afero v1.8.1 // indirect
|
||||||
@ -184,7 +189,7 @@ require (
|
|||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
gopkg.in/ini.v1 v1.66.4 // indirect
|
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
nhooyr.io/websocket v1.8.7 // indirect
|
nhooyr.io/websocket v1.8.7 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
28
go.sum
@ -95,6 +95,8 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5
|
|||||||
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
|
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
|
||||||
github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc=
|
github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc=
|
||||||
github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
|
github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
|
||||||
|
github.com/amdonov/xmlsig v0.1.0 h1:i0iQ3neKLmUhcfIRgiiR3eRPKgXZj+n5lAfqnfKoeXI=
|
||||||
|
github.com/amdonov/xmlsig v0.1.0/go.mod h1:jTR/jO0E8fSl/cLvMesP+RjxyV4Ux4WL1Ip64ZnQpA0=
|
||||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||||
@ -108,6 +110,8 @@ github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6l
|
|||||||
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
||||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
||||||
|
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||||
|
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||||
github.com/benbjohnson/clock v1.2.0 h1:9Re3G2TWxkE06LdMWMpcY6KV81GLXMGiYpPYUPkFAws=
|
github.com/benbjohnson/clock v1.2.0 h1:9Re3G2TWxkE06LdMWMpcY6KV81GLXMGiYpPYUPkFAws=
|
||||||
github.com/benbjohnson/clock v1.2.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.2.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
@ -560,6 +564,8 @@ github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
|
|||||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
|
github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
|
||||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
|
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
|
||||||
|
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
@ -600,8 +606,9 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
|||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
@ -735,6 +742,7 @@ github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko
|
|||||||
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
||||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
@ -785,6 +793,9 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn
|
|||||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||||
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||||
github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so=
|
github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so=
|
||||||
github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM=
|
github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM=
|
||||||
@ -792,6 +803,8 @@ github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
|
|||||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||||
|
github.com/russellhaering/goxmldsig v1.2.0 h1:Y6GTTc9Un5hCxSzVz4UIWQ/zuVwDvzJk80guqzwx6Vg=
|
||||||
|
github.com/russellhaering/goxmldsig v1.2.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
@ -845,14 +858,16 @@ github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5J
|
|||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 h1:1SWXcTphBQjYGWRRxLFIAR1LVtQEj4eR7xPtyeOVM/c=
|
github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 h1:1SWXcTphBQjYGWRRxLFIAR1LVtQEj4eR7xPtyeOVM/c=
|
||||||
@ -886,6 +901,12 @@ github.com/zitadel/logging v0.3.4 h1:9hZsTjMMTE3X2LUi0xcF9Q9EdLo+FAezeu52ireBbHM
|
|||||||
github.com/zitadel/logging v0.3.4/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0=
|
github.com/zitadel/logging v0.3.4/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0=
|
||||||
github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.5 h1:dP+6SheVtpF4T/oql6mJoqou8jlW3J/9NCTYnEpKgpM=
|
github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.5 h1:dP+6SheVtpF4T/oql6mJoqou8jlW3J/9NCTYnEpKgpM=
|
||||||
github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.5/go.mod h1:uoJw5Xc6HXfnQbNZiLbld9dED0/8UMu0M4gOipTRZBA=
|
github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.5/go.mod h1:uoJw5Xc6HXfnQbNZiLbld9dED0/8UMu0M4gOipTRZBA=
|
||||||
|
github.com/zitadel/saml v0.0.4 h1:xz97KyKD3mrQcIEKi0aUPyNX834xm3p1ToO9HK/vVeY=
|
||||||
|
github.com/zitadel/saml v0.0.4/go.mod h1:DIy/ln32rNYv/bIBA8uOB6Y2JmxjZldDYBeMNn7YyeQ=
|
||||||
|
github.com/zitadel/saml v0.0.5 h1:ufLE0MeWe2SLGGkSWbY3J20xxYtAL57IddeDsyMqCuM=
|
||||||
|
github.com/zitadel/saml v0.0.5/go.mod h1:DIy/ln32rNYv/bIBA8uOB6Y2JmxjZldDYBeMNn7YyeQ=
|
||||||
|
github.com/zitadel/saml v0.0.6 h1:avcOanSNd0x4jkn/+MsXIGQpJOvwqkk+slGJVIhqk84=
|
||||||
|
github.com/zitadel/saml v0.0.6/go.mod h1:DIy/ln32rNYv/bIBA8uOB6Y2JmxjZldDYBeMNn7YyeQ=
|
||||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
||||||
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
||||||
@ -1479,8 +1500,9 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg=
|
gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg=
|
||||||
gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||||
gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||||
|
@ -67,6 +67,16 @@ func (s *Server) AddOIDCApp(ctx context.Context, req *mgmt_pb.AddOIDCAppRequest)
|
|||||||
ComplianceProblems: project_grpc.ComplianceProblemsToLocalizedMessages(app.Compliance.Problems),
|
ComplianceProblems: project_grpc.ComplianceProblemsToLocalizedMessages(app.Compliance.Problems),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
func (s *Server) AddSAMLApp(ctx context.Context, req *mgmt_pb.AddSAMLAppRequest) (*mgmt_pb.AddSAMLAppResponse, error) {
|
||||||
|
app, err := s.command.AddSAMLApplication(ctx, AddSAMLAppRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &mgmt_pb.AddSAMLAppResponse{
|
||||||
|
AppId: app.AppID,
|
||||||
|
Details: object_grpc.AddToDetailsPb(app.Sequence, app.ChangeDate, app.ResourceOwner),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) AddAPIApp(ctx context.Context, req *mgmt_pb.AddAPIAppRequest) (*mgmt_pb.AddAPIAppResponse, error) {
|
func (s *Server) AddAPIApp(ctx context.Context, req *mgmt_pb.AddAPIAppRequest) (*mgmt_pb.AddAPIAppResponse, error) {
|
||||||
appSecretGenerator, err := s.query.InitHashGenerator(ctx, domain.SecretGeneratorTypeAppSecret, s.passwordHashAlg)
|
appSecretGenerator, err := s.query.InitHashGenerator(ctx, domain.SecretGeneratorTypeAppSecret, s.passwordHashAlg)
|
||||||
@ -109,6 +119,20 @@ func (s *Server) UpdateOIDCAppConfig(ctx context.Context, req *mgmt_pb.UpdateOID
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) UpdateSAMLAppConfig(ctx context.Context, req *mgmt_pb.UpdateSAMLAppConfigRequest) (*mgmt_pb.UpdateSAMLAppConfigResponse, error) {
|
||||||
|
config, err := s.command.ChangeSAMLApplication(ctx, UpdateSAMLAppConfigRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &mgmt_pb.UpdateSAMLAppConfigResponse{
|
||||||
|
Details: object_grpc.ChangeToDetailsPb(
|
||||||
|
config.Sequence,
|
||||||
|
config.ChangeDate,
|
||||||
|
config.ResourceOwner,
|
||||||
|
),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) UpdateAPIAppConfig(ctx context.Context, req *mgmt_pb.UpdateAPIAppConfigRequest) (*mgmt_pb.UpdateAPIAppConfigResponse, error) {
|
func (s *Server) UpdateAPIAppConfig(ctx context.Context, req *mgmt_pb.UpdateAPIAppConfigRequest) (*mgmt_pb.UpdateAPIAppConfigResponse, error) {
|
||||||
config, err := s.command.ChangeAPIApplication(ctx, UpdateAPIAppConfigRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
|
config, err := s.command.ChangeAPIApplication(ctx, UpdateAPIAppConfigRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -59,6 +59,17 @@ func AddOIDCAppRequestToDomain(req *mgmt_pb.AddOIDCAppRequest) *domain.OIDCApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AddSAMLAppRequestToDomain(req *mgmt_pb.AddSAMLAppRequest) *domain.SAMLApp {
|
||||||
|
return &domain.SAMLApp{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: req.ProjectId,
|
||||||
|
},
|
||||||
|
AppName: req.Name,
|
||||||
|
Metadata: req.GetMetadataXml(),
|
||||||
|
MetadataURL: req.GetMetadataUrl(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func AddAPIAppRequestToDomain(app *mgmt_pb.AddAPIAppRequest) *domain.APIApp {
|
func AddAPIAppRequestToDomain(app *mgmt_pb.AddAPIAppRequest) *domain.APIApp {
|
||||||
return &domain.APIApp{
|
return &domain.APIApp{
|
||||||
ObjectRoot: models.ObjectRoot{
|
ObjectRoot: models.ObjectRoot{
|
||||||
@ -98,6 +109,17 @@ func UpdateOIDCAppConfigRequestToDomain(app *mgmt_pb.UpdateOIDCAppConfigRequest)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UpdateSAMLAppConfigRequestToDomain(app *mgmt_pb.UpdateSAMLAppConfigRequest) *domain.SAMLApp {
|
||||||
|
return &domain.SAMLApp{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: app.ProjectId,
|
||||||
|
},
|
||||||
|
AppID: app.AppId,
|
||||||
|
Metadata: app.GetMetadataXml(),
|
||||||
|
MetadataURL: app.GetMetadataUrl(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func UpdateAPIAppConfigRequestToDomain(app *mgmt_pb.UpdateAPIAppConfigRequest) *domain.APIApp {
|
func UpdateAPIAppConfigRequestToDomain(app *mgmt_pb.UpdateAPIAppConfigRequest) *domain.APIApp {
|
||||||
return &domain.APIApp{
|
return &domain.APIApp{
|
||||||
ObjectRoot: models.ObjectRoot{
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
@ -33,6 +33,9 @@ func AppConfigToPb(app *query.App) app_pb.AppConfig {
|
|||||||
if app.OIDCConfig != nil {
|
if app.OIDCConfig != nil {
|
||||||
return AppOIDCConfigToPb(app.OIDCConfig)
|
return AppOIDCConfigToPb(app.OIDCConfig)
|
||||||
}
|
}
|
||||||
|
if app.SAMLConfig != nil {
|
||||||
|
return AppSAMLConfigToPb(app.SAMLConfig)
|
||||||
|
}
|
||||||
return AppAPIConfigToPb(app.APIConfig)
|
return AppAPIConfigToPb(app.APIConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,6 +64,14 @@ func AppOIDCConfigToPb(app *query.OIDCApp) *app_pb.App_OidcConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AppSAMLConfigToPb(app *query.SAMLApp) app_pb.AppConfig {
|
||||||
|
return &app_pb.App_SamlConfig{
|
||||||
|
SamlConfig: &app_pb.SAMLConfig{
|
||||||
|
Metadata: &app_pb.SAMLConfig_MetadataXml{MetadataXml: app.Metadata},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func AppAPIConfigToPb(app *query.APIApp) app_pb.AppConfig {
|
func AppAPIConfigToPb(app *query.APIApp) app_pb.AppConfig {
|
||||||
return &app_pb.App_ApiConfig{
|
return &app_pb.App_ApiConfig{
|
||||||
ApiConfig: &app_pb.APIConfig{
|
ApiConfig: &app_pb.APIConfig{
|
||||||
|
99
internal/api/saml/auth_request_converter.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package saml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zitadel/saml/pkg/provider/models"
|
||||||
|
"github.com/zitadel/saml/pkg/provider/xml/samlp"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ models.AuthRequestInt = &AuthRequest{}
|
||||||
|
|
||||||
|
type AuthRequest struct {
|
||||||
|
*domain.AuthRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthRequest) GetApplicationID() string {
|
||||||
|
return a.ApplicationID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthRequest) GetID() string {
|
||||||
|
return a.ID
|
||||||
|
}
|
||||||
|
func (a *AuthRequest) GetRelayState() string {
|
||||||
|
return a.TransferState
|
||||||
|
}
|
||||||
|
func (a *AuthRequest) GetAccessConsumerServiceURL() string {
|
||||||
|
return a.CallbackURI
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthRequest) GetNameID() string {
|
||||||
|
return a.UserName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthRequest) saml() *domain.AuthRequestSAML {
|
||||||
|
return a.Request.(*domain.AuthRequestSAML)
|
||||||
|
}
|
||||||
|
func (a *AuthRequest) GetAuthRequestID() string {
|
||||||
|
return a.saml().ID
|
||||||
|
}
|
||||||
|
func (a *AuthRequest) GetBindingType() string {
|
||||||
|
return a.saml().BindingType
|
||||||
|
}
|
||||||
|
func (a *AuthRequest) GetIssuer() string {
|
||||||
|
return a.saml().Issuer
|
||||||
|
}
|
||||||
|
func (a *AuthRequest) GetIssuerName() string {
|
||||||
|
return a.saml().IssuerName
|
||||||
|
}
|
||||||
|
func (a *AuthRequest) GetDestination() string {
|
||||||
|
return a.saml().Destination
|
||||||
|
}
|
||||||
|
func (a *AuthRequest) GetCode() string {
|
||||||
|
return a.saml().Code
|
||||||
|
}
|
||||||
|
func (a *AuthRequest) GetUserID() string {
|
||||||
|
return a.UserID
|
||||||
|
}
|
||||||
|
func (a *AuthRequest) GetUserName() string {
|
||||||
|
return a.UserName
|
||||||
|
}
|
||||||
|
func (a *AuthRequest) Done() bool {
|
||||||
|
for _, step := range a.PossibleSteps {
|
||||||
|
if step.Type() == domain.NextStepRedirectToCallback {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthRequestFromBusiness(authReq *domain.AuthRequest) (_ models.AuthRequestInt, err error) {
|
||||||
|
if _, ok := authReq.Request.(*domain.AuthRequestSAML); !ok {
|
||||||
|
return nil, errors.ThrowInvalidArgument(nil, "SAML-Hbz7A", "auth request is not of type saml")
|
||||||
|
}
|
||||||
|
return &AuthRequest{authReq}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateAuthRequestToBusiness(ctx context.Context, authReq *samlp.AuthnRequestType, acsUrl, protocolBinding, applicationID, relayState, userAgentID string) *domain.AuthRequest {
|
||||||
|
return &domain.AuthRequest{
|
||||||
|
CreationDate: time.Now(),
|
||||||
|
AgentID: userAgentID,
|
||||||
|
ApplicationID: applicationID,
|
||||||
|
CallbackURI: acsUrl,
|
||||||
|
TransferState: relayState,
|
||||||
|
InstanceID: authz.GetInstance(ctx).InstanceID(),
|
||||||
|
Request: &domain.AuthRequestSAML{
|
||||||
|
ID: authReq.Id,
|
||||||
|
BindingType: protocolBinding,
|
||||||
|
Code: "",
|
||||||
|
Issuer: authReq.Issuer.Text,
|
||||||
|
IssuerName: authReq.Issuer.SPProvidedID,
|
||||||
|
Destination: authReq.Destination,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
202
internal/api/saml/certificate.go
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
package saml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zitadel/logging"
|
||||||
|
"github.com/zitadel/saml/pkg/provider/key"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/errors"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/keypair"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
locksTable = "projections.locks"
|
||||||
|
signingKey = "signing_key"
|
||||||
|
samlUser = "SAML"
|
||||||
|
|
||||||
|
retryBackoff = 500 * time.Millisecond
|
||||||
|
retryCount = 3
|
||||||
|
lockDuration = retryCount * retryBackoff * 5
|
||||||
|
gracefulPeriod = 10 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
type CertificateAndKey struct {
|
||||||
|
algorithm jose.SignatureAlgorithm
|
||||||
|
id string
|
||||||
|
key interface{}
|
||||||
|
certificate interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CertificateAndKey) SignatureAlgorithm() jose.SignatureAlgorithm {
|
||||||
|
return c.algorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CertificateAndKey) Key() interface{} {
|
||||||
|
return c.key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CertificateAndKey) Certificate() interface{} {
|
||||||
|
return c.certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CertificateAndKey) ID() string {
|
||||||
|
return c.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Storage) GetCertificateAndKey(ctx context.Context, usage domain.KeyUsage) (certAndKey *key.CertificateAndKey, err error) {
|
||||||
|
err = retry(func() error {
|
||||||
|
certAndKey, err = p.getCertificateAndKey(ctx, usage)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if certAndKey == nil {
|
||||||
|
return errors.ThrowInternal(err, "SAML-8u01nks", "no certificate found")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return certAndKey, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Storage) getCertificateAndKey(ctx context.Context, usage domain.KeyUsage) (*key.CertificateAndKey, error) {
|
||||||
|
certs, err := p.query.ActiveCertificates(ctx, time.Now().Add(gracefulPeriod), usage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(certs.Certificates) > 0 {
|
||||||
|
return p.certificateToCertificateAndKey(selectCertificate(certs.Certificates))
|
||||||
|
}
|
||||||
|
|
||||||
|
var sequence uint64
|
||||||
|
if certs.LatestSequence != nil {
|
||||||
|
sequence = certs.LatestSequence.Sequence
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, p.refreshCertificate(ctx, usage, sequence)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Storage) refreshCertificate(
|
||||||
|
ctx context.Context,
|
||||||
|
usage domain.KeyUsage,
|
||||||
|
sequence uint64,
|
||||||
|
) error {
|
||||||
|
ok, err := p.ensureIsLatestCertificate(ctx, sequence)
|
||||||
|
if err != nil {
|
||||||
|
logging.WithError(err).Error("could not ensure latest key")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
logging.Warn("view not up to date, retrying later")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = p.lockAndGenerateCertificateAndKey(ctx, usage, sequence)
|
||||||
|
logging.OnError(err).Warn("could not create signing key")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Storage) ensureIsLatestCertificate(ctx context.Context, sequence uint64) (bool, error) {
|
||||||
|
maxSequence, err := p.getMaxKeySequence(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("error retrieving new events: %w", err)
|
||||||
|
}
|
||||||
|
return sequence == maxSequence, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage domain.KeyUsage, sequence uint64) error {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
ctx = setSAMLCtx(ctx)
|
||||||
|
|
||||||
|
errs := p.locker.Lock(ctx, lockDuration, authz.GetInstance(ctx).InstanceID())
|
||||||
|
err, ok := <-errs
|
||||||
|
if err != nil || !ok {
|
||||||
|
if errors.IsErrorAlreadyExists(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
logging.OnError(err).Warn("initial lock failed")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch usage {
|
||||||
|
case domain.KeyUsageSAMLMetadataSigning, domain.KeyUsageSAMLResponseSinging:
|
||||||
|
certAndKey, err := p.GetCertificateAndKey(ctx, domain.KeyUsageSAMLCA)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while reading ca certificate: %w", err)
|
||||||
|
}
|
||||||
|
if certAndKey.Key == nil || certAndKey.Certificate == nil {
|
||||||
|
return fmt.Errorf("has no ca certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch usage {
|
||||||
|
case domain.KeyUsageSAMLMetadataSigning:
|
||||||
|
return p.command.GenerateSAMLMetadataCertificate(setSAMLCtx(ctx), p.certificateAlgorithm, certAndKey.Key, certAndKey.Certificate)
|
||||||
|
case domain.KeyUsageSAMLResponseSinging:
|
||||||
|
return p.command.GenerateSAMLResponseCertificate(setSAMLCtx(ctx), p.certificateAlgorithm, certAndKey.Key, certAndKey.Certificate)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown usage")
|
||||||
|
}
|
||||||
|
case domain.KeyUsageSAMLCA:
|
||||||
|
return p.command.GenerateSAMLCACertificate(setSAMLCtx(ctx), p.certificateAlgorithm)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown certificate usage")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Storage) getMaxKeySequence(ctx context.Context) (uint64, error) {
|
||||||
|
return p.eventstore.LatestSequence(ctx,
|
||||||
|
eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence).
|
||||||
|
ResourceOwner(authz.GetInstance(ctx).InstanceID()).
|
||||||
|
AddQuery().
|
||||||
|
AggregateTypes(keypair.AggregateType).
|
||||||
|
Builder(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Storage) certificateToCertificateAndKey(certificate query.Certificate) (_ *key.CertificateAndKey, err error) {
|
||||||
|
keyData, err := crypto.Decrypt(certificate.Key(), p.encAlg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
privateKey, err := crypto.BytesToPrivateKey(keyData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := crypto.BytesToCertificate(certificate.Certificate())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &key.CertificateAndKey{
|
||||||
|
Key: privateKey,
|
||||||
|
Certificate: cert,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectCertificate(certs []query.Certificate) query.Certificate {
|
||||||
|
return certs[len(certs)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSAMLCtx(ctx context.Context) context.Context {
|
||||||
|
return authz.SetCtxData(ctx, authz.CtxData{UserID: samlUser, OrgID: authz.GetInstance(ctx).InstanceID()})
|
||||||
|
}
|
||||||
|
|
||||||
|
func retry(retryable func() error) (err error) {
|
||||||
|
for i := 0; i < retryCount; i++ {
|
||||||
|
err = retryable()
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
time.Sleep(retryBackoff)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
102
internal/api/saml/provider.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package saml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zitadel/saml/pkg/provider"
|
||||||
|
|
||||||
|
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||||
|
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||||
|
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||||
|
"github.com/zitadel/zitadel/internal/auth/repository"
|
||||||
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
|
||||||
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
|
"github.com/zitadel/zitadel/internal/telemetry/metrics"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
HandlerPrefix = "/saml/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ProviderConfig *provider.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProvider(
|
||||||
|
ctx context.Context,
|
||||||
|
conf Config,
|
||||||
|
externalSecure bool,
|
||||||
|
command *command.Commands,
|
||||||
|
query *query.Queries,
|
||||||
|
repo repository.Repository,
|
||||||
|
encAlg crypto.EncryptionAlgorithm,
|
||||||
|
certEncAlg crypto.EncryptionAlgorithm,
|
||||||
|
es *eventstore.Eventstore,
|
||||||
|
projections *sql.DB,
|
||||||
|
instanceHandler,
|
||||||
|
userAgentCookie func(http.Handler) http.Handler,
|
||||||
|
) (*provider.Provider, error) {
|
||||||
|
metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount}
|
||||||
|
|
||||||
|
provStorage, err := newStorage(
|
||||||
|
command,
|
||||||
|
query,
|
||||||
|
repo,
|
||||||
|
encAlg,
|
||||||
|
certEncAlg,
|
||||||
|
es,
|
||||||
|
projections,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
options := []provider.Option{
|
||||||
|
provider.WithHttpInterceptors(
|
||||||
|
middleware.MetricsHandler(metricTypes),
|
||||||
|
middleware.TelemetryHandler(),
|
||||||
|
middleware.NoCacheInterceptor().Handler,
|
||||||
|
instanceHandler,
|
||||||
|
userAgentCookie,
|
||||||
|
http_utils.CopyHeadersToContext,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if !externalSecure {
|
||||||
|
options = append(options, provider.WithAllowInsecure())
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider.NewProvider(
|
||||||
|
ctx,
|
||||||
|
provStorage,
|
||||||
|
HandlerPrefix,
|
||||||
|
conf.ProviderConfig,
|
||||||
|
options...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStorage(
|
||||||
|
command *command.Commands,
|
||||||
|
query *query.Queries,
|
||||||
|
repo repository.Repository,
|
||||||
|
encAlg crypto.EncryptionAlgorithm,
|
||||||
|
certEncAlg crypto.EncryptionAlgorithm,
|
||||||
|
es *eventstore.Eventstore,
|
||||||
|
projections *sql.DB,
|
||||||
|
) (*Storage, error) {
|
||||||
|
return &Storage{
|
||||||
|
encAlg: encAlg,
|
||||||
|
certEncAlg: certEncAlg,
|
||||||
|
locker: crdb.NewLocker(projections, locksTable, signingKey),
|
||||||
|
eventstore: es,
|
||||||
|
repo: repo,
|
||||||
|
command: command,
|
||||||
|
query: query,
|
||||||
|
defaultLoginURL: fmt.Sprintf("%s%s?%s=", login.HandlerPrefix, login.EndpointLogin, login.QueryAuthRequestID),
|
||||||
|
}, nil
|
||||||
|
}
|
187
internal/api/saml/storage.go
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
package saml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zitadel/saml/pkg/provider"
|
||||||
|
"github.com/zitadel/saml/pkg/provider/key"
|
||||||
|
"github.com/zitadel/saml/pkg/provider/models"
|
||||||
|
"github.com/zitadel/saml/pkg/provider/serviceprovider"
|
||||||
|
"github.com/zitadel/saml/pkg/provider/xml/samlp"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||||
|
"github.com/zitadel/zitadel/internal/auth/repository"
|
||||||
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/errors"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
|
||||||
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ provider.EntityStorage = &Storage{}
|
||||||
|
var _ provider.IdentityProviderStorage = &Storage{}
|
||||||
|
var _ provider.AuthStorage = &Storage{}
|
||||||
|
var _ provider.UserStorage = &Storage{}
|
||||||
|
|
||||||
|
type Storage struct {
|
||||||
|
certChan <-chan interface{}
|
||||||
|
defaultCertificateLifetime time.Duration
|
||||||
|
|
||||||
|
currentCACertificate query.Certificate
|
||||||
|
currentMetadataCertificate query.Certificate
|
||||||
|
currentResponseCertificate query.Certificate
|
||||||
|
|
||||||
|
locker crdb.Locker
|
||||||
|
certificateAlgorithm string
|
||||||
|
encAlg crypto.EncryptionAlgorithm
|
||||||
|
certEncAlg crypto.EncryptionAlgorithm
|
||||||
|
|
||||||
|
eventstore *eventstore.Eventstore
|
||||||
|
repo repository.Repository
|
||||||
|
command *command.Commands
|
||||||
|
query *query.Queries
|
||||||
|
|
||||||
|
defaultLoginURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Storage) GetEntityByID(ctx context.Context, entityID string) (*serviceprovider.ServiceProvider, error) {
|
||||||
|
app, err := p.query.AppBySAMLEntityID(ctx, entityID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return serviceprovider.NewServiceProvider(
|
||||||
|
app.ID,
|
||||||
|
&serviceprovider.Config{
|
||||||
|
Metadata: app.SAMLConfig.Metadata,
|
||||||
|
},
|
||||||
|
p.defaultLoginURL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Storage) GetEntityIDByAppID(ctx context.Context, appID string) (string, error) {
|
||||||
|
app, err := p.query.AppByID(ctx, appID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return app.SAMLConfig.EntityID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Storage) Health(context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Storage) GetCA(ctx context.Context) (*key.CertificateAndKey, error) {
|
||||||
|
return p.GetCertificateAndKey(ctx, domain.KeyUsageSAMLCA)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Storage) GetMetadataSigningKey(ctx context.Context) (*key.CertificateAndKey, error) {
|
||||||
|
return p.GetCertificateAndKey(ctx, domain.KeyUsageSAMLMetadataSigning)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Storage) GetResponseSigningKey(ctx context.Context) (*key.CertificateAndKey, error) {
|
||||||
|
return p.GetCertificateAndKey(ctx, domain.KeyUsageSAMLResponseSinging)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Storage) CreateAuthRequest(ctx context.Context, req *samlp.AuthnRequestType, acsUrl, protocolBinding, relayState, applicationID string) (_ models.AuthRequestInt, err error) {
|
||||||
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
|
defer func() { span.EndWithError(err) }()
|
||||||
|
userAgentID, ok := middleware.UserAgentIDFromCtx(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.ThrowPreconditionFailed(nil, "SAML-sd436", "no user agent id")
|
||||||
|
}
|
||||||
|
|
||||||
|
authRequest := CreateAuthRequestToBusiness(ctx, req, acsUrl, protocolBinding, applicationID, relayState, userAgentID)
|
||||||
|
|
||||||
|
resp, err := p.repo.CreateAuthRequest(ctx, authRequest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthRequestFromBusiness(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Storage) AuthRequestByID(ctx context.Context, id string) (_ models.AuthRequestInt, err error) {
|
||||||
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
|
defer func() { span.EndWithError(err) }()
|
||||||
|
userAgentID, ok := middleware.UserAgentIDFromCtx(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.ThrowPreconditionFailed(nil, "SAML-D3g21", "no user agent id")
|
||||||
|
}
|
||||||
|
resp, err := p.repo.AuthRequestByIDCheckLoggedIn(ctx, id, userAgentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return AuthRequestFromBusiness(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Storage) SetUserinfoWithUserID(ctx context.Context, userinfo models.AttributeSetter, userID string, attributes []int) (err error) {
|
||||||
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
|
defer func() { span.EndWithError(err) }()
|
||||||
|
user, err := p.query.GetUserByID(ctx, true, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserinfo(user, userinfo, attributes)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Storage) SetUserinfoWithLoginName(ctx context.Context, userinfo models.AttributeSetter, loginName string, attributes []int) (err error) {
|
||||||
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
|
loginNameSQ, err := query.NewUserLoginNamesSearchQuery(loginName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user, err := p.query.GetUser(ctx, true, loginNameSQ)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserinfo(user, userinfo, attributes)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUserinfo(user *query.User, userinfo models.AttributeSetter, attributes []int) {
|
||||||
|
if len(attributes) == 0 {
|
||||||
|
userinfo.SetUsername(user.PreferredLoginName)
|
||||||
|
userinfo.SetUserID(user.ID)
|
||||||
|
if user.Human == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userinfo.SetEmail(user.Human.Email)
|
||||||
|
userinfo.SetSurname(user.Human.LastName)
|
||||||
|
userinfo.SetGivenName(user.Human.FirstName)
|
||||||
|
userinfo.SetFullName(user.Human.DisplayName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, attribute := range attributes {
|
||||||
|
switch attribute {
|
||||||
|
case provider.AttributeEmail:
|
||||||
|
if user.Human != nil {
|
||||||
|
userinfo.SetEmail(user.Human.Email)
|
||||||
|
}
|
||||||
|
case provider.AttributeSurname:
|
||||||
|
if user.Human != nil {
|
||||||
|
userinfo.SetSurname(user.Human.LastName)
|
||||||
|
}
|
||||||
|
case provider.AttributeFullName:
|
||||||
|
if user.Human != nil {
|
||||||
|
userinfo.SetFullName(user.Human.DisplayName)
|
||||||
|
}
|
||||||
|
case provider.AttributeGivenName:
|
||||||
|
if user.Human != nil {
|
||||||
|
userinfo.SetGivenName(user.Human.FirstName)
|
||||||
|
}
|
||||||
|
case provider.AttributeUsername:
|
||||||
|
userinfo.SetUsername(user.PreferredLoginName)
|
||||||
|
case provider.AttributeUserID:
|
||||||
|
userinfo.SetUserID(user.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -37,6 +37,7 @@ type Login struct {
|
|||||||
externalSecure bool
|
externalSecure bool
|
||||||
consolePath string
|
consolePath string
|
||||||
oidcAuthCallbackURL func(context.Context, string) string
|
oidcAuthCallbackURL func(context.Context, string) string
|
||||||
|
samlAuthCallbackURL func(context.Context, string) string
|
||||||
idpConfigAlg crypto.EncryptionAlgorithm
|
idpConfigAlg crypto.EncryptionAlgorithm
|
||||||
userCodeAlg crypto.EncryptionAlgorithm
|
userCodeAlg crypto.EncryptionAlgorithm
|
||||||
}
|
}
|
||||||
@ -61,10 +62,12 @@ func CreateLogin(config Config,
|
|||||||
staticStorage static.Storage,
|
staticStorage static.Storage,
|
||||||
consolePath string,
|
consolePath string,
|
||||||
oidcAuthCallbackURL func(context.Context, string) string,
|
oidcAuthCallbackURL func(context.Context, string) string,
|
||||||
|
samlAuthCallbackURL func(context.Context, string) string,
|
||||||
externalSecure bool,
|
externalSecure bool,
|
||||||
userAgentCookie,
|
userAgentCookie,
|
||||||
issuerInterceptor,
|
issuerInterceptor,
|
||||||
instanceHandler,
|
oidcInstanceHandler,
|
||||||
|
samlInstanceHandler mux.MiddlewareFunc,
|
||||||
assetCache mux.MiddlewareFunc,
|
assetCache mux.MiddlewareFunc,
|
||||||
userCodeAlg crypto.EncryptionAlgorithm,
|
userCodeAlg crypto.EncryptionAlgorithm,
|
||||||
idpConfigAlg crypto.EncryptionAlgorithm,
|
idpConfigAlg crypto.EncryptionAlgorithm,
|
||||||
@ -73,6 +76,7 @@ func CreateLogin(config Config,
|
|||||||
|
|
||||||
login := &Login{
|
login := &Login{
|
||||||
oidcAuthCallbackURL: oidcAuthCallbackURL,
|
oidcAuthCallbackURL: oidcAuthCallbackURL,
|
||||||
|
samlAuthCallbackURL: samlAuthCallbackURL,
|
||||||
externalSecure: externalSecure,
|
externalSecure: externalSecure,
|
||||||
consolePath: consolePath,
|
consolePath: consolePath,
|
||||||
command: command,
|
command: command,
|
||||||
@ -91,7 +95,7 @@ func CreateLogin(config Config,
|
|||||||
cacheInterceptor := createCacheInterceptor(config.Cache.MaxAge, config.Cache.SharedMaxAge, assetCache)
|
cacheInterceptor := createCacheInterceptor(config.Cache.MaxAge, config.Cache.SharedMaxAge, assetCache)
|
||||||
security := middleware.SecurityHeaders(csp(), login.cspErrorHandler)
|
security := middleware.SecurityHeaders(csp(), login.cspErrorHandler)
|
||||||
|
|
||||||
login.router = CreateRouter(login, statikFS, middleware.TelemetryHandler(IgnoreInstanceEndpoints...), instanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, issuerInterceptor)
|
login.router = CreateRouter(login, statikFS, middleware.TelemetryHandler(IgnoreInstanceEndpoints...), oidcInstanceHandler, samlInstanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, issuerInterceptor)
|
||||||
login.renderer = CreateRenderer(HandlerPrefix, statikFS, staticStorage, config.LanguageCookieName)
|
login.renderer = CreateRenderer(HandlerPrefix, statikFS, staticStorage, config.LanguageCookieName)
|
||||||
login.parser = form.NewParser()
|
login.parser = form.NewParser()
|
||||||
return login, nil
|
return login, nil
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -43,11 +44,26 @@ func (l *Login) renderSuccessAndCallback(w http.ResponseWriter, r *http.Request,
|
|||||||
userData: l.getUserData(r, authReq, "Login Successful", errID, errMessage),
|
userData: l.getUserData(r, authReq, "Login Successful", errID, errMessage),
|
||||||
}
|
}
|
||||||
if authReq != nil {
|
if authReq != nil {
|
||||||
data.RedirectURI = l.oidcAuthCallbackURL(r.Context(), "") //the id will be set via the html (maybe change this with the login refactoring)
|
//the id will be set via the html (maybe change this with the login refactoring)
|
||||||
|
if _, ok := authReq.Request.(*domain.AuthRequestOIDC); ok {
|
||||||
|
data.RedirectURI = l.oidcAuthCallbackURL(r.Context(), "")
|
||||||
|
} else if _, ok := authReq.Request.(*domain.AuthRequestSAML); ok {
|
||||||
|
data.RedirectURI = l.samlAuthCallbackURL(r.Context(), "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplLoginSuccess], data, nil)
|
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplLoginSuccess], data, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Login) redirectToCallback(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
|
func (l *Login) redirectToCallback(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
|
||||||
http.Redirect(w, r, l.oidcAuthCallbackURL(r.Context(), authReq.ID), http.StatusFound)
|
var callback string
|
||||||
|
switch authReq.Request.(type) {
|
||||||
|
case *domain.AuthRequestOIDC:
|
||||||
|
callback = l.oidcAuthCallbackURL(r.Context(), authReq.ID)
|
||||||
|
case *domain.AuthRequestSAML:
|
||||||
|
callback = l.samlAuthCallbackURL(r.Context(), authReq.ID)
|
||||||
|
default:
|
||||||
|
l.renderInternalError(w, r, authReq, caos_errs.ThrowInternal(nil, "LOGIN-rhjQF", "Errors.AuthRequest.RequestTypeNotSupported"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, callback, http.StatusFound)
|
||||||
}
|
}
|
||||||
|
@ -97,12 +97,12 @@ type orgViewProvider interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type userGrantProvider interface {
|
type userGrantProvider interface {
|
||||||
ProjectByOIDCClientID(context.Context, string) (*query.Project, error)
|
ProjectByClientID(context.Context, string) (*query.Project, error)
|
||||||
UserGrantsByProjectAndUserID(context.Context, string, string) ([]*query.UserGrant, error)
|
UserGrantsByProjectAndUserID(context.Context, string, string) ([]*query.UserGrant, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type projectProvider interface {
|
type projectProvider interface {
|
||||||
ProjectByOIDCClientID(context.Context, string) (*query.Project, error)
|
ProjectByClientID(context.Context, string) (*query.Project, error)
|
||||||
OrgProjectMappingByIDs(orgID, projectID, instanceID string) (*project_view_model.OrgProjectMapping, error)
|
OrgProjectMappingByIDs(orgID, projectID, instanceID string) (*project_view_model.OrgProjectMapping, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,7 +122,7 @@ func (repo *AuthRequestRepo) CreateAuthRequest(ctx context.Context, request *dom
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
request.ID = reqID
|
request.ID = reqID
|
||||||
project, err := repo.ProjectProvider.ProjectByOIDCClientID(ctx, request.ApplicationID)
|
project, err := repo.ProjectProvider.ProjectByClientID(ctx, request.ApplicationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1048,6 +1048,9 @@ func (repo *AuthRequestRepo) getLoginTexts(ctx context.Context, aggregateID stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (repo *AuthRequestRepo) hasSucceededPage(ctx context.Context, request *domain.AuthRequest, provider applicationProvider) (bool, error) {
|
func (repo *AuthRequestRepo) hasSucceededPage(ctx context.Context, request *domain.AuthRequest, provider applicationProvider) (bool, error) {
|
||||||
|
if _, ok := request.Request.(*domain.AuthRequestOIDC); !ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
app, err := provider.AppByOIDCClientID(ctx, request.ApplicationID)
|
app, err := provider.AppByOIDCClientID(ctx, request.ApplicationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -1257,8 +1260,8 @@ func linkingIDPConfigExistingInAllowedIDPs(linkingUsers []*domain.ExternalUser,
|
|||||||
func userGrantRequired(ctx context.Context, request *domain.AuthRequest, user *user_model.UserView, userGrantProvider userGrantProvider) (_ bool, err error) {
|
func userGrantRequired(ctx context.Context, request *domain.AuthRequest, user *user_model.UserView, userGrantProvider userGrantProvider) (_ bool, err error) {
|
||||||
var project *query.Project
|
var project *query.Project
|
||||||
switch request.Request.Type() {
|
switch request.Request.Type() {
|
||||||
case domain.AuthRequestTypeOIDC:
|
case domain.AuthRequestTypeOIDC, domain.AuthRequestTypeSAML:
|
||||||
project, err = userGrantProvider.ProjectByOIDCClientID(ctx, request.ApplicationID)
|
project, err = userGrantProvider.ProjectByClientID(ctx, request.ApplicationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -1278,8 +1281,8 @@ func userGrantRequired(ctx context.Context, request *domain.AuthRequest, user *u
|
|||||||
func projectRequired(ctx context.Context, request *domain.AuthRequest, projectProvider projectProvider) (_ bool, err error) {
|
func projectRequired(ctx context.Context, request *domain.AuthRequest, projectProvider projectProvider) (_ bool, err error) {
|
||||||
var project *query.Project
|
var project *query.Project
|
||||||
switch request.Request.Type() {
|
switch request.Request.Type() {
|
||||||
case domain.AuthRequestTypeOIDC:
|
case domain.AuthRequestTypeOIDC, domain.AuthRequestTypeSAML:
|
||||||
project, err = projectProvider.ProjectByOIDCClientID(ctx, request.ApplicationID)
|
project, err = projectProvider.ProjectByClientID(ctx, request.ApplicationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,10 @@ import (
|
|||||||
user_view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
|
user_view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testNow = time.Now()
|
||||||
|
)
|
||||||
|
|
||||||
type mockViewNoUserSession struct{}
|
type mockViewNoUserSession struct{}
|
||||||
|
|
||||||
func (m *mockViewNoUserSession) UserSessionByIDs(string, string, string) (*user_view_model.UserSessionView, error) {
|
func (m *mockViewNoUserSession) UserSessionByIDs(string, string, string) (*user_view_model.UserSessionView, error) {
|
||||||
@ -191,7 +195,7 @@ type mockUserGrants struct {
|
|||||||
userGrants int
|
userGrants int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockUserGrants) ProjectByOIDCClientID(ctx context.Context, s string) (*query.Project, error) {
|
func (m *mockUserGrants) ProjectByClientID(ctx context.Context, s string) (*query.Project, error) {
|
||||||
return &query.Project{ProjectRoleCheck: m.roleCheck}, nil
|
return &query.Project{ProjectRoleCheck: m.roleCheck}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,7 +212,7 @@ type mockProject struct {
|
|||||||
projectCheck bool
|
projectCheck bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockProject) ProjectByOIDCClientID(ctx context.Context, s string) (*query.Project, error) {
|
func (m *mockProject) ProjectByClientID(ctx context.Context, s string) (*query.Project, error) {
|
||||||
return &query.Project{HasProjectCheck: m.projectCheck}, nil
|
return &query.Project{HasProjectCheck: m.projectCheck}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -615,8 +619,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
|||||||
"passwordless verified, email not verified, email verification step",
|
"passwordless verified, email not verified, email verification step",
|
||||||
fields{
|
fields{
|
||||||
userSessionViewProvider: &mockViewUserSession{
|
userSessionViewProvider: &mockViewUserSession{
|
||||||
PasswordlessVerification: time.Now().Add(-5 * time.Minute),
|
PasswordlessVerification: testNow.Add(-5 * time.Minute),
|
||||||
MultiFactorVerification: time.Now().Add(-5 * time.Minute),
|
MultiFactorVerification: testNow.Add(-5 * time.Minute),
|
||||||
},
|
},
|
||||||
userViewProvider: &mockViewUser{
|
userViewProvider: &mockViewUser{
|
||||||
PasswordSet: true,
|
PasswordSet: true,
|
||||||
@ -667,7 +671,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
|||||||
"external user (no external verification), external login step",
|
"external user (no external verification), external login step",
|
||||||
fields{
|
fields{
|
||||||
userSessionViewProvider: &mockViewUserSession{
|
userSessionViewProvider: &mockViewUserSession{
|
||||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||||
},
|
},
|
||||||
userViewProvider: &mockViewUser{
|
userViewProvider: &mockViewUser{
|
||||||
IsEmailVerified: true,
|
IsEmailVerified: true,
|
||||||
@ -699,8 +703,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
|||||||
"external user (external verification set), callback",
|
"external user (external verification set), callback",
|
||||||
fields{
|
fields{
|
||||||
userSessionViewProvider: &mockViewUserSession{
|
userSessionViewProvider: &mockViewUserSession{
|
||||||
ExternalLoginVerification: time.Now().UTC().Add(-5 * time.Minute),
|
ExternalLoginVerification: testNow.Add(-5 * time.Minute),
|
||||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||||
},
|
},
|
||||||
userViewProvider: &mockViewUser{
|
userViewProvider: &mockViewUser{
|
||||||
IsEmailVerified: true,
|
IsEmailVerified: true,
|
||||||
@ -759,8 +763,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
|||||||
"external user (no password check needed), callback",
|
"external user (no password check needed), callback",
|
||||||
fields{
|
fields{
|
||||||
userSessionViewProvider: &mockViewUserSession{
|
userSessionViewProvider: &mockViewUserSession{
|
||||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||||
ExternalLoginVerification: time.Now().UTC().Add(-5 * time.Minute),
|
ExternalLoginVerification: testNow.Add(-5 * time.Minute),
|
||||||
},
|
},
|
||||||
userViewProvider: &mockViewUser{
|
userViewProvider: &mockViewUser{
|
||||||
PasswordSet: true,
|
PasswordSet: true,
|
||||||
@ -795,7 +799,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
|||||||
"password verified, passwordless set up, mfa not verified, mfa check step",
|
"password verified, passwordless set up, mfa not verified, mfa check step",
|
||||||
fields{
|
fields{
|
||||||
userSessionViewProvider: &mockViewUserSession{
|
userSessionViewProvider: &mockViewUserSession{
|
||||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||||
},
|
},
|
||||||
userViewProvider: &mockViewUser{
|
userViewProvider: &mockViewUser{
|
||||||
PasswordSet: true,
|
PasswordSet: true,
|
||||||
@ -829,7 +833,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
|||||||
"mfa not verified, mfa check step",
|
"mfa not verified, mfa check step",
|
||||||
fields{
|
fields{
|
||||||
userSessionViewProvider: &mockViewUserSession{
|
userSessionViewProvider: &mockViewUserSession{
|
||||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||||
},
|
},
|
||||||
userViewProvider: &mockViewUser{
|
userViewProvider: &mockViewUser{
|
||||||
PasswordSet: true,
|
PasswordSet: true,
|
||||||
@ -862,8 +866,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
|||||||
"external user, mfa not verified, mfa check step",
|
"external user, mfa not verified, mfa check step",
|
||||||
fields{
|
fields{
|
||||||
userSessionViewProvider: &mockViewUserSession{
|
userSessionViewProvider: &mockViewUserSession{
|
||||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||||
ExternalLoginVerification: time.Now().UTC().Add(-5 * time.Minute),
|
ExternalLoginVerification: testNow.Add(-5 * time.Minute),
|
||||||
},
|
},
|
||||||
userViewProvider: &mockViewUser{
|
userViewProvider: &mockViewUser{
|
||||||
PasswordSet: true,
|
PasswordSet: true,
|
||||||
@ -898,8 +902,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
|||||||
"password change required and email verified, password change step",
|
"password change required and email verified, password change step",
|
||||||
fields{
|
fields{
|
||||||
userSessionViewProvider: &mockViewUserSession{
|
userSessionViewProvider: &mockViewUserSession{
|
||||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||||
},
|
},
|
||||||
userViewProvider: &mockViewUser{
|
userViewProvider: &mockViewUser{
|
||||||
PasswordSet: true,
|
PasswordSet: true,
|
||||||
@ -931,8 +935,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
|||||||
"email not verified and no password change required, mail verification step",
|
"email not verified and no password change required, mail verification step",
|
||||||
fields{
|
fields{
|
||||||
userSessionViewProvider: &mockViewUserSession{
|
userSessionViewProvider: &mockViewUserSession{
|
||||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||||
},
|
},
|
||||||
userViewProvider: &mockViewUser{
|
userViewProvider: &mockViewUser{
|
||||||
PasswordSet: true,
|
PasswordSet: true,
|
||||||
@ -961,8 +965,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
|||||||
"email not verified and password change required, mail verification step",
|
"email not verified and password change required, mail verification step",
|
||||||
fields{
|
fields{
|
||||||
userSessionViewProvider: &mockViewUserSession{
|
userSessionViewProvider: &mockViewUserSession{
|
||||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||||
},
|
},
|
||||||
userViewProvider: &mockViewUser{
|
userViewProvider: &mockViewUser{
|
||||||
PasswordSet: true,
|
PasswordSet: true,
|
||||||
@ -992,8 +996,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
|||||||
"email verified and no password change required, redirect to callback step",
|
"email verified and no password change required, redirect to callback step",
|
||||||
fields{
|
fields{
|
||||||
userSessionViewProvider: &mockViewUserSession{
|
userSessionViewProvider: &mockViewUserSession{
|
||||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||||
},
|
},
|
||||||
userViewProvider: &mockViewUser{
|
userViewProvider: &mockViewUser{
|
||||||
PasswordSet: true,
|
PasswordSet: true,
|
||||||
@ -1027,8 +1031,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
|||||||
"prompt none, checkLoggedIn true and authenticated, redirect to callback step",
|
"prompt none, checkLoggedIn true and authenticated, redirect to callback step",
|
||||||
fields{
|
fields{
|
||||||
userSessionViewProvider: &mockViewUserSession{
|
userSessionViewProvider: &mockViewUserSession{
|
||||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||||
},
|
},
|
||||||
userViewProvider: &mockViewUser{
|
userViewProvider: &mockViewUser{
|
||||||
PasswordSet: true,
|
PasswordSet: true,
|
||||||
@ -1063,8 +1067,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
|||||||
"prompt none, checkLoggedIn true, authenticated and native, login succeeded step",
|
"prompt none, checkLoggedIn true, authenticated and native, login succeeded step",
|
||||||
fields{
|
fields{
|
||||||
userSessionViewProvider: &mockViewUserSession{
|
userSessionViewProvider: &mockViewUserSession{
|
||||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||||
},
|
},
|
||||||
userViewProvider: &mockViewUser{
|
userViewProvider: &mockViewUser{
|
||||||
PasswordSet: true,
|
PasswordSet: true,
|
||||||
@ -1099,8 +1103,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
|||||||
"prompt none, checkLoggedIn true, authenticated and required user grants missing, grant required step",
|
"prompt none, checkLoggedIn true, authenticated and required user grants missing, grant required step",
|
||||||
fields{
|
fields{
|
||||||
userSessionViewProvider: &mockViewUserSession{
|
userSessionViewProvider: &mockViewUserSession{
|
||||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||||
},
|
},
|
||||||
userViewProvider: &mockViewUser{
|
userViewProvider: &mockViewUser{
|
||||||
PasswordSet: true,
|
PasswordSet: true,
|
||||||
@ -1137,8 +1141,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
|||||||
"prompt none, checkLoggedIn true, authenticated and required user grants exist, redirect to callback step",
|
"prompt none, checkLoggedIn true, authenticated and required user grants exist, redirect to callback step",
|
||||||
fields{
|
fields{
|
||||||
userSessionViewProvider: &mockViewUserSession{
|
userSessionViewProvider: &mockViewUserSession{
|
||||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||||
},
|
},
|
||||||
userViewProvider: &mockViewUser{
|
userViewProvider: &mockViewUser{
|
||||||
PasswordSet: true,
|
PasswordSet: true,
|
||||||
@ -1176,8 +1180,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
|||||||
"prompt none, checkLoggedIn true, authenticated and required project missing, project required step",
|
"prompt none, checkLoggedIn true, authenticated and required project missing, project required step",
|
||||||
fields{
|
fields{
|
||||||
userSessionViewProvider: &mockViewUserSession{
|
userSessionViewProvider: &mockViewUserSession{
|
||||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||||
},
|
},
|
||||||
userViewProvider: &mockViewUser{
|
userViewProvider: &mockViewUser{
|
||||||
PasswordSet: true,
|
PasswordSet: true,
|
||||||
@ -1214,8 +1218,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
|||||||
"prompt none, checkLoggedIn true, authenticated and required project exist, redirect to callback step",
|
"prompt none, checkLoggedIn true, authenticated and required project exist, redirect to callback step",
|
||||||
fields{
|
fields{
|
||||||
userSessionViewProvider: &mockViewUserSession{
|
userSessionViewProvider: &mockViewUserSession{
|
||||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||||
},
|
},
|
||||||
userViewProvider: &mockViewUser{
|
userViewProvider: &mockViewUser{
|
||||||
PasswordSet: true,
|
PasswordSet: true,
|
||||||
@ -1253,7 +1257,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
|||||||
"linking users, password step",
|
"linking users, password step",
|
||||||
fields{
|
fields{
|
||||||
userSessionViewProvider: &mockViewUserSession{
|
userSessionViewProvider: &mockViewUserSession{
|
||||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||||
},
|
},
|
||||||
userViewProvider: &mockViewUser{
|
userViewProvider: &mockViewUser{
|
||||||
PasswordSet: true,
|
PasswordSet: true,
|
||||||
@ -1287,8 +1291,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
|||||||
"linking users, linking step",
|
"linking users, linking step",
|
||||||
fields{
|
fields{
|
||||||
userSessionViewProvider: &mockViewUserSession{
|
userSessionViewProvider: &mockViewUserSession{
|
||||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||||
},
|
},
|
||||||
userViewProvider: &mockViewUser{
|
userViewProvider: &mockViewUser{
|
||||||
PasswordSet: true,
|
PasswordSet: true,
|
||||||
@ -1463,7 +1467,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
|||||||
user: &user_model.UserView{
|
user: &user_model.UserView{
|
||||||
HumanView: &user_model.HumanView{
|
HumanView: &user_model.HumanView{
|
||||||
MFAMaxSetUp: domain.MFALevelNotSetUp,
|
MFAMaxSetUp: domain.MFALevelNotSetUp,
|
||||||
MFAInitSkipped: time.Now().UTC(),
|
MFAInitSkipped: testNow,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1486,7 +1490,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
|||||||
OTPState: user_model.MFAStateReady,
|
OTPState: user_model.MFAStateReady,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
userSession: &user_model.UserSessionView{SecondFactorVerification: time.Now().UTC().Add(-5 * time.Hour)},
|
userSession: &user_model.UserSessionView{SecondFactorVerification: testNow.Add(-5 * time.Hour)},
|
||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
true,
|
true,
|
||||||
@ -1569,7 +1573,7 @@ func TestAuthRequestRepo_mfaSkippedOrSetUp(t *testing.T) {
|
|||||||
user: &user_model.UserView{
|
user: &user_model.UserView{
|
||||||
HumanView: &user_model.HumanView{
|
HumanView: &user_model.HumanView{
|
||||||
MFAMaxSetUp: -1,
|
MFAMaxSetUp: -1,
|
||||||
MFAInitSkipped: time.Now().UTC().Add(-10 * time.Hour),
|
MFAInitSkipped: testNow.Add(-10 * time.Hour),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
request: &domain.AuthRequest{
|
request: &domain.AuthRequest{
|
||||||
@ -1587,7 +1591,7 @@ func TestAuthRequestRepo_mfaSkippedOrSetUp(t *testing.T) {
|
|||||||
user: &user_model.UserView{
|
user: &user_model.UserView{
|
||||||
HumanView: &user_model.HumanView{
|
HumanView: &user_model.HumanView{
|
||||||
MFAMaxSetUp: -1,
|
MFAMaxSetUp: -1,
|
||||||
MFAInitSkipped: time.Now().UTC().Add(-40 * 24 * time.Hour),
|
MFAInitSkipped: testNow.Add(-40 * 24 * time.Hour),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
request: &domain.AuthRequest{
|
request: &domain.AuthRequest{
|
||||||
@ -1645,13 +1649,13 @@ func Test_userSessionByIDs(t *testing.T) {
|
|||||||
"error user events, old view model state",
|
"error user events, old view model state",
|
||||||
args{
|
args{
|
||||||
userProvider: &mockViewUserSession{
|
userProvider: &mockViewUserSession{
|
||||||
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
|
PasswordVerification: testNow,
|
||||||
},
|
},
|
||||||
user: &user_model.UserView{ID: "id", HumanView: &user_model.HumanView{FirstName: "FirstName"}},
|
user: &user_model.UserView{ID: "id", HumanView: &user_model.HumanView{FirstName: "FirstName"}},
|
||||||
eventProvider: &mockEventErrUser{},
|
eventProvider: &mockEventErrUser{},
|
||||||
},
|
},
|
||||||
&user_model.UserSessionView{
|
&user_model.UserSessionView{
|
||||||
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
|
PasswordVerification: testNow,
|
||||||
SecondFactorVerification: time.Time{},
|
SecondFactorVerification: time.Time{},
|
||||||
MultiFactorVerification: time.Time{},
|
MultiFactorVerification: time.Time{},
|
||||||
},
|
},
|
||||||
@ -1661,7 +1665,7 @@ func Test_userSessionByIDs(t *testing.T) {
|
|||||||
"new user events but error, old view model state",
|
"new user events but error, old view model state",
|
||||||
args{
|
args{
|
||||||
userProvider: &mockViewUserSession{
|
userProvider: &mockViewUserSession{
|
||||||
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
|
PasswordVerification: testNow,
|
||||||
},
|
},
|
||||||
agentID: "agentID",
|
agentID: "agentID",
|
||||||
user: &user_model.UserView{ID: "id", HumanView: &user_model.HumanView{FirstName: "FirstName"}},
|
user: &user_model.UserView{ID: "id", HumanView: &user_model.HumanView{FirstName: "FirstName"}},
|
||||||
@ -1669,12 +1673,12 @@ func Test_userSessionByIDs(t *testing.T) {
|
|||||||
&es_models.Event{
|
&es_models.Event{
|
||||||
AggregateType: user_repo.AggregateType,
|
AggregateType: user_repo.AggregateType,
|
||||||
Type: es_models.EventType(user_repo.UserV1MFAOTPCheckSucceededType),
|
Type: es_models.EventType(user_repo.UserV1MFAOTPCheckSucceededType),
|
||||||
CreationDate: time.Now().UTC().Round(1 * time.Second),
|
CreationDate: testNow,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&user_model.UserSessionView{
|
&user_model.UserSessionView{
|
||||||
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
|
PasswordVerification: testNow,
|
||||||
SecondFactorVerification: time.Time{},
|
SecondFactorVerification: time.Time{},
|
||||||
MultiFactorVerification: time.Time{},
|
MultiFactorVerification: time.Time{},
|
||||||
},
|
},
|
||||||
@ -1684,7 +1688,7 @@ func Test_userSessionByIDs(t *testing.T) {
|
|||||||
"new user events but other agentID, old view model state",
|
"new user events but other agentID, old view model state",
|
||||||
args{
|
args{
|
||||||
userProvider: &mockViewUserSession{
|
userProvider: &mockViewUserSession{
|
||||||
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
|
PasswordVerification: testNow,
|
||||||
},
|
},
|
||||||
agentID: "agentID",
|
agentID: "agentID",
|
||||||
user: &user_model.UserView{ID: "id"},
|
user: &user_model.UserView{ID: "id"},
|
||||||
@ -1692,7 +1696,7 @@ func Test_userSessionByIDs(t *testing.T) {
|
|||||||
&es_models.Event{
|
&es_models.Event{
|
||||||
AggregateType: user_repo.AggregateType,
|
AggregateType: user_repo.AggregateType,
|
||||||
Type: es_models.EventType(user_repo.UserV1MFAOTPCheckSucceededType),
|
Type: es_models.EventType(user_repo.UserV1MFAOTPCheckSucceededType),
|
||||||
CreationDate: time.Now().UTC().Round(1 * time.Second),
|
CreationDate: testNow,
|
||||||
Data: func() []byte {
|
Data: func() []byte {
|
||||||
data, _ := json.Marshal(&user_es_model.AuthRequest{UserAgentID: "otherID"})
|
data, _ := json.Marshal(&user_es_model.AuthRequest{UserAgentID: "otherID"})
|
||||||
return data
|
return data
|
||||||
@ -1701,7 +1705,7 @@ func Test_userSessionByIDs(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
&user_model.UserSessionView{
|
&user_model.UserSessionView{
|
||||||
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
|
PasswordVerification: testNow,
|
||||||
SecondFactorVerification: time.Time{},
|
SecondFactorVerification: time.Time{},
|
||||||
MultiFactorVerification: time.Time{},
|
MultiFactorVerification: time.Time{},
|
||||||
},
|
},
|
||||||
@ -1711,7 +1715,7 @@ func Test_userSessionByIDs(t *testing.T) {
|
|||||||
"new user events, new view model state",
|
"new user events, new view model state",
|
||||||
args{
|
args{
|
||||||
userProvider: &mockViewUserSession{
|
userProvider: &mockViewUserSession{
|
||||||
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
|
PasswordVerification: testNow,
|
||||||
},
|
},
|
||||||
agentID: "agentID",
|
agentID: "agentID",
|
||||||
user: &user_model.UserView{ID: "id", HumanView: &user_model.HumanView{FirstName: "FirstName"}},
|
user: &user_model.UserView{ID: "id", HumanView: &user_model.HumanView{FirstName: "FirstName"}},
|
||||||
@ -1719,7 +1723,7 @@ func Test_userSessionByIDs(t *testing.T) {
|
|||||||
&es_models.Event{
|
&es_models.Event{
|
||||||
AggregateType: user_repo.AggregateType,
|
AggregateType: user_repo.AggregateType,
|
||||||
Type: es_models.EventType(user_repo.UserV1MFAOTPCheckSucceededType),
|
Type: es_models.EventType(user_repo.UserV1MFAOTPCheckSucceededType),
|
||||||
CreationDate: time.Now().UTC().Round(1 * time.Second),
|
CreationDate: testNow,
|
||||||
Data: func() []byte {
|
Data: func() []byte {
|
||||||
data, _ := json.Marshal(&user_es_model.AuthRequest{UserAgentID: "agentID"})
|
data, _ := json.Marshal(&user_es_model.AuthRequest{UserAgentID: "agentID"})
|
||||||
return data
|
return data
|
||||||
@ -1728,9 +1732,9 @@ func Test_userSessionByIDs(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
&user_model.UserSessionView{
|
&user_model.UserSessionView{
|
||||||
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
|
PasswordVerification: testNow,
|
||||||
SecondFactorVerification: time.Now().UTC().Round(1 * time.Second),
|
SecondFactorVerification: testNow,
|
||||||
ChangeDate: time.Now().UTC().Round(1 * time.Second),
|
ChangeDate: testNow,
|
||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
},
|
},
|
||||||
@ -1738,7 +1742,7 @@ func Test_userSessionByIDs(t *testing.T) {
|
|||||||
"new user events (user deleted), precondition failed error",
|
"new user events (user deleted), precondition failed error",
|
||||||
args{
|
args{
|
||||||
userProvider: &mockViewUserSession{
|
userProvider: &mockViewUserSession{
|
||||||
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
|
PasswordVerification: testNow,
|
||||||
},
|
},
|
||||||
agentID: "agentID",
|
agentID: "agentID",
|
||||||
user: &user_model.UserView{ID: "id"},
|
user: &user_model.UserView{ID: "id"},
|
||||||
@ -1816,7 +1820,7 @@ func Test_userByID(t *testing.T) {
|
|||||||
&es_models.Event{
|
&es_models.Event{
|
||||||
AggregateType: user_repo.AggregateType,
|
AggregateType: user_repo.AggregateType,
|
||||||
Type: es_models.EventType(user_repo.UserV1PasswordChangedType),
|
Type: es_models.EventType(user_repo.UserV1PasswordChangedType),
|
||||||
CreationDate: time.Now().UTC().Round(1 * time.Second),
|
CreationDate: testNow,
|
||||||
Data: nil,
|
Data: nil,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1842,7 +1846,7 @@ func Test_userByID(t *testing.T) {
|
|||||||
&es_models.Event{
|
&es_models.Event{
|
||||||
AggregateType: user_repo.AggregateType,
|
AggregateType: user_repo.AggregateType,
|
||||||
Type: es_models.EventType(user_repo.UserV1PasswordChangedType),
|
Type: es_models.EventType(user_repo.UserV1PasswordChangedType),
|
||||||
CreationDate: time.Now().UTC().Round(1 * time.Second),
|
CreationDate: testNow,
|
||||||
Data: func() []byte {
|
Data: func() []byte {
|
||||||
data, _ := json.Marshal(user_es_model.Password{ChangeRequired: false, Secret: &crypto.CryptoValue{}})
|
data, _ := json.Marshal(user_es_model.Password{ChangeRequired: false, Secret: &crypto.CryptoValue{}})
|
||||||
return data
|
return data
|
||||||
@ -1851,13 +1855,13 @@ func Test_userByID(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
&user_model.UserView{
|
&user_model.UserView{
|
||||||
ChangeDate: time.Now().UTC().Round(1 * time.Second),
|
ChangeDate: testNow,
|
||||||
State: user_model.UserStateActive,
|
State: user_model.UserStateActive,
|
||||||
UserName: "UserName",
|
UserName: "UserName",
|
||||||
HumanView: &user_model.HumanView{
|
HumanView: &user_model.HumanView{
|
||||||
PasswordSet: true,
|
PasswordSet: true,
|
||||||
PasswordChangeRequired: false,
|
PasswordChangeRequired: false,
|
||||||
PasswordChanged: time.Now().UTC().Round(1 * time.Second),
|
PasswordChanged: testNow,
|
||||||
FirstName: "FirstName",
|
FirstName: "FirstName",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/api/http"
|
api_http "github.com/zitadel/zitadel/internal/api/http"
|
||||||
sd "github.com/zitadel/zitadel/internal/config/systemdefaults"
|
sd "github.com/zitadel/zitadel/internal/config/systemdefaults"
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
@ -23,6 +24,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Commands struct {
|
type Commands struct {
|
||||||
|
httpClient *http.Client
|
||||||
|
|
||||||
eventstore *eventstore.Eventstore
|
eventstore *eventstore.Eventstore
|
||||||
static static.Storage
|
static static.Storage
|
||||||
idGenerator id.Generator
|
idGenerator id.Generator
|
||||||
@ -40,14 +43,17 @@ type Commands struct {
|
|||||||
applicationKeySize int
|
applicationKeySize int
|
||||||
domainVerificationAlg crypto.EncryptionAlgorithm
|
domainVerificationAlg crypto.EncryptionAlgorithm
|
||||||
domainVerificationGenerator crypto.Generator
|
domainVerificationGenerator crypto.Generator
|
||||||
domainVerificationValidator func(domain, token, verifier string, checkType http.CheckType) error
|
domainVerificationValidator func(domain, token, verifier string, checkType api_http.CheckType) error
|
||||||
|
|
||||||
multifactors domain.MultifactorConfigs
|
multifactors domain.MultifactorConfigs
|
||||||
webauthnConfig *webauthn_helper.Config
|
webauthnConfig *webauthn_helper.Config
|
||||||
keySize int
|
keySize int
|
||||||
keyAlgorithm crypto.EncryptionAlgorithm
|
keyAlgorithm crypto.EncryptionAlgorithm
|
||||||
|
certificateAlgorithm crypto.EncryptionAlgorithm
|
||||||
|
certKeySize int
|
||||||
privateKeyLifetime time.Duration
|
privateKeyLifetime time.Duration
|
||||||
publicKeyLifetime time.Duration
|
publicKeyLifetime time.Duration
|
||||||
|
certificateLifetime time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartCommands(es *eventstore.Eventstore,
|
func StartCommands(es *eventstore.Eventstore,
|
||||||
@ -64,7 +70,9 @@ func StartCommands(es *eventstore.Eventstore,
|
|||||||
smsEncryption,
|
smsEncryption,
|
||||||
userEncryption,
|
userEncryption,
|
||||||
domainVerificationEncryption,
|
domainVerificationEncryption,
|
||||||
oidcEncryption crypto.EncryptionAlgorithm,
|
oidcEncryption,
|
||||||
|
samlEncryption crypto.EncryptionAlgorithm,
|
||||||
|
httpClient *http.Client,
|
||||||
) (repo *Commands, err error) {
|
) (repo *Commands, err error) {
|
||||||
if externalDomain == "" {
|
if externalDomain == "" {
|
||||||
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-Df21s", "no external domain specified")
|
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-Df21s", "no external domain specified")
|
||||||
@ -78,15 +86,19 @@ func StartCommands(es *eventstore.Eventstore,
|
|||||||
externalSecure: externalSecure,
|
externalSecure: externalSecure,
|
||||||
externalPort: externalPort,
|
externalPort: externalPort,
|
||||||
keySize: defaults.KeyConfig.Size,
|
keySize: defaults.KeyConfig.Size,
|
||||||
|
certKeySize: defaults.KeyConfig.CertificateSize,
|
||||||
privateKeyLifetime: defaults.KeyConfig.PrivateKeyLifetime,
|
privateKeyLifetime: defaults.KeyConfig.PrivateKeyLifetime,
|
||||||
publicKeyLifetime: defaults.KeyConfig.PublicKeyLifetime,
|
publicKeyLifetime: defaults.KeyConfig.PublicKeyLifetime,
|
||||||
|
certificateLifetime: defaults.KeyConfig.CertificateLifetime,
|
||||||
idpConfigEncryption: idpConfigEncryption,
|
idpConfigEncryption: idpConfigEncryption,
|
||||||
smtpEncryption: smtpEncryption,
|
smtpEncryption: smtpEncryption,
|
||||||
smsEncryption: smsEncryption,
|
smsEncryption: smsEncryption,
|
||||||
userEncryption: userEncryption,
|
userEncryption: userEncryption,
|
||||||
domainVerificationAlg: domainVerificationEncryption,
|
domainVerificationAlg: domainVerificationEncryption,
|
||||||
keyAlgorithm: oidcEncryption,
|
keyAlgorithm: oidcEncryption,
|
||||||
|
certificateAlgorithm: samlEncryption,
|
||||||
webauthnConfig: webAuthN,
|
webauthnConfig: webAuthN,
|
||||||
|
httpClient: httpClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
instance_repo.RegisterEventMappers(repo.eventstore)
|
instance_repo.RegisterEventMappers(repo.eventstore)
|
||||||
@ -109,7 +121,7 @@ func StartCommands(es *eventstore.Eventstore,
|
|||||||
}
|
}
|
||||||
|
|
||||||
repo.domainVerificationGenerator = crypto.NewEncryptionGenerator(defaults.DomainVerification.VerificationGenerator, repo.domainVerificationAlg)
|
repo.domainVerificationGenerator = crypto.NewEncryptionGenerator(defaults.DomainVerification.VerificationGenerator, repo.domainVerificationAlg)
|
||||||
repo.domainVerificationValidator = http.ValidateDomain
|
repo.domainVerificationValidator = api_http.ValidateDomain
|
||||||
return repo, nil
|
return repo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,10 @@ package command
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"math/big"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
@ -34,3 +38,138 @@ func (c *Commands) GenerateSigningKeyPair(ctx context.Context, algorithm string)
|
|||||||
privateKeyExp, publicKeyExp))
|
privateKeyExp, publicKeyExp))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Commands) GenerateSAMLCACertificate(ctx context.Context, algorithm string) error {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
after := now.Add(c.certificateLifetime)
|
||||||
|
randInt, err := rand.Int(rand.Reader, big.NewInt(1000))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
privateCrypto, publicCrypto, certificateCrypto, err := crypto.GenerateEncryptedKeyPairWithCACertificate(c.certKeySize, c.keyAlgorithm, c.certificateAlgorithm, &crypto.CertificateInformations{
|
||||||
|
SerialNumber: randInt,
|
||||||
|
Organisation: []string{"ZITADEL"},
|
||||||
|
CommonName: "ZITADEL SAML CA",
|
||||||
|
NotBefore: now,
|
||||||
|
NotAfter: after,
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
keyID, err := c.idGenerator.Next()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPairWriteModel := NewKeyPairWriteModel(keyID, authz.GetInstance(ctx).InstanceID())
|
||||||
|
keyAgg := KeyPairAggregateFromWriteModel(&keyPairWriteModel.WriteModel)
|
||||||
|
_, err = c.eventstore.Push(ctx,
|
||||||
|
keypair.NewAddedEvent(
|
||||||
|
ctx,
|
||||||
|
keyAgg,
|
||||||
|
domain.KeyUsageSAMLCA,
|
||||||
|
algorithm,
|
||||||
|
privateCrypto, publicCrypto,
|
||||||
|
after, after,
|
||||||
|
),
|
||||||
|
keypair.NewAddedCertificateEvent(
|
||||||
|
ctx,
|
||||||
|
keyAgg,
|
||||||
|
certificateCrypto,
|
||||||
|
after,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) GenerateSAMLResponseCertificate(ctx context.Context, algorithm string, caPrivateKey *rsa.PrivateKey, caCertificate []byte) error {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
after := now.Add(c.certificateLifetime)
|
||||||
|
randInt, err := rand.Int(rand.Reader, big.NewInt(1000))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
privateCrypto, publicCrypto, certificateCrypto, err := crypto.GenerateEncryptedKeyPairWithCertificate(c.certKeySize, c.keyAlgorithm, c.certificateAlgorithm, caPrivateKey, caCertificate, &crypto.CertificateInformations{
|
||||||
|
SerialNumber: randInt,
|
||||||
|
Organisation: []string{"ZITADEL"},
|
||||||
|
CommonName: "ZITADEL SAML response",
|
||||||
|
NotBefore: now,
|
||||||
|
NotAfter: after,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
keyID, err := c.idGenerator.Next()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPairWriteModel := NewKeyPairWriteModel(keyID, authz.GetInstance(ctx).InstanceID())
|
||||||
|
keyAgg := KeyPairAggregateFromWriteModel(&keyPairWriteModel.WriteModel)
|
||||||
|
_, err = c.eventstore.Push(ctx,
|
||||||
|
keypair.NewAddedEvent(
|
||||||
|
ctx,
|
||||||
|
keyAgg,
|
||||||
|
domain.KeyUsageSAMLResponseSinging,
|
||||||
|
algorithm,
|
||||||
|
privateCrypto, publicCrypto,
|
||||||
|
after, after,
|
||||||
|
),
|
||||||
|
keypair.NewAddedCertificateEvent(
|
||||||
|
ctx,
|
||||||
|
keyAgg,
|
||||||
|
certificateCrypto,
|
||||||
|
after,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) GenerateSAMLMetadataCertificate(ctx context.Context, algorithm string, caPrivateKey *rsa.PrivateKey, caCertificate []byte) error {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
after := now.Add(c.certificateLifetime)
|
||||||
|
randInt, err := rand.Int(rand.Reader, big.NewInt(1000))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
privateCrypto, publicCrypto, certificateCrypto, err := crypto.GenerateEncryptedKeyPairWithCertificate(c.certKeySize, c.keyAlgorithm, c.certificateAlgorithm, caPrivateKey, caCertificate, &crypto.CertificateInformations{
|
||||||
|
SerialNumber: randInt,
|
||||||
|
Organisation: []string{"ZITADEL"},
|
||||||
|
CommonName: "ZITADEL SAML metadata",
|
||||||
|
NotBefore: now,
|
||||||
|
NotAfter: after,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
keyID, err := c.idGenerator.Next()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPairWriteModel := NewKeyPairWriteModel(keyID, authz.GetInstance(ctx).InstanceID())
|
||||||
|
keyAgg := KeyPairAggregateFromWriteModel(&keyPairWriteModel.WriteModel)
|
||||||
|
_, err = c.eventstore.Push(ctx,
|
||||||
|
keypair.NewAddedEvent(
|
||||||
|
ctx,
|
||||||
|
keyAgg,
|
||||||
|
domain.KeyUsageSAMLMetadataSigning,
|
||||||
|
algorithm,
|
||||||
|
privateCrypto, publicCrypto,
|
||||||
|
after, after),
|
||||||
|
keypair.NewAddedCertificateEvent(
|
||||||
|
ctx,
|
||||||
|
keyAgg,
|
||||||
|
certificateCrypto,
|
||||||
|
after,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
@ -13,6 +13,7 @@ type KeyPairWriteModel struct {
|
|||||||
Algorithm string
|
Algorithm string
|
||||||
PrivateKey *domain.Key
|
PrivateKey *domain.Key
|
||||||
PublicKey *domain.Key
|
PublicKey *domain.Key
|
||||||
|
Certificate *domain.Key
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewKeyPairWriteModel(aggregateID, resourceOwner string) *KeyPairWriteModel {
|
func NewKeyPairWriteModel(aggregateID, resourceOwner string) *KeyPairWriteModel {
|
||||||
@ -42,6 +43,11 @@ func (wm *KeyPairWriteModel) Reduce() error {
|
|||||||
Key: e.PublicKey.Key,
|
Key: e.PublicKey.Key,
|
||||||
Expiry: e.PublicKey.Expiry,
|
Expiry: e.PublicKey.Expiry,
|
||||||
}
|
}
|
||||||
|
case *keypair.AddedCertificateEvent:
|
||||||
|
wm.Certificate = &domain.Key{
|
||||||
|
Key: e.Certificate.Key,
|
||||||
|
Expiry: e.Certificate.Expiry,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return wm.WriteModel.Reduce()
|
return wm.WriteModel.Reduce()
|
||||||
@ -53,11 +59,10 @@ func (wm *KeyPairWriteModel) Query() *eventstore.SearchQueryBuilder {
|
|||||||
AddQuery().
|
AddQuery().
|
||||||
AggregateTypes(keypair.AggregateType).
|
AggregateTypes(keypair.AggregateType).
|
||||||
AggregateIDs(wm.AggregateID).
|
AggregateIDs(wm.AggregateID).
|
||||||
EventTypes(keypair.AddedEventType).
|
EventTypes(keypair.AddedEventType, keypair.AddedCertificateEventType).
|
||||||
Builder()
|
Builder()
|
||||||
}
|
}
|
||||||
|
|
||||||
func KeyPairAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate {
|
func KeyPairAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate {
|
||||||
return eventstore.AggregateFromWriteModel(wm, keypair.AggregateType, keypair.AggregateVersion)
|
return eventstore.AggregateFromWriteModel(wm, keypair.AggregateType, keypair.AggregateVersion)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/zitadel/logging"
|
"github.com/zitadel/logging"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/errors"
|
"github.com/zitadel/zitadel/internal/errors"
|
||||||
@ -276,9 +277,20 @@ func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner s
|
|||||||
if existingProject.State == domain.ProjectStateUnspecified || existingProject.State == domain.ProjectStateRemoved {
|
if existingProject.State == domain.ProjectStateUnspecified || existingProject.State == domain.ProjectStateRemoved {
|
||||||
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.Project.NotFound")
|
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.Project.NotFound")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
samlEntityIDsAgg, err := c.getSAMLEntityIdsWriteModelByProjectID(ctx, projectID, resourceOwner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
uniqueConstraints := make([]*eventstore.EventUniqueConstraint, len(samlEntityIDsAgg.EntityIDs))
|
||||||
|
for i, entityID := range samlEntityIDsAgg.EntityIDs {
|
||||||
|
uniqueConstraints[i] = project.NewRemoveSAMLConfigEntityIDUniqueConstraint(entityID.EntityID)
|
||||||
|
}
|
||||||
|
|
||||||
projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel)
|
projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel)
|
||||||
events := []eventstore.Command{
|
events := []eventstore.Command{
|
||||||
project.NewProjectRemovedEvent(ctx, projectAgg, existingProject.Name),
|
project.NewProjectRemovedEvent(ctx, projectAgg, existingProject.Name, uniqueConstraints),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, grantID := range cascadingUserGrantIDs {
|
for _, grantID := range cascadingUserGrantIDs {
|
||||||
@ -309,3 +321,12 @@ func (c *Commands) getProjectWriteModelByID(ctx context.Context, projectID, reso
|
|||||||
}
|
}
|
||||||
return projectWriteModel, nil
|
return projectWriteModel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Commands) getSAMLEntityIdsWriteModelByProjectID(ctx context.Context, projectID, resourceOwner string) (*SAMLEntityIDsWriteModel, error) {
|
||||||
|
samlEntityIDsAgg := NewSAMLEntityIDsWriteModel(projectID, resourceOwner)
|
||||||
|
err := c.eventstore.FilterToQueryReducer(ctx, samlEntityIDsAgg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return samlEntityIDsAgg, nil
|
||||||
|
}
|
||||||
|
@ -118,7 +118,13 @@ func (c *Commands) RemoveApplication(ctx context.Context, projectID, appID, reso
|
|||||||
}
|
}
|
||||||
projectAgg := ProjectAggregateFromWriteModel(&existingApp.WriteModel)
|
projectAgg := ProjectAggregateFromWriteModel(&existingApp.WriteModel)
|
||||||
|
|
||||||
pushedEvents, err := c.eventstore.Push(ctx, project.NewApplicationRemovedEvent(ctx, projectAgg, appID, existingApp.Name))
|
entityID := ""
|
||||||
|
samlWriteModel, err := c.getSAMLAppWriteModel(ctx, projectID, appID, resourceOwner)
|
||||||
|
if err == nil && samlWriteModel.State != domain.AppStateUnspecified && samlWriteModel.State != domain.AppStateRemoved && samlWriteModel.saml {
|
||||||
|
entityID = samlWriteModel.EntityID
|
||||||
|
}
|
||||||
|
|
||||||
|
pushedEvents, err := c.eventstore.Push(ctx, project.NewApplicationRemovedEvent(ctx, projectAgg, appID, existingApp.Name, entityID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
146
internal/command/project_application_saml.go
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/zitadel/saml/pkg/provider/xml"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/project"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Commands) AddSAMLApplication(ctx context.Context, application *domain.SAMLApp, resourceOwner string) (_ *domain.SAMLApp, err error) {
|
||||||
|
if application == nil || application.AggregateID == "" {
|
||||||
|
return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-35Fn0", "Errors.Project.App.Invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.getProjectByID(ctx, application.AggregateID, resourceOwner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, caos_errs.ThrowPreconditionFailed(err, "PROJECT-3p9ss", "Errors.Project.NotFound")
|
||||||
|
}
|
||||||
|
|
||||||
|
addedApplication := NewSAMLApplicationWriteModel(application.AggregateID, resourceOwner)
|
||||||
|
projectAgg := ProjectAggregateFromWriteModel(&addedApplication.WriteModel)
|
||||||
|
events, err := c.addSAMLApplication(ctx, projectAgg, application)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
addedApplication.AppID = application.AppID
|
||||||
|
pushedEvents, err := c.eventstore.Push(ctx, events...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = AppendAndReduce(addedApplication, pushedEvents...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := samlWriteModelToSAMLConfig(addedApplication)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) addSAMLApplication(ctx context.Context, projectAgg *eventstore.Aggregate, samlApp *domain.SAMLApp) (events []eventstore.Command, err error) {
|
||||||
|
|
||||||
|
if samlApp.AppName == "" || !samlApp.IsValid() {
|
||||||
|
return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-1n9df", "Errors.Project.App.Invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
if samlApp.Metadata == nil && samlApp.MetadataURL == "" {
|
||||||
|
return nil, caos_errs.ThrowInvalidArgument(nil, "SAML-podix9", "Errors.Project.App.SAMLMetadataMissing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if samlApp.MetadataURL != "" {
|
||||||
|
data, err := xml.ReadMetadataFromURL(c.httpClient, samlApp.MetadataURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, caos_errs.ThrowInvalidArgument(err, "SAML-wmqlo1", "Errors.Project.App.SAMLMetadataMissing")
|
||||||
|
}
|
||||||
|
samlApp.Metadata = data
|
||||||
|
}
|
||||||
|
|
||||||
|
entity, err := xml.ParseMetadataXmlIntoStruct(samlApp.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, caos_errs.ThrowInvalidArgument(err, "SAML-bquso", "Errors.Project.App.SAMLMetadataFormat")
|
||||||
|
}
|
||||||
|
|
||||||
|
samlApp.AppID, err = c.idGenerator.Next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return []eventstore.Command{
|
||||||
|
project.NewApplicationAddedEvent(ctx, projectAgg, samlApp.AppID, samlApp.AppName),
|
||||||
|
project.NewSAMLConfigAddedEvent(ctx,
|
||||||
|
projectAgg,
|
||||||
|
samlApp.AppID,
|
||||||
|
string(entity.EntityID),
|
||||||
|
samlApp.Metadata,
|
||||||
|
samlApp.MetadataURL,
|
||||||
|
),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) ChangeSAMLApplication(ctx context.Context, samlApp *domain.SAMLApp, resourceOwner string) (*domain.SAMLApp, error) {
|
||||||
|
if !samlApp.IsValid() || samlApp.AppID == "" || samlApp.AggregateID == "" {
|
||||||
|
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5n9fs", "Errors.Project.App.SAMLConfigInvalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
existingSAML, err := c.getSAMLAppWriteModel(ctx, samlApp.AggregateID, samlApp.AppID, resourceOwner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if existingSAML.State == domain.AppStateUnspecified || existingSAML.State == domain.AppStateRemoved {
|
||||||
|
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-2n8uU", "Errors.Project.App.NotExisting")
|
||||||
|
}
|
||||||
|
if !existingSAML.IsSAML() {
|
||||||
|
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-GBr35", "Errors.Project.App.IsNotSAML")
|
||||||
|
}
|
||||||
|
projectAgg := ProjectAggregateFromWriteModel(&existingSAML.WriteModel)
|
||||||
|
|
||||||
|
if samlApp.MetadataURL != "" {
|
||||||
|
data, err := xml.ReadMetadataFromURL(c.httpClient, samlApp.MetadataURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, caos_errs.ThrowInvalidArgument(err, "SAML-J3kg3", "Errors.Project.App.SAMLMetadataMissing")
|
||||||
|
}
|
||||||
|
samlApp.Metadata = data
|
||||||
|
}
|
||||||
|
|
||||||
|
entity, err := xml.ParseMetadataXmlIntoStruct(samlApp.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, caos_errs.ThrowInvalidArgument(err, "SAML-3fk2b", "Errors.Project.App.SAMLMetadataFormat")
|
||||||
|
}
|
||||||
|
|
||||||
|
changedEvent, hasChanged, err := existingSAML.NewChangedEvent(
|
||||||
|
ctx,
|
||||||
|
projectAgg,
|
||||||
|
samlApp.AppID,
|
||||||
|
string(entity.EntityID),
|
||||||
|
samlApp.Metadata,
|
||||||
|
samlApp.MetadataURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !hasChanged {
|
||||||
|
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-1m88i", "Errors.NoChangesFound")
|
||||||
|
}
|
||||||
|
|
||||||
|
pushedEvents, err := c.eventstore.Push(ctx, changedEvent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = AppendAndReduce(existingSAML, pushedEvents...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return samlWriteModelToSAMLConfig(existingSAML), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) getSAMLAppWriteModel(ctx context.Context, projectID, appID, resourceOwner string) (*SAMLApplicationWriteModel, error) {
|
||||||
|
appWriteModel := NewSAMLApplicationWriteModelWithAppID(projectID, appID, resourceOwner)
|
||||||
|
err := c.eventstore.FilterToQueryReducer(ctx, appWriteModel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return appWriteModel, nil
|
||||||
|
}
|
268
internal/command/project_application_saml_model.go
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/project"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SAMLApplicationWriteModel struct {
|
||||||
|
eventstore.WriteModel
|
||||||
|
|
||||||
|
AppID string
|
||||||
|
AppName string
|
||||||
|
EntityID string
|
||||||
|
Metadata []byte
|
||||||
|
MetadataURL string
|
||||||
|
|
||||||
|
State domain.AppState
|
||||||
|
saml bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSAMLApplicationWriteModelWithAppID(projectID, appID, resourceOwner string) *SAMLApplicationWriteModel {
|
||||||
|
return &SAMLApplicationWriteModel{
|
||||||
|
WriteModel: eventstore.WriteModel{
|
||||||
|
AggregateID: projectID,
|
||||||
|
ResourceOwner: resourceOwner,
|
||||||
|
},
|
||||||
|
AppID: appID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSAMLApplicationWriteModel(projectID, resourceOwner string) *SAMLApplicationWriteModel {
|
||||||
|
return &SAMLApplicationWriteModel{
|
||||||
|
WriteModel: eventstore.WriteModel{
|
||||||
|
AggregateID: projectID,
|
||||||
|
ResourceOwner: resourceOwner,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *SAMLApplicationWriteModel) AppendEvents(events ...eventstore.Event) {
|
||||||
|
for _, event := range events {
|
||||||
|
switch e := event.(type) {
|
||||||
|
case *project.ApplicationAddedEvent:
|
||||||
|
if e.AppID != wm.AppID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wm.WriteModel.AppendEvents(e)
|
||||||
|
case *project.ApplicationChangedEvent:
|
||||||
|
if e.AppID != wm.AppID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wm.WriteModel.AppendEvents(e)
|
||||||
|
case *project.ApplicationDeactivatedEvent:
|
||||||
|
if e.AppID != wm.AppID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wm.WriteModel.AppendEvents(e)
|
||||||
|
case *project.ApplicationReactivatedEvent:
|
||||||
|
if e.AppID != wm.AppID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wm.WriteModel.AppendEvents(e)
|
||||||
|
case *project.ApplicationRemovedEvent:
|
||||||
|
if e.AppID != wm.AppID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wm.WriteModel.AppendEvents(e)
|
||||||
|
case *project.SAMLConfigAddedEvent:
|
||||||
|
if e.AppID != wm.AppID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wm.WriteModel.AppendEvents(e)
|
||||||
|
case *project.SAMLConfigChangedEvent:
|
||||||
|
if e.AppID != wm.AppID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wm.WriteModel.AppendEvents(e)
|
||||||
|
case *project.ProjectRemovedEvent:
|
||||||
|
wm.WriteModel.AppendEvents(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *SAMLApplicationWriteModel) Reduce() error {
|
||||||
|
for _, event := range wm.Events {
|
||||||
|
switch e := event.(type) {
|
||||||
|
case *project.ApplicationAddedEvent:
|
||||||
|
wm.AppName = e.Name
|
||||||
|
wm.State = domain.AppStateActive
|
||||||
|
case *project.ApplicationChangedEvent:
|
||||||
|
wm.AppName = e.Name
|
||||||
|
case *project.ApplicationDeactivatedEvent:
|
||||||
|
if wm.State == domain.AppStateRemoved {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wm.State = domain.AppStateInactive
|
||||||
|
case *project.ApplicationReactivatedEvent:
|
||||||
|
if wm.State == domain.AppStateRemoved {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wm.State = domain.AppStateActive
|
||||||
|
case *project.ApplicationRemovedEvent:
|
||||||
|
wm.State = domain.AppStateRemoved
|
||||||
|
case *project.SAMLConfigAddedEvent:
|
||||||
|
wm.appendAddSAMLEvent(e)
|
||||||
|
case *project.SAMLConfigChangedEvent:
|
||||||
|
wm.appendChangeSAMLEvent(e)
|
||||||
|
case *project.ProjectRemovedEvent:
|
||||||
|
wm.State = domain.AppStateRemoved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wm.WriteModel.Reduce()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *SAMLApplicationWriteModel) appendAddSAMLEvent(e *project.SAMLConfigAddedEvent) {
|
||||||
|
wm.saml = true
|
||||||
|
wm.Metadata = e.Metadata
|
||||||
|
wm.MetadataURL = e.MetadataURL
|
||||||
|
wm.EntityID = e.EntityID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *SAMLApplicationWriteModel) appendChangeSAMLEvent(e *project.SAMLConfigChangedEvent) {
|
||||||
|
wm.saml = true
|
||||||
|
if e.Metadata != nil {
|
||||||
|
wm.Metadata = e.Metadata
|
||||||
|
}
|
||||||
|
if e.MetadataURL != nil {
|
||||||
|
wm.MetadataURL = *e.MetadataURL
|
||||||
|
}
|
||||||
|
if e.EntityID != "" {
|
||||||
|
wm.EntityID = e.EntityID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *SAMLApplicationWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||||
|
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||||
|
ResourceOwner(wm.ResourceOwner).
|
||||||
|
AddQuery().
|
||||||
|
AggregateTypes(project.AggregateType).
|
||||||
|
AggregateIDs(wm.AggregateID).
|
||||||
|
EventTypes(
|
||||||
|
project.ApplicationAddedType,
|
||||||
|
project.ApplicationChangedType,
|
||||||
|
project.ApplicationDeactivatedType,
|
||||||
|
project.ApplicationReactivatedType,
|
||||||
|
project.ApplicationRemovedType,
|
||||||
|
project.SAMLConfigAddedType,
|
||||||
|
project.SAMLConfigChangedType,
|
||||||
|
project.ProjectRemovedType).
|
||||||
|
Builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *SAMLApplicationWriteModel) NewChangedEvent(
|
||||||
|
ctx context.Context,
|
||||||
|
aggregate *eventstore.Aggregate,
|
||||||
|
appID string,
|
||||||
|
entityID string,
|
||||||
|
metadata []byte,
|
||||||
|
metadataURL string,
|
||||||
|
) (*project.SAMLConfigChangedEvent, bool, error) {
|
||||||
|
changes := make([]project.SAMLConfigChanges, 0)
|
||||||
|
var err error
|
||||||
|
if !reflect.DeepEqual(wm.Metadata, metadata) {
|
||||||
|
changes = append(changes, project.ChangeMetadata(metadata))
|
||||||
|
}
|
||||||
|
if wm.MetadataURL != metadataURL {
|
||||||
|
changes = append(changes, project.ChangeMetadataURL(metadataURL))
|
||||||
|
}
|
||||||
|
if wm.EntityID != entityID {
|
||||||
|
changes = append(changes, project.ChangeEntityID(entityID))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(changes) == 0 {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
changeEvent, err := project.NewSAMLConfigChangedEvent(ctx, aggregate, appID, wm.EntityID, changes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
return changeEvent, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *SAMLApplicationWriteModel) IsSAML() bool {
|
||||||
|
return wm.saml
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppIDToEntityID struct {
|
||||||
|
AppID string
|
||||||
|
EntityID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SAMLEntityIDsWriteModel struct {
|
||||||
|
eventstore.WriteModel
|
||||||
|
|
||||||
|
EntityIDs []*AppIDToEntityID
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSAMLEntityIDsWriteModel(projectID, resourceOwner string) *SAMLEntityIDsWriteModel {
|
||||||
|
return &SAMLEntityIDsWriteModel{
|
||||||
|
WriteModel: eventstore.WriteModel{
|
||||||
|
AggregateID: projectID,
|
||||||
|
ResourceOwner: resourceOwner,
|
||||||
|
},
|
||||||
|
EntityIDs: []*AppIDToEntityID{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *SAMLEntityIDsWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||||
|
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||||
|
ResourceOwner(wm.ResourceOwner).
|
||||||
|
AddQuery().
|
||||||
|
AggregateTypes(project.AggregateType).
|
||||||
|
AggregateIDs(wm.AggregateID).
|
||||||
|
EventTypes(
|
||||||
|
project.ApplicationRemovedType,
|
||||||
|
project.SAMLConfigAddedType,
|
||||||
|
project.SAMLConfigChangedType).
|
||||||
|
Builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *SAMLEntityIDsWriteModel) AppendEvents(events ...eventstore.Event) {
|
||||||
|
for _, event := range events {
|
||||||
|
switch e := event.(type) {
|
||||||
|
case *project.ApplicationRemovedEvent:
|
||||||
|
wm.WriteModel.AppendEvents(e)
|
||||||
|
case *project.SAMLConfigAddedEvent:
|
||||||
|
wm.WriteModel.AppendEvents(e)
|
||||||
|
case *project.SAMLConfigChangedEvent:
|
||||||
|
if e.EntityID != "" {
|
||||||
|
wm.WriteModel.AppendEvents(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *SAMLEntityIDsWriteModel) Reduce() error {
|
||||||
|
for _, event := range wm.Events {
|
||||||
|
switch e := event.(type) {
|
||||||
|
case *project.ApplicationRemovedEvent:
|
||||||
|
removeAppIDFromEntityIDs(wm.EntityIDs, e.AppID)
|
||||||
|
case *project.SAMLConfigAddedEvent:
|
||||||
|
wm.EntityIDs = append(wm.EntityIDs, &AppIDToEntityID{AppID: e.AppID, EntityID: e.EntityID})
|
||||||
|
case *project.SAMLConfigChangedEvent:
|
||||||
|
for i := range wm.EntityIDs {
|
||||||
|
item := wm.EntityIDs[i]
|
||||||
|
if e.AppID == item.AppID && e.EntityID != "" {
|
||||||
|
item.EntityID = e.EntityID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wm.WriteModel.Reduce()
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeAppIDFromEntityIDs(items []*AppIDToEntityID, appID string) []*AppIDToEntityID {
|
||||||
|
for i := len(items) - 1; i >= 0; i-- {
|
||||||
|
if items[i].AppID == appID {
|
||||||
|
items[i] = items[len(items)-1]
|
||||||
|
items[len(items)-1] = nil
|
||||||
|
items = items[:len(items)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
776
internal/command/project_application_saml_test.go
Normal file
@ -0,0 +1,776 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/errors"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||||
|
"github.com/zitadel/zitadel/internal/id"
|
||||||
|
id_mock "github.com/zitadel/zitadel/internal/id/mock"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/project"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testMetadata = []byte(`<?xml version="1.0"?>
|
||||||
|
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
|
||||||
|
validUntil="2022-08-26T14:08:16Z"
|
||||||
|
cacheDuration="PT604800S"
|
||||||
|
entityID="https://test.com/saml/metadata">
|
||||||
|
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||||
|
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
|
||||||
|
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
||||||
|
Location="https://test.com/saml/acs"
|
||||||
|
index="1" />
|
||||||
|
|
||||||
|
</md:SPSSODescriptor>
|
||||||
|
</md:EntityDescriptor>
|
||||||
|
`)
|
||||||
|
var testMetadataChangedEntityID = []byte(`<?xml version="1.0"?>
|
||||||
|
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
|
||||||
|
validUntil="2022-08-26T14:08:16Z"
|
||||||
|
cacheDuration="PT604800S"
|
||||||
|
entityID="https://test2.com/saml/metadata">
|
||||||
|
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||||
|
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
|
||||||
|
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
||||||
|
Location="https://test.com/saml/acs"
|
||||||
|
index="1" />
|
||||||
|
|
||||||
|
</md:SPSSODescriptor>
|
||||||
|
</md:EntityDescriptor>
|
||||||
|
`)
|
||||||
|
|
||||||
|
func TestCommandSide_AddSAMLApplication(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
eventstore *eventstore.Eventstore
|
||||||
|
idGenerator id.Generator
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
samlApp *domain.SAMLApp
|
||||||
|
resourceOwner string
|
||||||
|
}
|
||||||
|
type res struct {
|
||||||
|
want *domain.SAMLApp
|
||||||
|
err func(error) bool
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
res res
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no aggregate id, invalid argument error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
samlApp: &domain.SAMLApp{},
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: errors.IsErrorInvalidArgument,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "project not existing, not found error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
samlApp: &domain.SAMLApp{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "project1",
|
||||||
|
},
|
||||||
|
AppID: "app1",
|
||||||
|
AppName: "app",
|
||||||
|
},
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: errors.IsPreconditionFailed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid app, invalid argument error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewProjectAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"project", true, true, true,
|
||||||
|
domain.PrivateLabelingSettingUnspecified),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
samlApp: &domain.SAMLApp{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "project1",
|
||||||
|
},
|
||||||
|
AppID: "app1",
|
||||||
|
AppName: "",
|
||||||
|
},
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: errors.IsErrorInvalidArgument,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create saml app, metadata not parsable",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewProjectAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"project", true, true, true,
|
||||||
|
domain.PrivateLabelingSettingUnspecified),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
samlApp: &domain.SAMLApp{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "project1",
|
||||||
|
},
|
||||||
|
AppName: "app",
|
||||||
|
EntityID: "https://test.com/saml/metadata",
|
||||||
|
Metadata: []byte("test metadata"),
|
||||||
|
MetadataURL: "",
|
||||||
|
},
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: errors.IsErrorInvalidArgument,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create saml app, ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewProjectAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"project", true, true, true,
|
||||||
|
domain.PrivateLabelingSettingUnspecified),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewApplicationAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app1",
|
||||||
|
"app",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewSAMLConfigAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app1",
|
||||||
|
"https://test.com/saml/metadata",
|
||||||
|
testMetadata,
|
||||||
|
"",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
uniqueConstraintsFromEventConstraint(project.NewAddApplicationUniqueConstraint("app", "project1")),
|
||||||
|
uniqueConstraintsFromEventConstraint(project.NewAddSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1"),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
samlApp: &domain.SAMLApp{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "project1",
|
||||||
|
},
|
||||||
|
AppName: "app",
|
||||||
|
EntityID: "https://test.com/saml/metadata",
|
||||||
|
Metadata: testMetadata,
|
||||||
|
MetadataURL: "",
|
||||||
|
},
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.SAMLApp{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "project1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
AppID: "app1",
|
||||||
|
AppName: "app",
|
||||||
|
EntityID: "https://test.com/saml/metadata",
|
||||||
|
Metadata: testMetadata,
|
||||||
|
MetadataURL: "",
|
||||||
|
State: domain.AppStateActive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create saml app metadataURL, ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewProjectAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"project", true, true, true,
|
||||||
|
domain.PrivateLabelingSettingUnspecified),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewApplicationAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app1",
|
||||||
|
"app",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewSAMLConfigAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app1",
|
||||||
|
"https://test.com/saml/metadata",
|
||||||
|
testMetadata,
|
||||||
|
"http://localhost:8080/saml/metadata",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
uniqueConstraintsFromEventConstraint(project.NewAddApplicationUniqueConstraint("app", "project1")),
|
||||||
|
uniqueConstraintsFromEventConstraint(project.NewAddSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1"),
|
||||||
|
httpClient: newTestClient(200, testMetadata),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
samlApp: &domain.SAMLApp{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "project1",
|
||||||
|
},
|
||||||
|
AppName: "app",
|
||||||
|
EntityID: "https://test.com/saml/metadata",
|
||||||
|
Metadata: nil,
|
||||||
|
MetadataURL: "http://localhost:8080/saml/metadata",
|
||||||
|
},
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.SAMLApp{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "project1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
AppID: "app1",
|
||||||
|
AppName: "app",
|
||||||
|
EntityID: "https://test.com/saml/metadata",
|
||||||
|
Metadata: testMetadata,
|
||||||
|
MetadataURL: "http://localhost:8080/saml/metadata",
|
||||||
|
State: domain.AppStateActive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create saml app metadataURL, http error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewProjectAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"project", true, true, true,
|
||||||
|
domain.PrivateLabelingSettingUnspecified),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t),
|
||||||
|
httpClient: newTestClient(http.StatusNotFound, nil),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
samlApp: &domain.SAMLApp{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "project1",
|
||||||
|
},
|
||||||
|
AppName: "app",
|
||||||
|
EntityID: "https://test.com/saml/metadata",
|
||||||
|
Metadata: nil,
|
||||||
|
MetadataURL: "http://localhost:8080/saml/metadata",
|
||||||
|
},
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: errors.IsErrorInvalidArgument,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := &Commands{
|
||||||
|
eventstore: tt.fields.eventstore,
|
||||||
|
idGenerator: tt.fields.idGenerator,
|
||||||
|
httpClient: tt.fields.httpClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := r.AddSAMLApplication(tt.args.ctx, tt.args.samlApp, tt.args.resourceOwner)
|
||||||
|
if tt.res.err == nil {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
if tt.res.err != nil && !tt.res.err(err) {
|
||||||
|
t.Errorf("got wrong err: %v ", err)
|
||||||
|
}
|
||||||
|
if tt.res.err == nil {
|
||||||
|
assert.Equal(t, tt.res.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommandSide_ChangeSAMLApplication(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
eventstore *eventstore.Eventstore
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
samlApp *domain.SAMLApp
|
||||||
|
resourceOwner string
|
||||||
|
}
|
||||||
|
type res struct {
|
||||||
|
want *domain.SAMLApp
|
||||||
|
err func(error) bool
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
res res
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "invalid app, invalid argument error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
samlApp: &domain.SAMLApp{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "project1",
|
||||||
|
},
|
||||||
|
AppID: "app1",
|
||||||
|
},
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: errors.IsErrorInvalidArgument,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing appid, invalid argument error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
samlApp: &domain.SAMLApp{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "project1",
|
||||||
|
},
|
||||||
|
AppID: "",
|
||||||
|
Metadata: []byte("just not empty"),
|
||||||
|
},
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: errors.IsErrorInvalidArgument,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing aggregateid, invalid argument error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
samlApp: &domain.SAMLApp{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "",
|
||||||
|
},
|
||||||
|
AppID: "appid",
|
||||||
|
Metadata: []byte("just not empty"),
|
||||||
|
},
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: errors.IsErrorInvalidArgument,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "app not existing, not found error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
samlApp: &domain.SAMLApp{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "project1",
|
||||||
|
},
|
||||||
|
AppID: "app1",
|
||||||
|
Metadata: []byte("just not empty"),
|
||||||
|
},
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: errors.IsNotFound,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no changes, precondition error, metadataURL",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewApplicationAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app1",
|
||||||
|
"app",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewSAMLConfigAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app1",
|
||||||
|
"https://test.com/saml/metadata",
|
||||||
|
testMetadata,
|
||||||
|
"http://localhost:8080/saml/metadata",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
httpClient: newTestClient(http.StatusOK, testMetadata),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
samlApp: &domain.SAMLApp{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "project1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
AppName: "app",
|
||||||
|
AppID: "app1",
|
||||||
|
EntityID: "https://test.com/saml/metadata",
|
||||||
|
Metadata: nil,
|
||||||
|
MetadataURL: "http://localhost:8080/saml/metadata",
|
||||||
|
},
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: errors.IsPreconditionFailed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no changes, precondition error, metadata",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewApplicationAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app1",
|
||||||
|
"app",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewSAMLConfigAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app1",
|
||||||
|
"https://test.com/saml/metadata",
|
||||||
|
testMetadata,
|
||||||
|
"",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
httpClient: nil,
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
samlApp: &domain.SAMLApp{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "project1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
AppName: "app",
|
||||||
|
AppID: "app1",
|
||||||
|
EntityID: "https://test.com/saml/metadata",
|
||||||
|
Metadata: testMetadata,
|
||||||
|
MetadataURL: "",
|
||||||
|
},
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: errors.IsPreconditionFailed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "change saml app, ok, metadataURL",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewApplicationAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app1",
|
||||||
|
"app",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewSAMLConfigAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app1",
|
||||||
|
"https://test.com/saml/metadata",
|
||||||
|
testMetadata,
|
||||||
|
"http://localhost:8080/saml/metadata",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(
|
||||||
|
newSAMLAppChangedEventMetadataURL(context.Background(),
|
||||||
|
"app1",
|
||||||
|
"project1",
|
||||||
|
"org1",
|
||||||
|
"https://test.com/saml/metadata",
|
||||||
|
"https://test2.com/saml/metadata",
|
||||||
|
testMetadataChangedEntityID,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata")),
|
||||||
|
uniqueConstraintsFromEventConstraint(project.NewAddSAMLConfigEntityIDUniqueConstraint("https://test2.com/saml/metadata")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
httpClient: newTestClient(http.StatusOK, testMetadataChangedEntityID),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
samlApp: &domain.SAMLApp{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "project1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
AppID: "app1",
|
||||||
|
AppName: "app",
|
||||||
|
EntityID: "https://test2.com/saml/metadata",
|
||||||
|
Metadata: nil,
|
||||||
|
MetadataURL: "http://localhost:8080/saml/metadata",
|
||||||
|
},
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.SAMLApp{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "project1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
AppID: "app1",
|
||||||
|
AppName: "app",
|
||||||
|
EntityID: "https://test2.com/saml/metadata",
|
||||||
|
Metadata: testMetadataChangedEntityID,
|
||||||
|
MetadataURL: "http://localhost:8080/saml/metadata",
|
||||||
|
State: domain.AppStateActive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "change saml app, ok, metadata",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewApplicationAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app1",
|
||||||
|
"app",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewSAMLConfigAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app1",
|
||||||
|
"https://test.com/saml/metadata",
|
||||||
|
testMetadata,
|
||||||
|
"",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(
|
||||||
|
newSAMLAppChangedEventMetadata(context.Background(),
|
||||||
|
"app1",
|
||||||
|
"project1",
|
||||||
|
"org1",
|
||||||
|
"https://test.com/saml/metadata",
|
||||||
|
"https://test2.com/saml/metadata",
|
||||||
|
testMetadataChangedEntityID,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata")),
|
||||||
|
uniqueConstraintsFromEventConstraint(project.NewAddSAMLConfigEntityIDUniqueConstraint("https://test2.com/saml/metadata")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
httpClient: nil,
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
samlApp: &domain.SAMLApp{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "project1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
AppID: "app1",
|
||||||
|
AppName: "app",
|
||||||
|
EntityID: "https://test2.com/saml/metadata",
|
||||||
|
Metadata: testMetadataChangedEntityID,
|
||||||
|
MetadataURL: "",
|
||||||
|
},
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.SAMLApp{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: "project1",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
AppID: "app1",
|
||||||
|
AppName: "app",
|
||||||
|
EntityID: "https://test2.com/saml/metadata",
|
||||||
|
Metadata: testMetadataChangedEntityID,
|
||||||
|
MetadataURL: "",
|
||||||
|
State: domain.AppStateActive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := &Commands{
|
||||||
|
eventstore: tt.fields.eventstore,
|
||||||
|
httpClient: tt.fields.httpClient,
|
||||||
|
}
|
||||||
|
got, err := r.ChangeSAMLApplication(tt.args.ctx, tt.args.samlApp, tt.args.resourceOwner)
|
||||||
|
if tt.res.err == nil {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
if tt.res.err != nil && !tt.res.err(err) {
|
||||||
|
t.Errorf("got wrong err: %v ", err)
|
||||||
|
}
|
||||||
|
if tt.res.err == nil {
|
||||||
|
assert.Equal(t, tt.res.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSAMLAppChangedEventMetadata(ctx context.Context, appID, projectID, resourceOwner, oldEntityID, entityID string, metadata []byte) *project.SAMLConfigChangedEvent {
|
||||||
|
changes := []project.SAMLConfigChanges{
|
||||||
|
project.ChangeEntityID(entityID),
|
||||||
|
project.ChangeMetadata(metadata),
|
||||||
|
}
|
||||||
|
event, _ := project.NewSAMLConfigChangedEvent(ctx,
|
||||||
|
&project.NewAggregate(projectID, resourceOwner).Aggregate,
|
||||||
|
appID,
|
||||||
|
oldEntityID,
|
||||||
|
changes,
|
||||||
|
)
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSAMLAppChangedEventMetadataURL(ctx context.Context, appID, projectID, resourceOwner, oldEntityID, entityID string, metadata []byte) *project.SAMLConfigChangedEvent {
|
||||||
|
changes := []project.SAMLConfigChanges{
|
||||||
|
project.ChangeEntityID(entityID),
|
||||||
|
project.ChangeMetadata(metadata),
|
||||||
|
}
|
||||||
|
event, _ := project.NewSAMLConfigChangedEvent(ctx,
|
||||||
|
&project.NewAggregate(projectID, resourceOwner).Aggregate,
|
||||||
|
appID,
|
||||||
|
oldEntityID,
|
||||||
|
changes,
|
||||||
|
)
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
type roundTripperFunc func(*http.Request) *http.Response
|
||||||
|
|
||||||
|
// RoundTrip implements the http.RoundTripper interface.
|
||||||
|
func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
return fn(req), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTestClient returns *http.Client with Transport replaced to avoid making real calls
|
||||||
|
func newTestClient(httpStatus int, metadata []byte) *http.Client {
|
||||||
|
fn := roundTripperFunc(func(req *http.Request) *http.Response {
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: httpStatus,
|
||||||
|
Body: ioutil.NopCloser(bytes.NewBuffer(metadata)),
|
||||||
|
Header: make(http.Header), //must be non-nil value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return &http.Client{
|
||||||
|
Transport: fn,
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
@ -580,6 +581,58 @@ func TestCommandSide_RemoveApplication(t *testing.T) {
|
|||||||
err: caos_errs.IsNotFound,
|
err: caos_errs.IsNotFound,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "app remove, entityID, ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app1",
|
||||||
|
"app",
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app1",
|
||||||
|
"app",
|
||||||
|
)),
|
||||||
|
eventFromEventPusher(project.NewSAMLConfigAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app1",
|
||||||
|
"https://test.com/saml/metadata",
|
||||||
|
[]byte("<?xml version=\"1.0\"?>\n<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n validUntil=\"2022-08-26T14:08:16Z\"\n cacheDuration=\"PT604800S\"\n entityID=\"https://test.com/saml/metadata\">\n <md:SPSSODescriptor AuthnRequestsSigned=\"false\" WantAssertionsSigned=\"false\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>\n <md:AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n Location=\"https://test.com/saml/acs\"\n index=\"1\" />\n \n </md:SPSSODescriptor>\n</md:EntityDescriptor>"),
|
||||||
|
"",
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(project.NewApplicationRemovedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app1",
|
||||||
|
"app",
|
||||||
|
"https://test.com/saml/metadata",
|
||||||
|
)),
|
||||||
|
}, /**/
|
||||||
|
uniqueConstraintsFromEventConstraint(project.NewRemoveApplicationUniqueConstraint("app", "project1")),
|
||||||
|
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
projectID: "project1",
|
||||||
|
appID: "app1",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "app remove, ok",
|
name: "app remove, ok",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
@ -592,12 +645,15 @@ func TestCommandSide_RemoveApplication(t *testing.T) {
|
|||||||
"app",
|
"app",
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
|
// app is not saml, or no saml config available
|
||||||
|
expectFilter(),
|
||||||
expectPush(
|
expectPush(
|
||||||
[]*repository.Event{
|
[]*repository.Event{
|
||||||
eventFromEventPusher(project.NewApplicationRemovedEvent(context.Background(),
|
eventFromEventPusher(project.NewApplicationRemovedEvent(context.Background(),
|
||||||
&project.NewAggregate("project1", "org1").Aggregate,
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
"app1",
|
"app1",
|
||||||
"app",
|
"app",
|
||||||
|
"",
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
uniqueConstraintsFromEventConstraint(project.NewRemoveApplicationUniqueConstraint("app", "project1")),
|
uniqueConstraintsFromEventConstraint(project.NewRemoveApplicationUniqueConstraint("app", "project1")),
|
||||||
|
@ -57,6 +57,18 @@ func oidcWriteModelToOIDCConfig(writeModel *OIDCApplicationWriteModel) *domain.O
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func samlWriteModelToSAMLConfig(writeModel *SAMLApplicationWriteModel) *domain.SAMLApp {
|
||||||
|
return &domain.SAMLApp{
|
||||||
|
ObjectRoot: writeModelToObjectRoot(writeModel.WriteModel),
|
||||||
|
AppID: writeModel.AppID,
|
||||||
|
AppName: writeModel.AppName,
|
||||||
|
State: writeModel.State,
|
||||||
|
Metadata: writeModel.Metadata,
|
||||||
|
MetadataURL: writeModel.MetadataURL,
|
||||||
|
EntityID: writeModel.EntityID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func apiWriteModelToAPIConfig(writeModel *APIApplicationWriteModel) *domain.APIApp {
|
func apiWriteModelToAPIConfig(writeModel *APIApplicationWriteModel) *domain.APIApp {
|
||||||
return &domain.APIApp{
|
return &domain.APIApp{
|
||||||
ObjectRoot: writeModelToObjectRoot(writeModel.WriteModel),
|
ObjectRoot: writeModelToObjectRoot(writeModel.WriteModel),
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
@ -50,6 +51,7 @@ func TestCommandSide_AddProjectRole(t *testing.T) {
|
|||||||
project.NewProjectRemovedEvent(context.Background(),
|
project.NewProjectRemovedEvent(context.Background(),
|
||||||
&project.NewAggregate("project1", "org1").Aggregate,
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
"projectname1",
|
"projectname1",
|
||||||
|
nil,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -253,6 +255,7 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) {
|
|||||||
project.NewProjectRemovedEvent(context.Background(),
|
project.NewProjectRemovedEvent(context.Background(),
|
||||||
&project.NewAggregate("project1", "org1").Aggregate,
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
"projectname1",
|
"projectname1",
|
||||||
|
nil,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -503,6 +506,7 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) {
|
|||||||
project.NewProjectRemovedEvent(context.Background(),
|
project.NewProjectRemovedEvent(context.Background(),
|
||||||
&project.NewAggregate("project1", "org1").Aggregate,
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
"projectname1",
|
"projectname1",
|
||||||
|
nil,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -269,7 +269,8 @@ func TestCommandSide_ChangeProject(t *testing.T) {
|
|||||||
eventFromEventPusher(
|
eventFromEventPusher(
|
||||||
project.NewProjectRemovedEvent(context.Background(),
|
project.NewProjectRemovedEvent(context.Background(),
|
||||||
&project.NewAggregate("project1", "org1").Aggregate,
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
"project"),
|
"project",
|
||||||
|
nil),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -542,7 +543,8 @@ func TestCommandSide_DeactivateProject(t *testing.T) {
|
|||||||
eventFromEventPusher(
|
eventFromEventPusher(
|
||||||
project.NewProjectRemovedEvent(context.Background(),
|
project.NewProjectRemovedEvent(context.Background(),
|
||||||
&project.NewAggregate("project1", "org1").Aggregate,
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
"project"),
|
"project",
|
||||||
|
nil),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -721,7 +723,8 @@ func TestCommandSide_ReactivateProject(t *testing.T) {
|
|||||||
eventFromEventPusher(
|
eventFromEventPusher(
|
||||||
project.NewProjectRemovedEvent(context.Background(),
|
project.NewProjectRemovedEvent(context.Background(),
|
||||||
&project.NewAggregate("project1", "org1").Aggregate,
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
"project"),
|
"project",
|
||||||
|
nil),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -900,7 +903,8 @@ func TestCommandSide_RemoveProject(t *testing.T) {
|
|||||||
eventFromEventPusher(
|
eventFromEventPusher(
|
||||||
project.NewProjectRemovedEvent(context.Background(),
|
project.NewProjectRemovedEvent(context.Background(),
|
||||||
&project.NewAggregate("project1", "org1").Aggregate,
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
"project"),
|
"project",
|
||||||
|
nil),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -915,7 +919,7 @@ func TestCommandSide_RemoveProject(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "project remove, ok",
|
name: "project remove, without entityConstraints, ok",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
eventstore: eventstoreExpect(
|
eventstore: eventstoreExpect(
|
||||||
t,
|
t,
|
||||||
@ -927,12 +931,15 @@ func TestCommandSide_RemoveProject(t *testing.T) {
|
|||||||
domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy),
|
domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// no saml application events
|
||||||
|
expectFilter(),
|
||||||
expectPush(
|
expectPush(
|
||||||
[]*repository.Event{
|
[]*repository.Event{
|
||||||
eventFromEventPusher(
|
eventFromEventPusher(
|
||||||
project.NewProjectRemovedEvent(context.Background(),
|
project.NewProjectRemovedEvent(context.Background(),
|
||||||
&project.NewAggregate("project1", "org1").Aggregate,
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
"project"),
|
"project",
|
||||||
|
nil),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
uniqueConstraintsFromEventConstraint(project.NewRemoveProjectNameUniqueConstraint("project", "org1")),
|
uniqueConstraintsFromEventConstraint(project.NewRemoveProjectNameUniqueConstraint("project", "org1")),
|
||||||
@ -950,6 +957,150 @@ func TestCommandSide_RemoveProject(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "project remove, with entityConstraints, ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewProjectAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"project", true, true, true,
|
||||||
|
domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app1",
|
||||||
|
"app",
|
||||||
|
)),
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewSAMLConfigAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app1",
|
||||||
|
"https://test.com/saml/metadata",
|
||||||
|
[]byte("<?xml version=\"1.0\"?>\n<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n validUntil=\"2022-08-26T14:08:16Z\"\n cacheDuration=\"PT604800S\"\n entityID=\"https://test.com/saml/metadata\">\n <md:SPSSODescriptor AuthnRequestsSigned=\"false\" WantAssertionsSigned=\"false\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>\n <md:AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n Location=\"https://test.com/saml/acs\"\n index=\"1\" />\n \n </md:SPSSODescriptor>\n</md:EntityDescriptor>"),
|
||||||
|
"http://localhost:8080/saml/metadata",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewProjectRemovedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"project",
|
||||||
|
[]*eventstore.EventUniqueConstraint{
|
||||||
|
project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata"),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
uniqueConstraintsFromEventConstraint(project.NewRemoveProjectNameUniqueConstraint("project", "org1")),
|
||||||
|
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
projectID: "project1",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "project remove, with multiple entityConstraints, ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewProjectAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"project", true, true, true,
|
||||||
|
domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app1",
|
||||||
|
"app",
|
||||||
|
)),
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewSAMLConfigAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app1",
|
||||||
|
"https://test1.com/saml/metadata",
|
||||||
|
[]byte("<?xml version=\"1.0\"?>\n<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n validUntil=\"2022-08-26T14:08:16Z\"\n cacheDuration=\"PT604800S\"\n entityID=\"https://test.com/saml/metadata\">\n <md:SPSSODescriptor AuthnRequestsSigned=\"false\" WantAssertionsSigned=\"false\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>\n <md:AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n Location=\"https://test.com/saml/acs\"\n index=\"1\" />\n \n </md:SPSSODescriptor>\n</md:EntityDescriptor>"),
|
||||||
|
"",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app2",
|
||||||
|
"app",
|
||||||
|
)),
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewSAMLConfigAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app2",
|
||||||
|
"https://test2.com/saml/metadata",
|
||||||
|
[]byte("<?xml version=\"1.0\"?>\n<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n validUntil=\"2022-08-26T14:08:16Z\"\n cacheDuration=\"PT604800S\"\n entityID=\"https://test.com/saml/metadata\">\n <md:SPSSODescriptor AuthnRequestsSigned=\"false\" WantAssertionsSigned=\"false\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>\n <md:AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n Location=\"https://test.com/saml/acs\"\n index=\"1\" />\n \n </md:SPSSODescriptor>\n</md:EntityDescriptor>"),
|
||||||
|
"",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app3",
|
||||||
|
"app",
|
||||||
|
)),
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewSAMLConfigAddedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"app3",
|
||||||
|
"https://test3.com/saml/metadata",
|
||||||
|
[]byte("<?xml version=\"1.0\"?>\n<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n validUntil=\"2022-08-26T14:08:16Z\"\n cacheDuration=\"PT604800S\"\n entityID=\"https://test.com/saml/metadata\">\n <md:SPSSODescriptor AuthnRequestsSigned=\"false\" WantAssertionsSigned=\"false\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>\n <md:AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n Location=\"https://test.com/saml/acs\"\n index=\"1\" />\n \n </md:SPSSODescriptor>\n</md:EntityDescriptor>"),
|
||||||
|
"",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(
|
||||||
|
project.NewProjectRemovedEvent(context.Background(),
|
||||||
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
|
"project",
|
||||||
|
[]*eventstore.EventUniqueConstraint{
|
||||||
|
project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test1.com/saml/metadata"),
|
||||||
|
project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test2.com/saml/metadata"),
|
||||||
|
project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test3.com/saml/metadata"),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
uniqueConstraintsFromEventConstraint(project.NewRemoveProjectNameUniqueConstraint("project", "org1")),
|
||||||
|
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test1.com/saml/metadata")),
|
||||||
|
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test2.com/saml/metadata")),
|
||||||
|
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test3.com/saml/metadata")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
projectID: "project1",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/zitadel/logging"
|
"github.com/zitadel/logging"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/repository/idpconfig"
|
"github.com/zitadel/zitadel/internal/repository/idpconfig"
|
||||||
@ -92,6 +93,10 @@ func (rm *UniqueConstraintReadModel) Reduce() error {
|
|||||||
rm.addUniqueConstraint(e.Aggregate().ID, e.AppID, project.NewAddApplicationUniqueConstraint(e.Name, e.Aggregate().ID))
|
rm.addUniqueConstraint(e.Aggregate().ID, e.AppID, project.NewAddApplicationUniqueConstraint(e.Name, e.Aggregate().ID))
|
||||||
case *project.ApplicationChangedEvent:
|
case *project.ApplicationChangedEvent:
|
||||||
rm.changeUniqueConstraint(e.Aggregate().ID, e.AppID, project.NewAddApplicationUniqueConstraint(e.Name, e.Aggregate().ID))
|
rm.changeUniqueConstraint(e.Aggregate().ID, e.AppID, project.NewAddApplicationUniqueConstraint(e.Name, e.Aggregate().ID))
|
||||||
|
case *project.SAMLConfigAddedEvent:
|
||||||
|
rm.addUniqueConstraint(e.Aggregate().ID, e.AppID, project.NewAddSAMLConfigEntityIDUniqueConstraint(e.EntityID))
|
||||||
|
case *project.SAMLConfigChangedEvent:
|
||||||
|
rm.addUniqueConstraint(e.Aggregate().ID, e.AppID, project.NewRemoveSAMLConfigEntityIDUniqueConstraint(e.EntityID))
|
||||||
case *project.ApplicationRemovedEvent:
|
case *project.ApplicationRemovedEvent:
|
||||||
rm.removeUniqueConstraint(e.Aggregate().ID, e.AppID, project.UniqueAppNameType)
|
rm.removeUniqueConstraint(e.Aggregate().ID, e.AppID, project.UniqueAppNameType)
|
||||||
case *project.GrantAddedEvent:
|
case *project.GrantAddedEvent:
|
||||||
|
@ -133,6 +133,7 @@ func TestCommandSide_AddUserGrant(t *testing.T) {
|
|||||||
project.NewProjectRemovedEvent(context.Background(),
|
project.NewProjectRemovedEvent(context.Background(),
|
||||||
&project.NewAggregate("project1", "org1").Aggregate,
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
"projectname1",
|
"projectname1",
|
||||||
|
nil,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -819,6 +820,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) {
|
|||||||
project.NewProjectRemovedEvent(context.Background(),
|
project.NewProjectRemovedEvent(context.Background(),
|
||||||
&project.NewAggregate("project1", "org1").Aggregate,
|
&project.NewAggregate("project1", "org1").Aggregate,
|
||||||
"projectname1",
|
"projectname1",
|
||||||
|
nil,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -40,4 +40,6 @@ type KeyConfig struct {
|
|||||||
Size int
|
Size int
|
||||||
PrivateKeyLifetime time.Duration
|
PrivateKeyLifetime time.Duration
|
||||||
PublicKeyLifetime time.Duration
|
PublicKeyLifetime time.Duration
|
||||||
|
CertificateSize int
|
||||||
|
CertificateLifetime time.Duration
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
package crypto
|
package crypto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateKeyPair(bits int) (*rsa.PrivateKey, *rsa.PublicKey, error) {
|
func GenerateKeyPair(bits int) (*rsa.PrivateKey, *rsa.PublicKey, error) {
|
||||||
@ -24,6 +29,104 @@ func GenerateEncryptedKeyPair(bits int, alg EncryptionAlgorithm) (*CryptoValue,
|
|||||||
return EncryptKeys(privateKey, publicKey, alg)
|
return EncryptKeys(privateKey, publicKey, alg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CertificateInformations struct {
|
||||||
|
SerialNumber *big.Int
|
||||||
|
Organisation []string
|
||||||
|
CommonName string
|
||||||
|
NotBefore time.Time
|
||||||
|
NotAfter time.Time
|
||||||
|
KeyUsage x509.KeyUsage
|
||||||
|
ExtKeyUsage []x509.ExtKeyUsage
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateEncryptedKeyPairWithCACertificate(bits int, keyAlg, certAlg EncryptionAlgorithm, informations *CertificateInformations) (*CryptoValue, *CryptoValue, *CryptoValue, error) {
|
||||||
|
privateKey, publicKey, cert, err := GenerateCACertificate(bits, informations)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
encryptPriv, encryptPub, encryptCaCert, err := EncryptKeysAndCert(privateKey, publicKey, cert, keyAlg, certAlg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
return encryptPriv, encryptPub, encryptCaCert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateEncryptedKeyPairWithCertificate(bits int, keyAlg, certAlg EncryptionAlgorithm, caPrivateKey *rsa.PrivateKey, caCertificate []byte, informations *CertificateInformations) (*CryptoValue, *CryptoValue, *CryptoValue, error) {
|
||||||
|
privateKey, publicKey, cert, err := GenerateCertificate(bits, caPrivateKey, caCertificate, informations)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
encryptPriv, encryptPub, encryptCaCert, err := EncryptKeysAndCert(privateKey, publicKey, cert, keyAlg, certAlg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
return encryptPriv, encryptPub, encryptCaCert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateCACertificate(bits int, informations *CertificateInformations) (*rsa.PrivateKey, *rsa.PublicKey, []byte, error) {
|
||||||
|
return generateCertificate(bits, nil, nil, informations)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateCertificate(bits int, caPrivateKey *rsa.PrivateKey, ca []byte, informations *CertificateInformations) (*rsa.PrivateKey, *rsa.PublicKey, []byte, error) {
|
||||||
|
return generateCertificate(bits, caPrivateKey, ca, informations)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateCertificate(bits int, caPrivateKey *rsa.PrivateKey, ca []byte, informations *CertificateInformations) (*rsa.PrivateKey, *rsa.PublicKey, []byte, error) {
|
||||||
|
notBefore := time.Now()
|
||||||
|
if !informations.NotBefore.IsZero() {
|
||||||
|
notBefore = informations.NotBefore
|
||||||
|
}
|
||||||
|
cert := &x509.Certificate{
|
||||||
|
SerialNumber: informations.SerialNumber,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: informations.CommonName,
|
||||||
|
Organization: informations.Organisation,
|
||||||
|
},
|
||||||
|
NotBefore: notBefore,
|
||||||
|
NotAfter: informations.NotAfter,
|
||||||
|
KeyUsage: informations.KeyUsage,
|
||||||
|
ExtKeyUsage: informations.ExtKeyUsage,
|
||||||
|
}
|
||||||
|
|
||||||
|
certPrivKey, err := rsa.GenerateKey(rand.Reader, bits)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
certBytes := make([]byte, 0)
|
||||||
|
if ca == nil {
|
||||||
|
cert.IsCA = true
|
||||||
|
cert.BasicConstraintsValid = true
|
||||||
|
|
||||||
|
certBytes, err = x509.CreateCertificate(rand.Reader, cert, cert, &certPrivKey.PublicKey, certPrivKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
caCert, err := x509.ParseCertificate(ca)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
certBytes, err = x509.CreateCertificate(rand.Reader, cert, caCert, &certPrivKey.PublicKey, caPrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
x509Cert, err := x509.ParseCertificate(certBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
certPem, err := CertificateToBytes(x509Cert)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return certPrivKey, &certPrivKey.PublicKey, certPem, nil
|
||||||
|
}
|
||||||
|
|
||||||
func PrivateKeyToBytes(priv *rsa.PrivateKey) []byte {
|
func PrivateKeyToBytes(priv *rsa.PrivateKey) []byte {
|
||||||
return pem.EncodeToMemory(
|
return pem.EncodeToMemory(
|
||||||
&pem.Block{
|
&pem.Block{
|
||||||
@ -101,3 +204,34 @@ func EncryptKeys(privateKey *rsa.PrivateKey, publicKey *rsa.PublicKey, alg Encry
|
|||||||
}
|
}
|
||||||
return encryptedPrivateKey, encryptedPublicKey, nil
|
return encryptedPrivateKey, encryptedPublicKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CertificateToBytes(cert *x509.Certificate) ([]byte, error) {
|
||||||
|
certPem := new(bytes.Buffer)
|
||||||
|
if err := pem.Encode(certPem, &pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: cert.Raw,
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return certPem.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func BytesToCertificate(data []byte) ([]byte, error) {
|
||||||
|
block, _ := pem.Decode(data)
|
||||||
|
if block == nil || block.Type != "CERTIFICATE" {
|
||||||
|
return nil, fmt.Errorf("failed to decode PEM block containing public key")
|
||||||
|
}
|
||||||
|
return block.Bytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncryptKeysAndCert(privateKey *rsa.PrivateKey, publicKey *rsa.PublicKey, cert []byte, keyAlg, certAlg EncryptionAlgorithm) (*CryptoValue, *CryptoValue, *CryptoValue, error) {
|
||||||
|
encryptedPrivateKey, encryptedPublicKey, err := EncryptKeys(privateKey, publicKey, keyAlg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
encryptedCertificate, err := Encrypt(cert, certAlg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
return encryptedPrivateKey, encryptedPublicKey, encryptedCertificate, nil
|
||||||
|
}
|
||||||
|
40
internal/domain/application_saml.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SAMLApp struct {
|
||||||
|
models.ObjectRoot
|
||||||
|
|
||||||
|
AppID string
|
||||||
|
AppName string
|
||||||
|
EntityID string
|
||||||
|
Metadata []byte
|
||||||
|
MetadataURL string
|
||||||
|
|
||||||
|
State AppState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SAMLApp) GetApplicationName() string {
|
||||||
|
return a.AppName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SAMLApp) GetState() AppState {
|
||||||
|
return a.State
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SAMLApp) GetMetadata() []byte {
|
||||||
|
return a.Metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SAMLApp) GetMetadataURL() string {
|
||||||
|
return a.MetadataURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SAMLApp) IsValid() bool {
|
||||||
|
if a.MetadataURL == "" && a.Metadata == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|