mirror of
https://github.com/zitadel/zitadel.git
synced 2025-02-28 22:47:23 +00:00
Merge branch 'main' into docker-bake
This commit is contained in:
commit
ed84e7a77a
27
cmd/setup/50.go
Normal file
27
cmd/setup/50.go
Normal file
@ -0,0 +1,27 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed 50.sql
|
||||
addUsePKCE string
|
||||
)
|
||||
|
||||
type IDPTemplate6UsePKCE struct {
|
||||
dbClient *database.DB
|
||||
}
|
||||
|
||||
func (mig *IDPTemplate6UsePKCE) Execute(ctx context.Context, _ eventstore.Event) error {
|
||||
_, err := mig.dbClient.ExecContext(ctx, addUsePKCE)
|
||||
return err
|
||||
}
|
||||
|
||||
func (mig *IDPTemplate6UsePKCE) String() string {
|
||||
return "50_idp_templates6_add_use_pkce"
|
||||
}
|
2
cmd/setup/50.sql
Normal file
2
cmd/setup/50.sql
Normal file
@ -0,0 +1,2 @@
|
||||
ALTER TABLE IF EXISTS projections.idp_templates6_oauth2 ADD COLUMN IF NOT EXISTS use_pkce BOOLEAN;
|
||||
ALTER TABLE IF EXISTS projections.idp_templates6_oidc ADD COLUMN IF NOT EXISTS use_pkce BOOLEAN;
|
@ -138,6 +138,7 @@ type Steps struct {
|
||||
s47FillMembershipFields *FillMembershipFields
|
||||
s48Apps7SAMLConfigsLoginVersion *Apps7SAMLConfigsLoginVersion
|
||||
s49InitPermittedOrgsFunction *InitPermittedOrgsFunction
|
||||
s50IDPTemplate6UsePKCE *IDPTemplate6UsePKCE
|
||||
}
|
||||
|
||||
func MustNewSteps(v *viper.Viper) *Steps {
|
||||
|
@ -175,6 +175,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
steps.s47FillMembershipFields = &FillMembershipFields{eventstore: eventstoreClient}
|
||||
steps.s48Apps7SAMLConfigsLoginVersion = &Apps7SAMLConfigsLoginVersion{dbClient: dbClient}
|
||||
steps.s49InitPermittedOrgsFunction = &InitPermittedOrgsFunction{eventstoreClient: dbClient}
|
||||
steps.s50IDPTemplate6UsePKCE = &IDPTemplate6UsePKCE{dbClient: dbClient}
|
||||
|
||||
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil)
|
||||
logging.OnError(err).Fatal("unable to start projections")
|
||||
@ -240,6 +241,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
steps.s46InitPermissionFunctions,
|
||||
steps.s47FillMembershipFields,
|
||||
steps.s49InitPermittedOrgsFunction,
|
||||
steps.s50IDPTemplate6UsePKCE,
|
||||
} {
|
||||
mustExecuteMigration(ctx, eventstoreClient, step, "migration failed")
|
||||
}
|
||||
|
@ -143,10 +143,6 @@ type Server struct {
|
||||
func startZitadel(ctx context.Context, config *Config, masterKey string, server chan<- *Server) error {
|
||||
showBasicInformation(config)
|
||||
|
||||
// sink Server is stubbed out in production builds, see function's godoc.
|
||||
closeSink := sink.StartServer()
|
||||
defer closeSink()
|
||||
|
||||
i18n.MustLoadSupportedLanguagesFromDir()
|
||||
|
||||
dbClient, err := database.Connect(config.Database, false)
|
||||
@ -254,6 +250,10 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
|
||||
}
|
||||
defer commands.Close(ctx) // wait for background jobs
|
||||
|
||||
// sink Server is stubbed out in production builds, see function's godoc.
|
||||
closeSink := sink.StartServer(commands)
|
||||
defer closeSink()
|
||||
|
||||
clock := clockpkg.New()
|
||||
actionsExecutionStdoutEmitter, err := logstore.NewEmitter[*record.ExecutionLog](ctx, clock, &logstore.EmitterConfig{Enabled: config.LogStore.Execution.Stdout.Enabled}, stdout.NewStdoutEmitter[*record.ExecutionLog]())
|
||||
if err != nil {
|
||||
@ -560,7 +560,7 @@ func startAPIs(
|
||||
if err := apis.RegisterService(ctx, oidc_v2beta.CreateServer(commands, queries, oidcServer, config.ExternalSecure)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, oidc_v2.CreateServer(commands, queries, oidcServer, config.ExternalSecure)); err != nil {
|
||||
if err := apis.RegisterService(ctx, oidc_v2.CreateServer(commands, queries, oidcServer, config.ExternalSecure, keys.OIDC)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// After SAML provider so that the callback endpoint can be used
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, DestroyRef, OnInit } from '@angular/core';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { take } from 'rxjs';
|
||||
@ -27,9 +27,10 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
router: Router,
|
||||
destroyRef: DestroyRef,
|
||||
protected override route: ActivatedRoute,
|
||||
) {
|
||||
super(router, route);
|
||||
super(router, route, destroyRef);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, DestroyRef, OnInit } from '@angular/core';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { take } from 'rxjs';
|
||||
@ -23,8 +23,8 @@ export class FilterProjectComponent extends FilterComponent implements OnInit {
|
||||
public searchQueries: ProjectQuery[] = [];
|
||||
|
||||
public states: ProjectState[] = [ProjectState.PROJECT_STATE_ACTIVE, ProjectState.PROJECT_STATE_INACTIVE];
|
||||
constructor(router: Router, route: ActivatedRoute) {
|
||||
super(router, route);
|
||||
constructor(router: Router, route: ActivatedRoute, destroyRef: DestroyRef) {
|
||||
super(router, route, destroyRef);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, DestroyRef, OnInit } from '@angular/core';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { take } from 'rxjs';
|
||||
@ -29,8 +29,8 @@ export class FilterUserGrantsComponent extends FilterComponent implements OnInit
|
||||
public SubQuery: any = SubQuery;
|
||||
public searchQueries: UserGrantQuery[] = [];
|
||||
|
||||
constructor(router: Router, route: ActivatedRoute) {
|
||||
super(router, route);
|
||||
constructor(router: Router, route: ActivatedRoute, destroyRef: DestroyRef) {
|
||||
super(router, route, destroyRef);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -1,4 +1,4 @@
|
||||
<cnsl-filter (resetted)="resetFilter()" (trigger)="emitFilter()" [queryCount]="searchQueries.length">
|
||||
<cnsl-filter (resetted)="resetFilter()" (trigger)="emitFilter()" [queryCount]="(filterChanged | async)?.length ?? 0">
|
||||
<div class="filter-row" id="filtercomp">
|
||||
<div class="name-query">
|
||||
<mat-checkbox
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, DestroyRef, OnInit } from '@angular/core';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { take } from 'rxjs';
|
||||
@ -13,6 +13,7 @@ import {
|
||||
} from 'src/app/proto/generated/zitadel/user_pb';
|
||||
|
||||
import { FilterComponent } from '../filter/filter.component';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
|
||||
enum SubQuery {
|
||||
STATE,
|
||||
@ -28,25 +29,27 @@ enum SubQuery {
|
||||
})
|
||||
export class FilterUserComponent extends FilterComponent implements OnInit {
|
||||
public SubQuery: any = SubQuery;
|
||||
public searchQueries: UserSearchQuery[] = [];
|
||||
private searchQueries: UserSearchQuery[] = [];
|
||||
|
||||
public states: UserState[] = [
|
||||
UserState.USER_STATE_ACTIVE,
|
||||
UserState.USER_STATE_INACTIVE,
|
||||
UserState.USER_STATE_DELETED,
|
||||
UserState.USER_STATE_INITIAL,
|
||||
UserState.USER_STATE_LOCKED,
|
||||
UserState.USER_STATE_SUSPEND,
|
||||
UserState.USER_STATE_INITIAL,
|
||||
];
|
||||
constructor(router: Router, route: ActivatedRoute) {
|
||||
super(router, route);
|
||||
constructor(router: Router, route: ActivatedRoute, destroyRef: DestroyRef) {
|
||||
super(router, route, destroyRef);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.queryParams.pipe(take(1)).subscribe((params) => {
|
||||
const { filter } = params;
|
||||
if (filter) {
|
||||
const stringifiedFilters = filter as string;
|
||||
this.route.queryParamMap
|
||||
.pipe(
|
||||
take(1),
|
||||
map((params) => params.get('filter')),
|
||||
filter(Boolean),
|
||||
)
|
||||
.subscribe((stringifiedFilters) => {
|
||||
const filters: UserSearchQuery.AsObject[] = JSON.parse(stringifiedFilters) as UserSearchQuery.AsObject[];
|
||||
|
||||
const userQueries = filters.map((filter) => {
|
||||
@ -94,8 +97,7 @@ export class FilterUserComponent extends FilterComponent implements OnInit {
|
||||
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
|
||||
// this.showFilter = true;
|
||||
// this.filterOpen.emit(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public changeCheckbox(subquery: SubQuery, event: MatCheckboxChange) {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { ConnectedPosition, ConnectionPositionPair } from '@angular/cdk/overlay';
|
||||
import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core';
|
||||
import { Component, DestroyRef, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Observable, Subject, takeUntil } from 'rxjs';
|
||||
import { SearchQuery as MemberSearchQuery } from 'src/app/proto/generated/zitadel/member_pb';
|
||||
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
|
||||
import { OrgQuery } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
@ -9,6 +8,7 @@ import { ProjectQuery } from 'src/app/proto/generated/zitadel/project_pb';
|
||||
import { SearchQuery as UserSearchQuery, UserGrantQuery } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
|
||||
import { ActionKeysType } from '../action-keys/action-keys.component';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
type FilterSearchQuery = UserSearchQuery | MemberSearchQuery | UserGrantQuery | ProjectQuery | OrgQuery;
|
||||
type FilterSearchQueryAsObject =
|
||||
@ -23,7 +23,7 @@ type FilterSearchQueryAsObject =
|
||||
templateUrl: './filter.component.html',
|
||||
styleUrls: ['./filter.component.scss'],
|
||||
})
|
||||
export class FilterComponent implements OnDestroy {
|
||||
export class FilterComponent {
|
||||
@Output() public filterChanged: EventEmitter<FilterSearchQuery[]> = new EventEmitter();
|
||||
@Output() public filterOpen: EventEmitter<boolean> = new EventEmitter<boolean>(false);
|
||||
|
||||
@ -32,9 +32,6 @@ export class FilterComponent implements OnDestroy {
|
||||
|
||||
@Input() public queryCount: number = 0;
|
||||
|
||||
private destroy$: Subject<void> = new Subject();
|
||||
public filterChanged$: Observable<FilterSearchQuery[]> = this.filterChanged.asObservable();
|
||||
|
||||
public showFilter: boolean = false;
|
||||
public methods: TextQueryMethod[] = [
|
||||
TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE,
|
||||
@ -59,17 +56,13 @@ export class FilterComponent implements OnDestroy {
|
||||
this.trigger.emit();
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
protected route: ActivatedRoute,
|
||||
destroyRef: DestroyRef,
|
||||
) {
|
||||
const changes$ = this.filterChanged.asObservable();
|
||||
changes$.pipe(takeUntil(this.destroy$)).subscribe((queries) => {
|
||||
changes$.pipe(takeUntilDestroyed(destroyRef)).subscribe((queries) => {
|
||||
const filters: Array<FilterSearchQueryAsObject | {}> | undefined = queries
|
||||
?.map((q) => q.toObject())
|
||||
.map((query) =>
|
||||
@ -81,15 +74,17 @@ export class FilterComponent implements OnDestroy {
|
||||
);
|
||||
|
||||
if (filters && Object.keys(filters)) {
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: {
|
||||
['filter']: JSON.stringify(filters),
|
||||
},
|
||||
replaceUrl: true,
|
||||
queryParamsHandling: 'merge',
|
||||
skipLocationChange: false,
|
||||
});
|
||||
this.router
|
||||
.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: {
|
||||
['filter']: JSON.stringify(filters),
|
||||
},
|
||||
replaceUrl: true,
|
||||
queryParamsHandling: 'merge',
|
||||
skipLocationChange: false,
|
||||
})
|
||||
.then();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -110,6 +110,15 @@
|
||||
</cnsl-form-field>
|
||||
</div>
|
||||
|
||||
<div class="specific-oauth-option">
|
||||
<cnsl-info-section>
|
||||
<div>
|
||||
<p class="checkbox-desc">{{ 'IDP.USEPKCE_DESC' | translate }}</p>
|
||||
<mat-checkbox formControlName="usePkce">{{ 'IDP.USEPKCE' | translate }}</mat-checkbox>
|
||||
</div>
|
||||
</cnsl-info-section>
|
||||
</div>
|
||||
|
||||
<cnsl-provider-options
|
||||
[initialOptions]="provider?.config?.options"
|
||||
(optionsChanged)="options = $event"
|
||||
|
@ -0,0 +1,8 @@
|
||||
.specific-oauth-option {
|
||||
max-width: 500px;
|
||||
|
||||
.checkbox-desc {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
@ -28,6 +28,7 @@ import { ProviderNextService } from '../provider-next/provider-next.service';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-provider-oauth',
|
||||
styleUrls: ['./provider-oauth.component.scss'],
|
||||
templateUrl: './provider-oauth.component.html',
|
||||
})
|
||||
export class ProviderOAuthComponent {
|
||||
@ -88,6 +89,7 @@ export class ProviderOAuthComponent {
|
||||
userEndpoint: new UntypedFormControl('', [requiredValidator]),
|
||||
idAttribute: new UntypedFormControl('', [requiredValidator]),
|
||||
scopesList: new UntypedFormControl(['openid', 'profile', 'email'], []),
|
||||
usePkce: new UntypedFormControl(false),
|
||||
});
|
||||
|
||||
this.authService
|
||||
@ -187,6 +189,7 @@ export class ProviderOAuthComponent {
|
||||
req.setClientSecret(this.clientSecret?.value);
|
||||
req.setScopesList(this.scopesList?.value);
|
||||
req.setProviderOptions(this.options);
|
||||
req.setUsePkce(this.usePkce?.value);
|
||||
|
||||
this.loading = true;
|
||||
this.service
|
||||
@ -217,6 +220,7 @@ export class ProviderOAuthComponent {
|
||||
req.setClientSecret(this.clientSecret?.value);
|
||||
req.setScopesList(this.scopesList?.value);
|
||||
req.setProviderOptions(this.options);
|
||||
req.setUsePkce(this.usePkce?.value);
|
||||
|
||||
this.loading = true;
|
||||
this.service
|
||||
@ -297,4 +301,8 @@ export class ProviderOAuthComponent {
|
||||
public get scopesList(): AbstractControl | null {
|
||||
return this.form.get('scopesList');
|
||||
}
|
||||
|
||||
public get usePkce(): AbstractControl | null {
|
||||
return this.form.get('usePkce');
|
||||
}
|
||||
}
|
||||
|
@ -92,14 +92,22 @@
|
||||
</cnsl-form-field>
|
||||
</div>
|
||||
|
||||
<div class="id-token-mapping">
|
||||
<div class="specific-oidc-option">
|
||||
<cnsl-info-section>
|
||||
<div>
|
||||
<p class="checkbox-desc">{{ 'IDP.ISIDTOKENMAPPING_DESC' | translate }}</p>
|
||||
<mat-checkbox formControlName="isIdTokenMapping">{{ 'IDP.ISIDTOKENMAPPING' | translate }}</mat-checkbox>
|
||||
</div>
|
||||
</cnsl-info-section>
|
||||
|
||||
<cnsl-info-section>
|
||||
<div>
|
||||
<p class="checkbox-desc">{{ 'IDP.USEPKCE_DESC' | translate }}</p>
|
||||
<mat-checkbox formControlName="usePkce">{{ 'IDP.USEPKCE' | translate }}</mat-checkbox>
|
||||
</div>
|
||||
</cnsl-info-section>
|
||||
</div>
|
||||
|
||||
<cnsl-provider-options
|
||||
[initialOptions]="provider?.config?.options"
|
||||
(optionsChanged)="options = $event"
|
||||
|
@ -1,5 +1,7 @@
|
||||
.id-token-mapping {
|
||||
max-width: 400px;
|
||||
.specific-oidc-option {
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.checkbox-desc {
|
||||
margin-top: 0;
|
||||
|
@ -85,6 +85,7 @@ export class ProviderOIDCComponent {
|
||||
issuer: new UntypedFormControl('', [requiredValidator]),
|
||||
scopesList: new UntypedFormControl(['openid', 'profile', 'email'], []),
|
||||
isIdTokenMapping: new UntypedFormControl(),
|
||||
usePkce: new UntypedFormControl(false),
|
||||
});
|
||||
|
||||
this.route.data.pipe(take(1)).subscribe((data) => {
|
||||
@ -165,6 +166,7 @@ export class ProviderOIDCComponent {
|
||||
req.setScopesList(this.scopesList?.value);
|
||||
req.setProviderOptions(this.options);
|
||||
req.setIsIdTokenMapping(this.isIdTokenMapping?.value);
|
||||
req.setUsePkce(this.usePkce?.value);
|
||||
|
||||
this.loading = true;
|
||||
this.service
|
||||
@ -193,6 +195,7 @@ export class ProviderOIDCComponent {
|
||||
req.setScopesList(this.scopesList?.value);
|
||||
req.setProviderOptions(this.options);
|
||||
req.setIsIdTokenMapping(this.isIdTokenMapping?.value);
|
||||
req.setUsePkce(this.usePkce?.value);
|
||||
|
||||
this.loading = true;
|
||||
this.service
|
||||
@ -261,4 +264,8 @@ export class ProviderOIDCComponent {
|
||||
public get isIdTokenMapping(): AbstractControl | null {
|
||||
return this.form.get('isIdTokenMapping');
|
||||
}
|
||||
|
||||
public get usePkce(): AbstractControl | null {
|
||||
return this.form.get('usePkce');
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,11 @@
|
||||
<cnsl-top-view
|
||||
title="{{ userName$ | async }}"
|
||||
sub="{{ user(userQuery)?.preferredLoginName }}"
|
||||
[isActive]="user(userQuery)?.state === UserState.USER_STATE_ACTIVE"
|
||||
[isInactive]="user(userQuery)?.state === UserState.USER_STATE_INACTIVE"
|
||||
[isActive]="user(userQuery)?.state === UserState.ACTIVE"
|
||||
[isInactive]="user(userQuery)?.state === UserState.INACTIVE"
|
||||
stateTooltip="{{ 'USER.STATE.' + user(userQuery)?.state | translate }}"
|
||||
[hasBackButton]="['org.read'] | hasRole | async"
|
||||
>
|
||||
<span *ngIf="userQuery.state === 'notfound'">{{ 'USER.PAGES.NOUSER' | translate }}</span>
|
||||
<cnsl-info-row
|
||||
topContent
|
||||
*ngIf="user(userQuery) as user"
|
||||
@ -42,7 +41,7 @@
|
||||
[disabled]="false"
|
||||
[genders]="genders"
|
||||
[languages]="(langSvc.supported$ | async) || []"
|
||||
[username]="user.userName"
|
||||
[username]="user.username"
|
||||
[profile]="profile"
|
||||
[showEditImage]="true"
|
||||
(changedLanguage)="changedLanguage($event)"
|
||||
@ -93,7 +92,7 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'idp'">
|
||||
<cnsl-external-idps [userId]="user.id" [service]="grpcAuthService"></cnsl-external-idps>
|
||||
<cnsl-external-idps [userId]="user.userId" [service]="grpcAuthService"></cnsl-external-idps>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'security'">
|
||||
@ -124,15 +123,14 @@
|
||||
<cnsl-auth-passwordless #mfaComponent></cnsl-auth-passwordless>
|
||||
|
||||
<cnsl-auth-user-mfa
|
||||
[phoneVerified]="humanUser(userQuery)?.type?.value?.phone?.isPhoneVerified ?? false"
|
||||
#mfaComponent
|
||||
[phoneVerified]="humanUser(userQuery)?.type?.value?.phone?.isVerified ?? false"
|
||||
></cnsl-auth-user-mfa>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'grants'">
|
||||
<cnsl-card title="{{ 'GRANTS.USER.TITLE' | translate }}" description="{{ 'GRANTS.USER.DESCRIPTION' | translate }}">
|
||||
<cnsl-user-grants
|
||||
[userId]="user.id"
|
||||
[userId]="user.userId"
|
||||
[context]="USERGRANTCONTEXT"
|
||||
[displayedColumns]="[
|
||||
'org',
|
||||
@ -165,7 +163,7 @@
|
||||
*ngIf="metadataQuery.state !== 'error'"
|
||||
[metadata]="metadataQuery.value"
|
||||
[description]="'DESCRIPTIONS.USERS.SELF.METADATA' | translate"
|
||||
[disabled]="(['user.write:' + user.id, 'user.write'] | hasRole | async) === false"
|
||||
[disabled]="(['user.write:' + user.userId, 'user.write'] | hasRole | async) === false"
|
||||
(editClicked)="editMetadata(user, metadataQuery.value)"
|
||||
(refresh)="refreshMetadata$.next(true)"
|
||||
[loading]="metadataQuery.state === 'loading'"
|
||||
|
@ -36,11 +36,10 @@ import { ToastService } from 'src/app/services/toast.service';
|
||||
import { formatPhone } from 'src/app/utils/formatPhone';
|
||||
import { EditDialogComponent, EditDialogData, EditDialogResult, EditDialogType } from './edit-dialog/edit-dialog.component';
|
||||
import { LanguagesService } from 'src/app/services/languages.service';
|
||||
import { Gender, HumanProfile } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { Gender, HumanProfile, HumanUser, User, UserState } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { catchError, filter, map, startWith, tap, withLatestFrom } from 'rxjs/operators';
|
||||
import { pairwiseStartWith } from 'src/app/utils/pairwiseStartWith';
|
||||
import { NewAuthService } from 'src/app/services/new-auth.service';
|
||||
import { Human, User, UserState } from '@zitadel/proto/zitadel/user_pb';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { NewMgmtService } from 'src/app/services/new-mgmt.service';
|
||||
import { Metadata } from '@zitadel/proto/zitadel/metadata_pb';
|
||||
@ -48,18 +47,14 @@ import { UserService } from 'src/app/services/user.service';
|
||||
import { LoginPolicy } from '@zitadel/proto/zitadel/policy_pb';
|
||||
import { query } from '@angular/animations';
|
||||
|
||||
type UserQuery =
|
||||
| { state: 'success'; value: User }
|
||||
| { state: 'error'; value: string }
|
||||
| { state: 'loading'; value?: User }
|
||||
| { state: 'notfound' };
|
||||
type UserQuery = { state: 'success'; value: User } | { state: 'error'; value: string } | { state: 'loading'; value?: User };
|
||||
|
||||
type MetadataQuery =
|
||||
| { state: 'success'; value: Metadata[] }
|
||||
| { state: 'loading'; value: Metadata[] }
|
||||
| { state: 'error'; value: string };
|
||||
|
||||
type UserWithHumanType = Omit<User, 'type'> & { type: { case: 'human'; value: Human } };
|
||||
type UserWithHumanType = Omit<User, 'type'> & { type: { case: 'human'; value: HumanUser } };
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-auth-user-detail',
|
||||
@ -67,17 +62,17 @@ type UserWithHumanType = Omit<User, 'type'> & { type: { case: 'human'; value: Hu
|
||||
styleUrls: ['./auth-user-detail.component.scss'],
|
||||
})
|
||||
export class AuthUserDetailComponent implements OnInit {
|
||||
public genders: Gender[] = [Gender.MALE, Gender.FEMALE, Gender.DIVERSE];
|
||||
protected readonly genders: Gender[] = [Gender.MALE, Gender.FEMALE, Gender.DIVERSE];
|
||||
|
||||
public ChangeType: any = ChangeType;
|
||||
protected readonly ChangeType = ChangeType;
|
||||
public userLoginMustBeDomain: boolean = false;
|
||||
public UserState: any = UserState;
|
||||
protected readonly UserState = UserState;
|
||||
|
||||
public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.AUTHUSER;
|
||||
public refreshChanges$: EventEmitter<void> = new EventEmitter();
|
||||
public refreshMetadata$ = new Subject<true>();
|
||||
protected USERGRANTCONTEXT: UserGrantContext = UserGrantContext.AUTHUSER;
|
||||
protected readonly refreshChanges$: EventEmitter<void> = new EventEmitter();
|
||||
protected readonly refreshMetadata$ = new Subject<true>();
|
||||
|
||||
public settingsList: SidenavSetting[] = [
|
||||
protected readonly settingsList: SidenavSetting[] = [
|
||||
{ id: 'general', i18nKey: 'USER.SETTINGS.GENERAL' },
|
||||
{ id: 'security', i18nKey: 'USER.SETTINGS.SECURITY' },
|
||||
{ id: 'idp', i18nKey: 'USER.SETTINGS.IDP' },
|
||||
@ -92,9 +87,9 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
protected readonly user$: Observable<UserQuery>;
|
||||
protected readonly metadata$: Observable<MetadataQuery>;
|
||||
private readonly savedLanguage$: Observable<string>;
|
||||
protected currentSetting$: Observable<string | undefined>;
|
||||
public loginPolicy$: Observable<LoginPolicy>;
|
||||
protected userName$: Observable<string>;
|
||||
protected readonly currentSetting$: Observable<string | undefined>;
|
||||
protected readonly loginPolicy$: Observable<LoginPolicy>;
|
||||
protected readonly userName$: Observable<string>;
|
||||
|
||||
constructor(
|
||||
public translate: TranslateService,
|
||||
@ -209,13 +204,8 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
}
|
||||
|
||||
private getMyUser(): Observable<UserQuery> {
|
||||
return defer(() => this.newAuthService.getMyUser()).pipe(
|
||||
map(({ user }) => {
|
||||
if (user) {
|
||||
return { state: 'success', value: user } as const;
|
||||
}
|
||||
return { state: 'notfound' } as const;
|
||||
}),
|
||||
return defer(() => this.userService.getMyUser()).pipe(
|
||||
map((user) => ({ state: 'success' as const, value: user })),
|
||||
catchError((error) => of({ state: 'error', value: error.message ?? '' } as const)),
|
||||
startWith({ state: 'loading' } as const),
|
||||
);
|
||||
@ -232,7 +222,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
if (!user.value) {
|
||||
return EMPTY;
|
||||
}
|
||||
return this.getMetadataById(user.value.id);
|
||||
return this.getMetadataById(user.value.userId);
|
||||
}),
|
||||
pairwiseStartWith(undefined),
|
||||
map(([prev, curr]) => {
|
||||
@ -259,7 +249,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
labelKey: 'ACTIONS.NEWVALUE' as const,
|
||||
titleKey: 'USER.PROFILE.CHANGEUSERNAME_TITLE' as const,
|
||||
descriptionKey: 'USER.PROFILE.CHANGEUSERNAME_DESC' as const,
|
||||
value: user.userName,
|
||||
value: user.username,
|
||||
};
|
||||
const dialogRef = this.dialog.open<EditDialogComponent, typeof data, EditDialogResult>(EditDialogComponent, {
|
||||
data,
|
||||
@ -271,8 +261,8 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
.pipe(
|
||||
map((value) => value?.value),
|
||||
filter(Boolean),
|
||||
filter((value) => user.userName != value),
|
||||
switchMap((username) => this.userService.updateUser({ userId: user.id, username })),
|
||||
filter((value) => user.username != value),
|
||||
switchMap((username) => this.userService.updateUser({ userId: user.userId, username })),
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
@ -288,7 +278,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
public saveProfile(user: User, profile: HumanProfile): void {
|
||||
this.userService
|
||||
.updateUser({
|
||||
userId: user.id,
|
||||
userId: user.userId,
|
||||
profile: {
|
||||
givenName: profile.givenName,
|
||||
familyName: profile.familyName,
|
||||
@ -350,7 +340,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
|
||||
public resendEmailVerification(user: User): void {
|
||||
this.newMgmtService
|
||||
.resendHumanEmailVerification(user.id)
|
||||
.resendHumanEmailVerification(user.userId)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true);
|
||||
this.refreshChanges$.emit();
|
||||
@ -362,7 +352,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
|
||||
public resendPhoneVerification(user: User): void {
|
||||
this.newMgmtService
|
||||
.resendHumanPhoneVerification(user.id)
|
||||
.resendHumanPhoneVerification(user.userId)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
|
||||
this.refreshChanges$.emit();
|
||||
@ -374,7 +364,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
|
||||
public deletePhone(user: User): void {
|
||||
this.userService
|
||||
.removePhone(user.id)
|
||||
.removePhone(user.userId)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
|
||||
this.refreshChanges$.emit();
|
||||
@ -417,7 +407,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
filter((resp): resp is Required<EditDialogResult> => !!resp?.value),
|
||||
switchMap(({ value, isVerified }) =>
|
||||
this.userService.setEmail({
|
||||
userId: user.id,
|
||||
userId: user.userId,
|
||||
email: value,
|
||||
verification: isVerified ? { case: 'isVerified', value: isVerified } : { case: undefined },
|
||||
}),
|
||||
@ -453,7 +443,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
.pipe(
|
||||
map((resp) => formatPhone(resp?.value)),
|
||||
filter(Boolean),
|
||||
switchMap(({ phone }) => this.userService.setPhone({ userId: user.id, phone })),
|
||||
switchMap(({ phone }) => this.userService.setPhone({ userId: user.userId, phone })),
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
@ -482,7 +472,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(() => this.userService.deleteUser(user.id)),
|
||||
switchMap(() => this.userService.deleteUser(user.userId)),
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
@ -498,9 +488,9 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
this.newMgmtService.setUserMetadata({
|
||||
key,
|
||||
value: Buffer.from(value),
|
||||
id: user.id,
|
||||
id: user.userId,
|
||||
});
|
||||
const removeFcn = (key: string): Promise<any> => this.newMgmtService.removeUserMetadata({ key, id: user.id });
|
||||
const removeFcn = (key: string): Promise<any> => this.newMgmtService.removeUserMetadata({ key, id: user.userId });
|
||||
|
||||
const dialogRef = this.dialog.open<MetadataDialogComponent, MetadataDialogData>(MetadataDialogComponent, {
|
||||
data: {
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="content">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'USER.MACHINE.USERNAME' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="userName" required />
|
||||
<input cnslInput formControlName="username" required />
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'USER.MACHINE.NAME' | translate }}</cnsl-label>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div class="max-width-container">
|
||||
<div class="enlarged-container" [ngSwitch]="type">
|
||||
<div class="enlarged-container">
|
||||
<div class="users-title-row">
|
||||
<h1>{{ 'DESCRIPTIONS.USERS.TITLE' | translate }}</h1>
|
||||
<a mat-icon-button href="https://zitadel.com/docs/concepts/structure/users" rel="noreferrer" target="_blank">
|
||||
@ -7,21 +7,6 @@
|
||||
</a>
|
||||
</div>
|
||||
<p class="user-list-sub cnsl-secondary-text">{{ 'DESCRIPTIONS.USERS.DESCRIPTION' | translate }}</p>
|
||||
<ng-container *ngSwitchCase="Type.TYPE_HUMAN">
|
||||
<cnsl-user-table
|
||||
[type]="Type.TYPE_HUMAN"
|
||||
[canWrite$]="['user.write$'] | hasRole"
|
||||
[canDelete$]="['user.delete$'] | hasRole"
|
||||
>
|
||||
</cnsl-user-table>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="Type.TYPE_MACHINE">
|
||||
<cnsl-user-table
|
||||
[type]="Type.TYPE_MACHINE"
|
||||
[canWrite$]="['user.write$'] | hasRole"
|
||||
[canDelete$]="['user.delete$'] | hasRole"
|
||||
>
|
||||
</cnsl-user-table>
|
||||
</ng-container>
|
||||
<cnsl-user-table [canWrite$]="['user.write$'] | hasRole" [canDelete$]="['user.delete$'] | hasRole"> </cnsl-user-table>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,8 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { Type } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||
|
||||
@Component({
|
||||
@ -11,23 +8,10 @@ import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/
|
||||
styleUrls: ['./user-list.component.scss'],
|
||||
})
|
||||
export class UserListComponent {
|
||||
public Type: any = Type;
|
||||
public type: Type = Type.TYPE_HUMAN;
|
||||
|
||||
constructor(
|
||||
public translate: TranslateService,
|
||||
activatedRoute: ActivatedRoute,
|
||||
protected readonly translate: TranslateService,
|
||||
breadcrumbService: BreadcrumbService,
|
||||
) {
|
||||
activatedRoute.queryParams.pipe(take(1)).subscribe((params: Params) => {
|
||||
const { type } = params;
|
||||
if (type && type === 'human') {
|
||||
this.type = Type.TYPE_HUMAN;
|
||||
} else if (type && type === 'machine') {
|
||||
this.type = Type.TYPE_MACHINE;
|
||||
}
|
||||
});
|
||||
|
||||
const bread: Breadcrumb = {
|
||||
type: BreadcrumbType.ORG,
|
||||
routerLink: ['/org'],
|
||||
|
@ -1,11 +1,11 @@
|
||||
<cnsl-refresh-table
|
||||
[loading]="loading$ | async"
|
||||
(refreshed)="refreshPage()"
|
||||
[dataSize]="totalResult"
|
||||
*ngIf="type$ | async as type"
|
||||
[loading]="loading()"
|
||||
(refreshed)="this.refresh$.next(true)"
|
||||
[dataSize]="dataSize()"
|
||||
[hideRefresh]="true"
|
||||
[timestamp]="viewTimestamp"
|
||||
[timestamp]="(users$ | async)?.details?.timestamp"
|
||||
[selection]="selection"
|
||||
[emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes"
|
||||
[showBorder]="true"
|
||||
>
|
||||
<div leftActions class="user-toggle-group">
|
||||
@ -60,12 +60,12 @@
|
||||
<cnsl-filter-user
|
||||
actions
|
||||
*ngIf="!selection.hasValue()"
|
||||
(filterChanged)="applySearchQuery($any($event))"
|
||||
(filterChanged)="this.searchQueries$.next($any($event))"
|
||||
(filterOpen)="filterOpen = $event"
|
||||
></cnsl-filter-user>
|
||||
<ng-template cnslHasRole [hasRole]="['user.write']" actions>
|
||||
<button
|
||||
(click)="gotoRouterLink(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
|
||||
(click)="router.navigate(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
|
||||
color="primary"
|
||||
mat-raised-button
|
||||
[disabled]="(canWrite$ | async) === false"
|
||||
@ -77,7 +77,7 @@
|
||||
<span>{{ 'ACTIONS.NEW' | translate }}</span>
|
||||
<cnsl-action-keys
|
||||
*ngIf="!filterOpen"
|
||||
(actionTriggered)="gotoRouterLink(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
|
||||
(actionTriggered)="router.navigate(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
|
||||
>
|
||||
</cnsl-action-keys>
|
||||
</div>
|
||||
@ -85,7 +85,7 @@
|
||||
</ng-template>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table class="table" mat-table [dataSource]="dataSource" matSort (matSortChange)="sortChange($event)">
|
||||
<table class="table" mat-table [dataSource]="dataSource" matSort>
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<div class="selection">
|
||||
@ -133,12 +133,7 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="displayName">
|
||||
<th
|
||||
mat-header-cell
|
||||
*matHeaderCellDef
|
||||
mat-sort-header
|
||||
[ngClass]="{ 'search-active': this.userSearchKey === UserListSearchKey.DISPLAY_NAME }"
|
||||
>
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||
{{ 'USER.PROFILE.DISPLAYNAME' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
@ -148,12 +143,7 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="preferredLoginName">
|
||||
<th
|
||||
mat-header-cell
|
||||
*matHeaderCellDef
|
||||
mat-sort-header
|
||||
[ngClass]="{ 'search-active': this.userSearchKey === UserListSearchKey.DISPLAY_NAME }"
|
||||
>
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||
{{ 'USER.PROFILE.PREFERREDLOGINNAME' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
@ -162,12 +152,7 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="username">
|
||||
<th
|
||||
mat-header-cell
|
||||
*matHeaderCellDef
|
||||
mat-sort-header
|
||||
[ngClass]="{ 'search-active': this.userSearchKey === UserListSearchKey.USER_NAME }"
|
||||
>
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||
{{ 'USER.PROFILE.USERNAME' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
@ -176,12 +161,7 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="email">
|
||||
<th
|
||||
mat-header-cell
|
||||
*matHeaderCellDef
|
||||
mat-sort-header
|
||||
[ngClass]="{ 'search-active': this.UserListSearchKey === UserListSearchKey.EMAIL }"
|
||||
>
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||
{{ 'USER.EMAIL' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
@ -250,17 +230,16 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div *ngIf="(loading$ | async) === false && !dataSource?.data?.length" class="no-content-row">
|
||||
<div *ngIf="!loading() && !dataSource?.data?.length" class="no-content-row">
|
||||
<i class="las la-exclamation"></i>
|
||||
<span>{{ 'USER.TABLE.EMPTY' | translate }}</span>
|
||||
</div>
|
||||
<cnsl-paginator
|
||||
#paginator
|
||||
class="paginator"
|
||||
[length]="totalResult || 0"
|
||||
[length]="dataSize()"
|
||||
[pageSize]="INITIAL_PAGE_SIZE"
|
||||
[timestamp]="viewTimestamp"
|
||||
[timestamp]="(users$ | async)?.details?.timestamp"
|
||||
[pageSizeOptions]="[10, 20, 50, 100]"
|
||||
(page)="changePage($event)"
|
||||
></cnsl-paginator>
|
||||
<!-- (page)="changePage($event)"-->
|
||||
</cnsl-refresh-table>
|
||||
|
@ -1,34 +1,45 @@
|
||||
import { LiveAnnouncer } from '@angular/cdk/a11y';
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { Component, EventEmitter, Input, OnInit, Output, Signal, ViewChild } from '@angular/core';
|
||||
import { Component, DestroyRef, EventEmitter, Input, OnInit, Output, signal, Signal, ViewChild } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSort, Sort } from '@angular/material/sort';
|
||||
import { MatSort, SortDirection } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import {
|
||||
combineLatestWith,
|
||||
defer,
|
||||
delay,
|
||||
distinctUntilChanged,
|
||||
EMPTY,
|
||||
from,
|
||||
Observable,
|
||||
of,
|
||||
ReplaySubject,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
toArray,
|
||||
} from 'rxjs';
|
||||
import { catchError, filter, finalize, map, startWith, take } from 'rxjs/operators';
|
||||
import { enterAnimations } from 'src/app/animations';
|
||||
import { ActionKeysType } from 'src/app/modules/action-keys/action-keys.component';
|
||||
import { PageEvent, PaginatorComponent } from 'src/app/modules/paginator/paginator.component';
|
||||
import { PaginatorComponent } from 'src/app/modules/paginator/paginator.component';
|
||||
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { UserService } from 'src/app/services/user.service';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { User } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { SearchQuery, SearchQuerySchema, Type, UserFieldName } from '@zitadel/proto/zitadel/user/v2/query_pb';
|
||||
import { UserState, User as UserV2 } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { create } from '@bufbuild/protobuf';
|
||||
import { Timestamp } from '@bufbuild/protobuf/wkt';
|
||||
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
||||
import { SearchQuery as UserSearchQuery } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { Type, UserFieldName } from '@zitadel/proto/zitadel/user/v2/query_pb';
|
||||
import { UserState, User } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { ListUsersRequestSchema, ListUsersResponse } from '@zitadel/proto/zitadel/user/v2/user_service_pb';
|
||||
import { AuthenticationService } from 'src/app/services/authentication.service';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { UserState as UserStateV1 } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
|
||||
enum UserListSearchKey {
|
||||
FIRST_NAME,
|
||||
LAST_NAME,
|
||||
DISPLAY_NAME,
|
||||
USER_NAME,
|
||||
EMAIL,
|
||||
}
|
||||
type Query = Exclude<
|
||||
Exclude<MessageInitShape<typeof ListUsersRequestSchema>['queries'], undefined>[number]['query'],
|
||||
undefined
|
||||
>;
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-user-table',
|
||||
@ -37,25 +48,33 @@ enum UserListSearchKey {
|
||||
animations: [enterAnimations],
|
||||
})
|
||||
export class UserTableComponent implements OnInit {
|
||||
public userSearchKey: UserListSearchKey | undefined = undefined;
|
||||
public Type = Type;
|
||||
@Input() public type: Type = Type.HUMAN;
|
||||
@Input() refreshOnPreviousRoutes: string[] = [];
|
||||
protected readonly Type = Type;
|
||||
protected readonly refresh$ = new ReplaySubject<true>(1);
|
||||
|
||||
@Input() public canWrite$: Observable<boolean> = of(false);
|
||||
@Input() public canDelete$: Observable<boolean> = of(false);
|
||||
|
||||
private user: Signal<User.AsObject | undefined> = toSignal(this.authService.user, { requireSync: true });
|
||||
protected readonly dataSize: Signal<number>;
|
||||
protected readonly loading = signal(false);
|
||||
|
||||
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
|
||||
@ViewChild(MatSort) public sort!: MatSort;
|
||||
public INITIAL_PAGE_SIZE: number = 20;
|
||||
private readonly paginator$ = new ReplaySubject<PaginatorComponent>(1);
|
||||
@ViewChild(PaginatorComponent) public set paginator(paginator: PaginatorComponent) {
|
||||
this.paginator$.next(paginator);
|
||||
}
|
||||
private readonly sort$ = new ReplaySubject<MatSort>(1);
|
||||
@ViewChild(MatSort) public set sort(sort: MatSort) {
|
||||
this.sort$.next(sort);
|
||||
}
|
||||
|
||||
protected readonly INITIAL_PAGE_SIZE = 20;
|
||||
|
||||
protected readonly dataSource: MatTableDataSource<User> = new MatTableDataSource<User>();
|
||||
protected readonly selection: SelectionModel<User> = new SelectionModel<User>(true, []);
|
||||
protected readonly users$: Observable<ListUsersResponse>;
|
||||
protected readonly type$: Observable<Type>;
|
||||
protected readonly searchQueries$ = new ReplaySubject<UserSearchQuery[]>(1);
|
||||
protected readonly myUser: Signal<User | undefined>;
|
||||
|
||||
public viewTimestamp!: Timestamp;
|
||||
public totalResult: number = 0;
|
||||
public dataSource: MatTableDataSource<UserV2> = new MatTableDataSource<UserV2>();
|
||||
public selection: SelectionModel<UserV2> = new SelectionModel<UserV2>(true, []);
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
@Input() public displayedColumnsHuman: string[] = [
|
||||
'select',
|
||||
'displayName',
|
||||
@ -76,46 +95,56 @@ export class UserTableComponent implements OnInit {
|
||||
'actions',
|
||||
];
|
||||
|
||||
@Output() public changedSelection: EventEmitter<Array<UserV2>> = new EventEmitter();
|
||||
@Output() public changedSelection: EventEmitter<Array<User>> = new EventEmitter();
|
||||
|
||||
public UserState: any = UserState;
|
||||
public UserListSearchKey: any = UserListSearchKey;
|
||||
protected readonly UserState = UserState;
|
||||
|
||||
public ActionKeysType: any = ActionKeysType;
|
||||
public filterOpen: boolean = false;
|
||||
protected ActionKeysType = ActionKeysType;
|
||||
protected filterOpen: boolean = false;
|
||||
|
||||
private searchQueries: SearchQuery[] = [];
|
||||
constructor(
|
||||
private router: Router,
|
||||
public translate: TranslateService,
|
||||
private authService: GrpcAuthService,
|
||||
private userService: UserService,
|
||||
private toast: ToastService,
|
||||
private dialog: MatDialog,
|
||||
private route: ActivatedRoute,
|
||||
private _liveAnnouncer: LiveAnnouncer,
|
||||
protected readonly router: Router,
|
||||
public readonly translate: TranslateService,
|
||||
private readonly userService: UserService,
|
||||
private readonly toast: ToastService,
|
||||
private readonly dialog: MatDialog,
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
private readonly authenticationService: AuthenticationService,
|
||||
private readonly authService: GrpcAuthService,
|
||||
) {
|
||||
this.selection.changed.subscribe(() => {
|
||||
this.changedSelection.emit(this.selection.selected);
|
||||
});
|
||||
this.type$ = this.getType$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.users$ = this.getUsers(this.type$).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.myUser = toSignal(this.getMyUser());
|
||||
|
||||
this.dataSize = toSignal(
|
||||
this.users$.pipe(
|
||||
map((users) => users.result.length),
|
||||
distinctUntilChanged(),
|
||||
),
|
||||
{ initialValue: 0 },
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.queryParams.pipe(take(1)).subscribe((params) => {
|
||||
if (!params['filter']) {
|
||||
this.getData(this.INITIAL_PAGE_SIZE, 0, this.type, this.searchQueries).then();
|
||||
}
|
||||
|
||||
if (params['deferredReload']) {
|
||||
setTimeout(() => {
|
||||
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize, this.type).then();
|
||||
}, 2000);
|
||||
}
|
||||
this.selection.changed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||
this.changedSelection.emit(this.selection.selected);
|
||||
});
|
||||
|
||||
this.users$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((users) => (this.dataSource.data = users.result));
|
||||
|
||||
this.route.queryParamMap
|
||||
.pipe(
|
||||
map((params) => params.get('deferredReload')),
|
||||
filter(Boolean),
|
||||
take(1),
|
||||
delay(2000),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe(() => this.refresh$.next(true));
|
||||
}
|
||||
|
||||
public setType(type: Type): void {
|
||||
this.type = type;
|
||||
setType(type: Type) {
|
||||
this.router
|
||||
.navigate([], {
|
||||
relativeTo: this.route,
|
||||
@ -127,12 +156,195 @@ export class UserTableComponent implements OnInit {
|
||||
skipLocationChange: false,
|
||||
})
|
||||
.then();
|
||||
this.getData(
|
||||
this.paginator.pageSize,
|
||||
this.paginator.pageIndex * this.paginator.pageSize,
|
||||
this.type,
|
||||
this.searchQueries,
|
||||
).then();
|
||||
}
|
||||
|
||||
private getMyUser() {
|
||||
return defer(() => this.userService.getMyUser()).pipe(
|
||||
catchError((error) => {
|
||||
this.toast.showError(error);
|
||||
return EMPTY;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private getType$(): Observable<Type> {
|
||||
return this.route.queryParamMap.pipe(
|
||||
map((params) => params.get('type')),
|
||||
filter(Boolean),
|
||||
map((type) => (type === 'machine' ? Type.MACHINE : Type.HUMAN)),
|
||||
startWith(Type.HUMAN),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
}
|
||||
|
||||
private getDirection$() {
|
||||
return this.sort$.pipe(
|
||||
switchMap((sort) =>
|
||||
sort.sortChange.pipe(
|
||||
map(({ direction }) => direction),
|
||||
startWith(sort.direction),
|
||||
),
|
||||
),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
}
|
||||
|
||||
private getSortingColumn$() {
|
||||
return this.sort$.pipe(
|
||||
switchMap((sort) =>
|
||||
sort.sortChange.pipe(
|
||||
map(({ active }) => active),
|
||||
startWith(sort.active),
|
||||
),
|
||||
),
|
||||
map((active) => {
|
||||
switch (active) {
|
||||
case 'displayName':
|
||||
return UserFieldName.DISPLAY_NAME;
|
||||
case 'username':
|
||||
return UserFieldName.USER_NAME;
|
||||
case 'preferredLoginName':
|
||||
// TODO: replace with preferred username sorting once implemented
|
||||
return UserFieldName.USER_NAME;
|
||||
case 'email':
|
||||
return UserFieldName.EMAIL;
|
||||
case 'state':
|
||||
return UserFieldName.STATE;
|
||||
case 'creationDate':
|
||||
return UserFieldName.CREATION_DATE;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
}
|
||||
|
||||
private getQueries(type$: Observable<Type>): Observable<Query[]> {
|
||||
const activeOrgId$ = this.getActiveOrgId();
|
||||
|
||||
return this.searchQueries$.pipe(
|
||||
startWith([]),
|
||||
combineLatestWith(type$, activeOrgId$),
|
||||
switchMap(([queries, type, organizationId]) =>
|
||||
from(queries).pipe(
|
||||
map((query) => this.searchQueryToV2(query.toObject())),
|
||||
startWith({ case: 'typeQuery' as const, value: { type } }),
|
||||
startWith(organizationId ? { case: 'organizationIdQuery' as const, value: { organizationId } } : undefined),
|
||||
filter(Boolean),
|
||||
toArray(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private searchQueryToV2(query: UserSearchQuery.AsObject): Query | undefined {
|
||||
if (query.userNameQuery) {
|
||||
return {
|
||||
case: 'userNameQuery' as const,
|
||||
value: {
|
||||
userName: query.userNameQuery.userName,
|
||||
method: query.userNameQuery.method as unknown as any,
|
||||
},
|
||||
};
|
||||
} else if (query.displayNameQuery) {
|
||||
return {
|
||||
case: 'displayNameQuery' as const,
|
||||
value: {
|
||||
displayName: query.displayNameQuery.displayName,
|
||||
method: query.displayNameQuery.method as unknown as any,
|
||||
},
|
||||
};
|
||||
} else if (query.emailQuery) {
|
||||
return {
|
||||
case: 'emailQuery' as const,
|
||||
value: {
|
||||
emailAddress: query.emailQuery.emailAddress,
|
||||
method: query.emailQuery.method as unknown as any,
|
||||
},
|
||||
};
|
||||
} else if (query.stateQuery) {
|
||||
return {
|
||||
case: 'stateQuery' as const,
|
||||
value: {
|
||||
state: this.toV2State(query.stateQuery.state),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private toV2State(state: UserStateV1) {
|
||||
switch (state) {
|
||||
case UserStateV1.USER_STATE_ACTIVE:
|
||||
return UserState.ACTIVE;
|
||||
case UserStateV1.USER_STATE_INACTIVE:
|
||||
return UserState.INACTIVE;
|
||||
case UserStateV1.USER_STATE_DELETED:
|
||||
return UserState.DELETED;
|
||||
case UserStateV1.USER_STATE_LOCKED:
|
||||
return UserState.LOCKED;
|
||||
case UserStateV1.USER_STATE_INITIAL:
|
||||
return UserState.INITIAL;
|
||||
default:
|
||||
throw new Error(`Invalid UserState ${state}`);
|
||||
}
|
||||
}
|
||||
|
||||
private getUsers(type$: Observable<Type>) {
|
||||
const queries$ = this.getQueries(type$);
|
||||
const direction$ = this.getDirection$();
|
||||
const sortingColumn$ = this.getSortingColumn$();
|
||||
|
||||
const page$ = this.paginator$.pipe(switchMap((paginator) => paginator.page));
|
||||
const pageSize$ = page$.pipe(
|
||||
map(({ pageSize }) => pageSize),
|
||||
startWith(this.INITIAL_PAGE_SIZE),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const pageIndex$ = page$.pipe(
|
||||
map(({ pageIndex }) => pageIndex),
|
||||
startWith(0),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
return this.refresh$.pipe(
|
||||
startWith(true),
|
||||
combineLatestWith(queries$, direction$, sortingColumn$, pageSize$, pageIndex$),
|
||||
switchMap(([_, queries, direction, sortingColumn, pageSize, pageIndex]) => {
|
||||
return this.fetchUsers(queries, direction, sortingColumn, pageSize, pageIndex);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private fetchUsers(
|
||||
queries: Query[],
|
||||
direction: SortDirection,
|
||||
sortingColumn: UserFieldName | undefined,
|
||||
pageSize: number,
|
||||
pageIndex: number,
|
||||
) {
|
||||
return defer(() => {
|
||||
const req = {
|
||||
query: {
|
||||
limit: pageSize,
|
||||
offset: BigInt(pageIndex * pageSize),
|
||||
asc: direction === 'asc',
|
||||
},
|
||||
sortingColumn,
|
||||
queries: queries.map((query) => ({ query })),
|
||||
};
|
||||
|
||||
this.loading.set(true);
|
||||
return this.userService.listUsers(req);
|
||||
}).pipe(
|
||||
catchError((error) => {
|
||||
this.toast.showError(error);
|
||||
return EMPTY;
|
||||
}),
|
||||
finalize(() => this.loading.set(false)),
|
||||
);
|
||||
}
|
||||
|
||||
public isAllSelected(): boolean {
|
||||
@ -145,147 +357,49 @@ export class UserTableComponent implements OnInit {
|
||||
this.isAllSelected() ? this.selection.clear() : this.dataSource.data.forEach((row) => this.selection.select(row));
|
||||
}
|
||||
|
||||
public changePage(event: PageEvent): void {
|
||||
this.selection.clear();
|
||||
this.getData(event.pageSize, event.pageIndex * event.pageSize, this.type, this.searchQueries).then();
|
||||
}
|
||||
|
||||
public deactivateSelectedUsers(): void {
|
||||
public async deactivateSelectedUsers(): Promise<void> {
|
||||
const usersToDeactivate = this.selection.selected
|
||||
.filter((u) => u.state === UserState.ACTIVE)
|
||||
.map((value) => {
|
||||
return this.userService.deactivateUser(value.userId);
|
||||
});
|
||||
|
||||
Promise.all(usersToDeactivate)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true);
|
||||
this.selection.clear();
|
||||
setTimeout(() => {
|
||||
this.refreshPage();
|
||||
}, 1000);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
try {
|
||||
await Promise.all(usersToDeactivate);
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true);
|
||||
this.selection.clear();
|
||||
setTimeout(() => {
|
||||
this.refresh$.next(true);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
public reactivateSelectedUsers(): void {
|
||||
public async reactivateSelectedUsers(): Promise<void> {
|
||||
const usersToReactivate = this.selection.selected
|
||||
.filter((u) => u.state === UserState.INACTIVE)
|
||||
.map((value) => {
|
||||
return this.userService.reactivateUser(value.userId);
|
||||
});
|
||||
|
||||
Promise.all(usersToReactivate)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.SELECTEDREACTIVATED', true);
|
||||
this.selection.clear();
|
||||
setTimeout(() => {
|
||||
this.refreshPage();
|
||||
}, 1000);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
public gotoRouterLink(rL: any): Promise<boolean> {
|
||||
return this.router.navigate(rL);
|
||||
}
|
||||
|
||||
private async getData(limit: number, offset: number, type: Type, searchQueries?: SearchQuery[]): Promise<void> {
|
||||
this.loadingSubject.next(true);
|
||||
|
||||
let queryT = create(SearchQuerySchema, {
|
||||
query: {
|
||||
case: 'typeQuery',
|
||||
value: {
|
||||
type,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let sortingField: UserFieldName | undefined = undefined;
|
||||
if (this.sort?.active && this.sort?.direction)
|
||||
switch (this.sort.active) {
|
||||
case 'displayName':
|
||||
sortingField = UserFieldName.DISPLAY_NAME;
|
||||
break;
|
||||
case 'username':
|
||||
sortingField = UserFieldName.USER_NAME;
|
||||
break;
|
||||
case 'preferredLoginName':
|
||||
// TODO: replace with preferred username sorting once implemented
|
||||
sortingField = UserFieldName.USER_NAME;
|
||||
break;
|
||||
case 'email':
|
||||
sortingField = UserFieldName.EMAIL;
|
||||
break;
|
||||
case 'state':
|
||||
sortingField = UserFieldName.STATE;
|
||||
break;
|
||||
case 'creationDate':
|
||||
sortingField = UserFieldName.CREATION_DATE;
|
||||
break;
|
||||
}
|
||||
|
||||
this.userService
|
||||
.listUsers(
|
||||
limit,
|
||||
offset,
|
||||
searchQueries?.length ? [queryT, ...searchQueries] : [queryT],
|
||||
sortingField,
|
||||
this.sort?.direction,
|
||||
)
|
||||
.then((resp) => {
|
||||
if (resp.details?.totalResult) {
|
||||
this.totalResult = Number(resp.details.totalResult);
|
||||
} else {
|
||||
this.totalResult = 0;
|
||||
}
|
||||
if (resp.details?.timestamp) {
|
||||
this.viewTimestamp = resp.details?.timestamp;
|
||||
}
|
||||
this.dataSource.data = resp.result;
|
||||
this.loadingSubject.next(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
this.loadingSubject.next(false);
|
||||
});
|
||||
}
|
||||
|
||||
public refreshPage(): void {
|
||||
this.getData(
|
||||
this.paginator.pageSize,
|
||||
this.paginator.pageIndex * this.paginator.pageSize,
|
||||
this.type,
|
||||
this.searchQueries,
|
||||
).then();
|
||||
}
|
||||
|
||||
public sortChange(sortState: Sort) {
|
||||
if (sortState.direction && sortState.active) {
|
||||
this._liveAnnouncer.announce(`Sorted ${sortState.direction} ending`).then();
|
||||
this.refreshPage();
|
||||
} else {
|
||||
this._liveAnnouncer.announce('Sorting cleared').then();
|
||||
try {
|
||||
await Promise.all(usersToReactivate);
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public applySearchQuery(searchQueries: SearchQuery[]): void {
|
||||
this.toast.showInfo('USER.TOAST.SELECTEDREACTIVATED', true);
|
||||
this.selection.clear();
|
||||
this.searchQueries = searchQueries;
|
||||
this.getData(
|
||||
this.paginator ? this.paginator.pageSize : this.INITIAL_PAGE_SIZE,
|
||||
this.paginator ? this.paginator.pageIndex * this.paginator.pageSize : 0,
|
||||
this.type,
|
||||
searchQueries,
|
||||
).then();
|
||||
setTimeout(() => {
|
||||
this.refresh$.next(true);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
public deleteUser(user: UserV2): void {
|
||||
public deleteUser(user: User): void {
|
||||
const authUserData = {
|
||||
confirmKey: 'ACTIONS.DELETE',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
@ -309,8 +423,9 @@ export class UserTableComponent implements OnInit {
|
||||
};
|
||||
|
||||
if (user?.userId) {
|
||||
const authUser = this.user();
|
||||
const isMe = authUser?.id === user.userId;
|
||||
const authUser = this.myUser();
|
||||
console.log('my user', authUser);
|
||||
const isMe = authUser?.userId === user.userId;
|
||||
|
||||
let dialogRef;
|
||||
|
||||
@ -326,22 +441,22 @@ export class UserTableComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
dialogRef.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
this.userService
|
||||
.deleteUser(user.userId)
|
||||
.then(() => {
|
||||
setTimeout(() => {
|
||||
this.refreshPage();
|
||||
}, 1000);
|
||||
this.selection.clear();
|
||||
this.toast.showInfo('USER.TOAST.DELETED', true);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(() => this.userService.deleteUser(user.userId)),
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
setTimeout(() => {
|
||||
this.refresh$.next(true);
|
||||
}, 1000);
|
||||
this.selection.clear();
|
||||
this.toast.showInfo('USER.TOAST.DELETED', true);
|
||||
},
|
||||
error: (err) => this.toast.showError(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -354,4 +469,20 @@ export class UserTableComponent implements OnInit {
|
||||
const selected = this.selection.selected;
|
||||
return selected ? selected.findIndex((user) => user.state !== UserState.INACTIVE) > -1 : false;
|
||||
}
|
||||
|
||||
private getActiveOrgId() {
|
||||
return this.authenticationService.authenticationChanged.pipe(
|
||||
startWith(true),
|
||||
filter(Boolean),
|
||||
switchMap(() =>
|
||||
from(this.authService.getActiveOrg()).pipe(
|
||||
catchError((err) => {
|
||||
this.toast.showError(err);
|
||||
return of(undefined);
|
||||
}),
|
||||
),
|
||||
),
|
||||
map((org) => org?.id),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { DestroyRef, Injectable } from '@angular/core';
|
||||
import { GrpcService } from './grpc.service';
|
||||
import {
|
||||
AddHumanUserRequestSchema,
|
||||
@ -11,7 +11,6 @@ import {
|
||||
DeactivateUserResponse,
|
||||
DeleteUserRequestSchema,
|
||||
DeleteUserResponse,
|
||||
GetUserByIDRequestSchema,
|
||||
GetUserByIDResponse,
|
||||
ListAuthenticationFactorsRequestSchema,
|
||||
ListAuthenticationFactorsResponse,
|
||||
@ -65,60 +64,87 @@ import {
|
||||
} from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { create } from '@bufbuild/protobuf';
|
||||
import { Timestamp as TimestampV2, TimestampSchema } from '@bufbuild/protobuf/wkt';
|
||||
import { Details, DetailsSchema, ListQuerySchema } from '@zitadel/proto/zitadel/object/v2/object_pb';
|
||||
import { SearchQuery, UserFieldName } from '@zitadel/proto/zitadel/user/v2/query_pb';
|
||||
import { SortDirection } from '@angular/material/sort';
|
||||
import { Details, DetailsSchema } from '@zitadel/proto/zitadel/object/v2/object_pb';
|
||||
import { Human, Machine, Phone, Profile, User } from '../proto/generated/zitadel/user_pb';
|
||||
import { ObjectDetails } from '../proto/generated/zitadel/object_pb';
|
||||
import { Timestamp } from '../proto/generated/google/protobuf/timestamp_pb';
|
||||
import { HumanPhone, HumanPhoneSchema } from '@zitadel/proto/zitadel/user/v2/phone_pb';
|
||||
import { OAuthService } from 'angular-oauth2-oidc';
|
||||
import { firstValueFrom, Observable, shareReplay } from 'rxjs';
|
||||
import { filter, map, startWith, tap, timeout } from 'rxjs/operators';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UserService {
|
||||
constructor(private readonly grpcService: GrpcService) {}
|
||||
private readonly userId$: Observable<string>;
|
||||
private user: UserV2 | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly grpcService: GrpcService,
|
||||
private readonly oauthService: OAuthService,
|
||||
destroyRef: DestroyRef,
|
||||
) {
|
||||
this.userId$ = this.getUserId().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
|
||||
// this preloads the userId and deletes the cache everytime the userId changes
|
||||
this.userId$.pipe(takeUntilDestroyed(destroyRef)).subscribe(async () => {
|
||||
this.user = undefined;
|
||||
try {
|
||||
await this.getMyUser();
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getUserId() {
|
||||
return this.oauthService.events.pipe(
|
||||
filter((event) => event.type === 'token_received'),
|
||||
startWith(this.oauthService.getIdToken),
|
||||
map(() => this.oauthService.getIdToken()),
|
||||
filter(Boolean),
|
||||
// split jwt and get base64 encoded payload
|
||||
map((token) => token.split('.')[1]),
|
||||
// decode payload
|
||||
map(atob),
|
||||
// parse payload
|
||||
map((payload) => JSON.parse(payload)),
|
||||
map((payload: unknown) => {
|
||||
// check if sub is in payload and is a string
|
||||
if (payload && typeof payload === 'object' && 'sub' in payload && typeof payload.sub === 'string') {
|
||||
return payload.sub;
|
||||
}
|
||||
throw new Error('Invalid payload');
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public addHumanUser(req: MessageInitShape<typeof AddHumanUserRequestSchema>): Promise<AddHumanUserResponse> {
|
||||
return this.grpcService.userNew.addHumanUser(create(AddHumanUserRequestSchema, req));
|
||||
}
|
||||
|
||||
public listUsers(
|
||||
limit: number,
|
||||
offset: number,
|
||||
queriesList?: SearchQuery[],
|
||||
sortingColumn?: UserFieldName,
|
||||
sortingDirection?: SortDirection,
|
||||
): Promise<ListUsersResponse> {
|
||||
const query = create(ListQuerySchema);
|
||||
|
||||
if (limit) {
|
||||
query.limit = limit;
|
||||
}
|
||||
if (offset) {
|
||||
query.offset = BigInt(offset);
|
||||
}
|
||||
if (sortingDirection) {
|
||||
query.asc = sortingDirection === 'asc';
|
||||
}
|
||||
|
||||
const req = create(ListUsersRequestSchema, {
|
||||
query,
|
||||
});
|
||||
|
||||
if (sortingColumn) {
|
||||
req.sortingColumn = sortingColumn;
|
||||
}
|
||||
|
||||
if (queriesList) {
|
||||
req.queries = queriesList;
|
||||
}
|
||||
|
||||
public listUsers(req: MessageInitShape<typeof ListUsersRequestSchema>): Promise<ListUsersResponse> {
|
||||
return this.grpcService.userNew.listUsers(req);
|
||||
}
|
||||
|
||||
public async getMyUser(): Promise<UserV2> {
|
||||
const userId = await firstValueFrom(this.userId$.pipe(timeout(2000)));
|
||||
if (this.user) {
|
||||
return this.user;
|
||||
}
|
||||
const resp = await this.getUserById(userId);
|
||||
if (!resp.user) {
|
||||
throw new Error("Couldn't find user");
|
||||
}
|
||||
|
||||
this.user = resp.user;
|
||||
return resp.user;
|
||||
}
|
||||
|
||||
public getUserById(userId: string): Promise<GetUserByIDResponse> {
|
||||
return this.grpcService.userNew.getUserByID(create(GetUserByIDRequestSchema, { userId }));
|
||||
return this.grpcService.userNew.getUserByID({ userId });
|
||||
}
|
||||
|
||||
public deactivateUser(userId: string): Promise<DeactivateUserResponse> {
|
||||
|
@ -2200,7 +2200,9 @@
|
||||
"REMOVED": "Премахнато успешно."
|
||||
},
|
||||
"ISIDTOKENMAPPING": "Съответствие от ID токен",
|
||||
"ISIDTOKENMAPPING_DESC": "Ако е избрано, информацията на доставчика се съответства от ID токена, а не от userinfo крайната точка."
|
||||
"ISIDTOKENMAPPING_DESC": "Ако е избрано, информацията на доставчика се съответства от ID токена, а не от userinfo крайната точка.",
|
||||
"USEPKCE": "Използвайте PKCE",
|
||||
"USEPKCE_DESC": "Определя дали параметрите code_challenge и code_challenge_method са включени в заявката за удостоверяване"
|
||||
},
|
||||
"MFA": {
|
||||
"LIST": {
|
||||
|
@ -2213,7 +2213,9 @@
|
||||
"REMOVED": "Úspěšně odebráno."
|
||||
},
|
||||
"ISIDTOKENMAPPING": "Mapování z ID tokenu",
|
||||
"ISIDTOKENMAPPING_DESC": "Pokud je vybráno, informace o poskytovateli jsou mapovány z ID tokenu, nikoli z koncového bodu userinfo."
|
||||
"ISIDTOKENMAPPING_DESC": "Pokud je vybráno, informace o poskytovateli jsou mapovány z ID tokenu, nikoli z koncového bodu userinfo.",
|
||||
"USEPKCE": "Použijte PKCE",
|
||||
"USEPKCE_DESC": "Určuje, zda jsou v požadavku na ověření zahrnuty parametry code_challenge a code_challenge_method"
|
||||
},
|
||||
"MFA": {
|
||||
"LIST": {
|
||||
|
@ -2204,7 +2204,9 @@
|
||||
"REMOVED": "Erfolgreich entfernt."
|
||||
},
|
||||
"ISIDTOKENMAPPING": "Zuordnung vom ID-Token",
|
||||
"ISIDTOKENMAPPING_DESC": "Legt fest, ob für das Mapping der Provider Informationen das ID-Token verwendet werden soll, anstatt des Userinfo-Endpoints."
|
||||
"ISIDTOKENMAPPING_DESC": "Legt fest, ob für das Mapping der Provider Informationen das ID-Token verwendet werden soll, anstatt des Userinfo-Endpoints.",
|
||||
"USEPKCE": "Verwenden Sie PKCE",
|
||||
"USEPKCE_DESC": "Bestimmt, ob die Parameter code_challenge und code_challenge_method in der Authentifizierungsanforderung enthalten sind"
|
||||
},
|
||||
"MFA": {
|
||||
"LIST": {
|
||||
|
@ -2225,7 +2225,9 @@
|
||||
"REMOVED": "Removed successfully."
|
||||
},
|
||||
"ISIDTOKENMAPPING": "Map from the ID token",
|
||||
"ISIDTOKENMAPPING_DESC": "If selected, provider information gets mapped from the ID token, not from the userinfo endpoint."
|
||||
"ISIDTOKENMAPPING_DESC": "If selected, provider information gets mapped from the ID token, not from the userinfo endpoint.",
|
||||
"USEPKCE": "Use PKCE",
|
||||
"USEPKCE_DESC": "Determines whether the code_challenge and code_challenge_method params are included in the auth request"
|
||||
},
|
||||
"MFA": {
|
||||
"LIST": {
|
||||
|
@ -2201,7 +2201,9 @@
|
||||
"REMOVED": "Eliminado con éxito."
|
||||
},
|
||||
"ISIDTOKENMAPPING": "Asignación del ID token",
|
||||
"ISIDTOKENMAPPING_DESC": "Si se selecciona, la información del proveedor se asigna desde el ID token, no desde el punto final de userinfo."
|
||||
"ISIDTOKENMAPPING_DESC": "Si se selecciona, la información del proveedor se asigna desde el ID token, no desde el punto final de userinfo.",
|
||||
"USEPKCE": "Usa PKCE",
|
||||
"USEPKCE_DESC": "Determina si los parámetros code_challenge y code_challenge_method son incluidos en la solicitud de autenticación."
|
||||
},
|
||||
"MFA": {
|
||||
"LIST": {
|
||||
|
@ -2205,7 +2205,9 @@
|
||||
"REMOVED": "Suppression réussie."
|
||||
},
|
||||
"ISIDTOKENMAPPING": "Mappage depuis le jeton ID",
|
||||
"ISIDTOKENMAPPING_DESC": "Si sélectionné, les informations du fournisseur sont mappées à partir du jeton ID, et non à partir du point d'extrémité userinfo."
|
||||
"ISIDTOKENMAPPING_DESC": "Si sélectionné, les informations du fournisseur sont mappées à partir du jeton ID, et non à partir du point d'extrémité userinfo.",
|
||||
"USEPKCE": "Utiliser PKCE",
|
||||
"USEPKCE_DESC": "Détermine si les paramètres code_challenge et code_challenge_method sont inclus dans la demande d'authentification"
|
||||
},
|
||||
"MFA": {
|
||||
"LIST": {
|
||||
|
@ -2223,7 +2223,9 @@
|
||||
"REMOVED": "Sikeresen eltávolítva."
|
||||
},
|
||||
"ISIDTOKENMAPPING": "Hozzárendelés az ID token alapján",
|
||||
"ISIDTOKENMAPPING_DESC": "Ha ezt választod, a szolgáltatói információkat az ID token alapján rendeljük hozzá, nem a userinfo végpontból."
|
||||
"ISIDTOKENMAPPING_DESC": "Ha ezt választod, a szolgáltatói információkat az ID token alapján rendeljük hozzá, nem a userinfo végpontból.",
|
||||
"USEPKCE": "PKCE használata",
|
||||
"USEPKCE_DESC": "Meghatározza, hogy a code_challenge és a code_challenge_method paraméterek szerepeljenek-e a hitelesítési kérelemben"
|
||||
},
|
||||
"MFA": {
|
||||
"LIST": {
|
||||
|
@ -2006,7 +2006,9 @@
|
||||
"REMOVED": "Berhasil dihapus."
|
||||
},
|
||||
"ISIDTOKENMAPPING": "Peta dari token ID",
|
||||
"ISIDTOKENMAPPING_DESC": "Jika dipilih, informasi penyedia akan dipetakan dari token ID, bukan dari titik akhir info pengguna."
|
||||
"ISIDTOKENMAPPING_DESC": "Jika dipilih, informasi penyedia akan dipetakan dari token ID, bukan dari titik akhir info pengguna.",
|
||||
"USEPKCE": "Gunakan PKCE",
|
||||
"USEPKCE_DESC": "Menentukan apakah parameter code_challenge dan code_challenge_method disertakan dalam permintaan autentikasi"
|
||||
},
|
||||
"MFA": {
|
||||
"LIST": {
|
||||
|
@ -2205,7 +2205,9 @@
|
||||
"REMOVED": "Rimosso con successo."
|
||||
},
|
||||
"ISIDTOKENMAPPING": "Mappatura dal token ID",
|
||||
"ISIDTOKENMAPPING_DESC": "Se selezionato, le informazioni del provider vengono mappate dal token ID, non dal punto finale userinfo."
|
||||
"ISIDTOKENMAPPING_DESC": "Se selezionato, le informazioni del provider vengono mappate dal token ID, non dal punto finale userinfo.",
|
||||
"USEPKCE": "Usa PKCE",
|
||||
"USEPKCE_DESC": "Determina se i parametri code_challenge e code_challenge_method sono inclusi nella richiesta di autenticazione"
|
||||
},
|
||||
"MFA": {
|
||||
"LIST": {
|
||||
|
@ -1547,6 +1547,10 @@
|
||||
"MAXSIZEEXCEEDED": "最大サイズの524KBを超えました。",
|
||||
"NOSVGSUPPORTED": "SVGはサポートされていません!",
|
||||
"FONTINLOGINONLY": "フォントは現在、ログインインターフェイスにのみ表示されます。",
|
||||
"BACKGROUNDCOLOR": "背景色",
|
||||
"PRIMARYCOLOR": "プライマリカラー",
|
||||
"WARNCOLOR": "警告色",
|
||||
"FONTCOLOR": "フォントカラー",
|
||||
"VIEWS": {
|
||||
"PREVIEW": "プレビュー",
|
||||
"CURRENT": "現在の構成"
|
||||
@ -1682,7 +1686,8 @@
|
||||
"PR": "パスワードのリセット",
|
||||
"DC": "ドメインクレーム",
|
||||
"PL": "パスワードレス",
|
||||
"PC": "パスワードの変更"
|
||||
"PC": "パスワードの変更",
|
||||
"IU": "ユーザーの招待"
|
||||
},
|
||||
"CHIPS": {
|
||||
"firstname": "名",
|
||||
@ -2064,6 +2069,14 @@
|
||||
"LDAP": {
|
||||
"TITLE": "LDAPプロバイダー",
|
||||
"DESCRIPTION": "LDAPプロバイダーのクレデンシャルを入力してください。"
|
||||
},
|
||||
"APPLE": {
|
||||
"TITLE": "Appleプロバイダー",
|
||||
"DESCRIPTION": "Appleプロバイダーのクレデンシャルを入力してください。"
|
||||
},
|
||||
"SAML": {
|
||||
"TITLE": "SAMLプロバイダー",
|
||||
"DESCRIPTION": "SAMLプロバイダーのクレデンシャルを入力してください。"
|
||||
}
|
||||
},
|
||||
"DETAIL": {
|
||||
@ -2183,6 +2196,23 @@
|
||||
"JWTENDPOINT": "JWTエンドポイント",
|
||||
"JWTKEYSENDPOINT": "JWTキーエンドポイント"
|
||||
},
|
||||
"APPLE": {
|
||||
"TEAMID": "チームID",
|
||||
"KEYID": "キーID",
|
||||
"PRIVATEKEY": "秘密鍵",
|
||||
"UPDATEPRIVATEKEY": "秘密鍵の更新",
|
||||
"UPLOADPRIVATEKEY": "秘密鍵のアップロード",
|
||||
"KEYMAXSIZEEXCEEDED": "最大サイズの5kBを超えています"
|
||||
},
|
||||
"SAML": {
|
||||
"METADATAXML": "Metadata XML",
|
||||
"METADATAURL": "Metadata URL",
|
||||
"BINDING": "Binding",
|
||||
"SIGNEDREQUEST": "署名付きリクエスト",
|
||||
"NAMEIDFORMAT": "NameIDフォーマット",
|
||||
"TRANSIENTMAPPINGATTRIBUTENAME": "カスタム属性名",
|
||||
"TRANSIENTMAPPINGATTRIBUTENAME_DESC": "NameIDフォーマットが`transient`の場合にユーザーをマッピングするためのカスタム属性名 (例: `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress`)"
|
||||
},
|
||||
"TOAST": {
|
||||
"SAVED": "正常に保存されました。",
|
||||
"REACTIVATED": "IDPがアクティブになりました。",
|
||||
@ -2195,7 +2225,9 @@
|
||||
"REMOVED": "正常に削除されました。"
|
||||
},
|
||||
"ISIDTOKENMAPPING": "IDトークンからのマッピング",
|
||||
"ISIDTOKENMAPPING_DESC": "選択された場合、プロバイダ情報はIDトークンからマッピングされ、userinfoエンドポイントからではありません。"
|
||||
"ISIDTOKENMAPPING_DESC": "選択された場合、プロバイダ情報はIDトークンからマッピングされ、userinfoエンドポイントからではありません。",
|
||||
"USEPKCE": "PKCEを使用する",
|
||||
"USEPKCE_DESC": "code_challenge パラメータと code_challenge_method パラメータが認証リクエストに含まれるかどうかを決定します。"
|
||||
},
|
||||
"MFA": {
|
||||
"LIST": {
|
||||
@ -2267,7 +2299,11 @@
|
||||
"DELETE_TITLE": "SMTP設定を削除する",
|
||||
"DELETE_DESCRIPTION": "構成を削除しようとしています。送信者名を入力してこのアクションを確認します",
|
||||
"DELETED": "SMTP設定が削除されました",
|
||||
"SENDER": "この SMTP 構成を削除するには、「{{ value }}」と入力します。"
|
||||
"SENDER": "この SMTP 構成を削除するには、「{{ value }}」と入力します。",
|
||||
"TEST_TITLE": "SMTP設定をテストする",
|
||||
"TEST_DESCRIPTION": "テスト用のメールアドレスを指定してください",
|
||||
"TEST_EMAIL": "メールアドレス",
|
||||
"TEST_RESULT": "テスト結果"
|
||||
}
|
||||
},
|
||||
"CREATE": {
|
||||
|
@ -2225,7 +2225,9 @@
|
||||
"REMOVED": "성공적으로 제거되었습니다."
|
||||
},
|
||||
"ISIDTOKENMAPPING": "ID 토큰에서 매핑",
|
||||
"ISIDTOKENMAPPING_DESC": "선택 시, 사용자 정보 엔드포인트가 아닌 ID 토큰에서 제공자 정보를 매핑합니다."
|
||||
"ISIDTOKENMAPPING_DESC": "선택 시, 사용자 정보 엔드포인트가 아닌 ID 토큰에서 제공자 정보를 매핑합니다.",
|
||||
"USEPKCE": "PKCE 사용",
|
||||
"USEPKCE_DESC": "code_challenge 및 code_challenge_method 매개변수가 인증 요청에 포함되는지 여부를 결정합니다"
|
||||
},
|
||||
"MFA": {
|
||||
"LIST": {
|
||||
|
@ -2201,7 +2201,9 @@
|
||||
"REMOVED": "Успешно отстрането."
|
||||
},
|
||||
"ISIDTOKENMAPPING": "Совпаѓање од ID токен",
|
||||
"ISIDTOKENMAPPING_DESC": "Ако е избрано, информациите од провајдерот се мапираат од ID токенот, а не од userinfo крајната точка."
|
||||
"ISIDTOKENMAPPING_DESC": "Ако е избрано, информациите од провајдерот се мапираат од ID токенот, а не од userinfo крајната точка.",
|
||||
"USEPKCE": "Користете PKCE",
|
||||
"USEPKCE_DESC": "Определува дали параметрите code_challenge и code_challenge_method се вклучени во барањето за авторизација"
|
||||
},
|
||||
"MFA": {
|
||||
"LIST": {
|
||||
|
@ -2220,7 +2220,9 @@
|
||||
"REMOVED": "Succesvol verwijderd."
|
||||
},
|
||||
"ISIDTOKENMAPPING": "Kaart van de ID token",
|
||||
"ISIDTOKENMAPPING_DESC": "Als geselecteerd, wordt provider informatie in kaart gebracht van de ID token, niet van de userinfo eindpunt."
|
||||
"ISIDTOKENMAPPING_DESC": "Als geselecteerd, wordt provider informatie in kaart gebracht van de ID token, niet van de userinfo eindpunt.",
|
||||
"USEPKCE": "Gebruik PKCE",
|
||||
"USEPKCE_DESC": "Bepaalt of de parameters code_challenge en code_challenge_method zijn opgenomen in het verificatieverzoek"
|
||||
},
|
||||
"MFA": {
|
||||
"LIST": {
|
||||
|
@ -2204,7 +2204,9 @@
|
||||
"REMOVED": "Usunięto pomyślnie."
|
||||
},
|
||||
"ISIDTOKENMAPPING": "Mapowanie z tokena ID",
|
||||
"ISIDTOKENMAPPING_DESC": "Jeśli wybrane, informacje dostawcy są mapowane z tokena ID, a nie z punktu końcowego userinfo."
|
||||
"ISIDTOKENMAPPING_DESC": "Jeśli wybrane, informacje dostawcy są mapowane z tokena ID, a nie z punktu końcowego userinfo.",
|
||||
"USEPKCE": "Skorzystaj z PKCE",
|
||||
"USEPKCE_DESC": "Określa, czy parametry code_challenge i code_challenge_method są uwzględnione w żądaniu uwierzytelnienia"
|
||||
},
|
||||
"MFA": {
|
||||
"LIST": {
|
||||
|
@ -2200,7 +2200,9 @@
|
||||
"REMOVED": "Removido com sucesso."
|
||||
},
|
||||
"ISIDTOKENMAPPING": "Mapeamento do token ID",
|
||||
"ISIDTOKENMAPPING_DESC": "Se selecionado, as informações do provedor são mapeadas a partir do token ID, e não do ponto final userinfo."
|
||||
"ISIDTOKENMAPPING_DESC": "Se selecionado, as informações do provedor são mapeadas a partir do token ID, e não do ponto final userinfo.",
|
||||
"USEPKCE": "Usar PKCE",
|
||||
"USEPKCE_DESC": "Determina se os parâmetros code_challenge e code_challenge_method estão incluídos na solicitação de autenticação"
|
||||
},
|
||||
"MFA": {
|
||||
"LIST": {
|
||||
|
@ -2316,7 +2316,9 @@
|
||||
"REMOVED": "Удалено успешно."
|
||||
},
|
||||
"ISIDTOKENMAPPING": "Карта из ID-токена",
|
||||
"ISIDTOKENMAPPING_DESC": "Если этот флажок установлен, информация о поставщике сопоставляется с маркером идентификатора, а не с конечной точкой информации о пользователе."
|
||||
"ISIDTOKENMAPPING_DESC": "Если этот флажок установлен, информация о поставщике сопоставляется с маркером идентификатора, а не с конечной точкой информации о пользователе.",
|
||||
"USEPKCE": "Используйте ПКСЕ",
|
||||
"USEPKCE_DESC": "Определяет, включены ли параметры code_challenge и code_challenge_method в запрос аутентификации."
|
||||
},
|
||||
"MFA": {
|
||||
"LIST": {
|
||||
|
@ -2229,7 +2229,9 @@
|
||||
"REMOVED": "Borttagen framgångsrikt."
|
||||
},
|
||||
"ISIDTOKENMAPPING": "Mappa från ID-token",
|
||||
"ISIDTOKENMAPPING_DESC": "Om valt, mappas leverantörsinformation från ID-token, inte från användarinfo-slutpunkten."
|
||||
"ISIDTOKENMAPPING_DESC": "Om valt, mappas leverantörsinformation från ID-token, inte från användarinfo-slutpunkten.",
|
||||
"USEPKCE": "Använd PKCE",
|
||||
"USEPKCE_DESC": "Avgör om parametrarna code_challenge och code_challenge_method ingår i autentiseringsbegäran"
|
||||
},
|
||||
"MFA": {
|
||||
"LIST": {
|
||||
|
@ -2204,7 +2204,9 @@
|
||||
"REMOVED": "成功删除。"
|
||||
},
|
||||
"ISIDTOKENMAPPING": "从ID令牌映射",
|
||||
"ISIDTOKENMAPPING_DESC": "如果选中,提供商信息将从ID令牌映射,而不是从userinfo端点。"
|
||||
"ISIDTOKENMAPPING_DESC": "如果选中,提供商信息将从ID令牌映射,而不是从userinfo端点。",
|
||||
"USEPKCE": "使用PKCE",
|
||||
"USEPKCE_DESC": "确定 auth 请求中是否包含 code_challenge 和 code_challenge_method 参数"
|
||||
},
|
||||
"MFA": {
|
||||
"LIST": {
|
||||
|
@ -50,6 +50,9 @@ A useful default will be filled if you don't change anything.
|
||||
This information will be taken to create/update the user within ZITADEL.
|
||||
ZITADEL ensures that at least the `openid`-scope is always sent.
|
||||
|
||||
**Use PKCE**: If enabled, the provider will use Proof Key for Code Exchange (PKCE) to secure the authorization code flow
|
||||
in addition to the client secret.
|
||||
|
||||
<GeneralConfigDescription provider_account="Keycloak account" />
|
||||
|
||||

|
||||
|
@ -49,6 +49,9 @@ A useful default will be filled if you don't change anything.
|
||||
This information will be taken to create/update the user within ZITADEL.
|
||||
ZITADEL ensures that at least the `openid`-scope is always sent.
|
||||
|
||||
**Use PKCE**: If enabled, the provider will use Proof Key for Code Exchange (PKCE) to secure the authorization code flow
|
||||
in addition to the client secret.
|
||||
|
||||
<GeneralConfigDescription provider_account="OKTA account" />
|
||||
|
||||
### Activate IdP
|
||||
|
165
docs/docs/guides/integrate/login-ui/device-auth.mdx
Normal file
165
docs/docs/guides/integrate/login-ui/device-auth.mdx
Normal file
@ -0,0 +1,165 @@
|
||||
---
|
||||
title: Support for the Device Authorization Grant in a Custom Login UI
|
||||
sidebar_label: Device Authorization
|
||||
---
|
||||
|
||||
In case one of your applications requires the [OAuth2 Device Authorization Grant](/docs/guides/integrate/login/oidc/device-authorization) this guide will show you how to implement
|
||||
this in your application as well as the custom login UI.
|
||||
|
||||
The following flow shows you the different components you need to enable OAuth2 Device Authorization Grant for your login.
|
||||

|
||||
|
||||
1. Your application makes a device authorization request to your login UI
|
||||
2. The login UI proxies the request to ZITADEL.
|
||||
3. ZITADEL parses the request and does what it needs to interpret certain parameters (e.g., organization scope, etc.)
|
||||
4. ZITADEL returns the device authorization response
|
||||
5. Your application presents the `user_code` and `verification_uri` or maybe even renders a QR code with the `verification_uri_complete` for the user to scan
|
||||
6. Your application starts a polling mechanism to check if the user has approved the device authorization request on the token endpoint
|
||||
7. When the user opens the browser at the verification_uri, he can enter the user_code, or it's automatically filled in, if they scan the QR code
|
||||
8. Request the device authorization request from the ZITADEL API using the user_code
|
||||
9. Your login UI allows to approve or deny the device request
|
||||
10. In case they approved, authenticate the user in your login UI by creating and updating a session with all the checks you need.
|
||||
11. Inform ZITADEL about the decision:
|
||||
1. Authorize the device authorization request by sending the session and the previously retrieved id of the device authorization request to the ZITADEL API
|
||||
2. In case they denied, deny the device authorization from the ZITADEL API using the previously retrieved id of the device authorization request
|
||||
12. Notify the user that they can close the window now and return to the application.
|
||||
13. Your applications request to the token endpoint now receives the tokens or an error if the user denied the request.
|
||||
|
||||
## Example
|
||||
|
||||
Let's assume you host your login UI on the following URL:
|
||||
```
|
||||
https://login.example.com
|
||||
```
|
||||
|
||||
## Device Authorization Request
|
||||
|
||||
A user opens your application and is unauthenticated, the application will create the following request:
|
||||
```HTTP
|
||||
POST /oauth/v2/device_authorization HTTP/1.1
|
||||
Host: login.example.com
|
||||
Content-type: application/x-www-form-urlencoded
|
||||
|
||||
client_id=170086824411201793&
|
||||
scope=openid%20email%20profile
|
||||
```
|
||||
|
||||
The request includes all the relevant information for the OAuth2 Device Authorization Grant and in this example we also have some scopes for the user.
|
||||
|
||||
You now have to proxy the auth request from your own UI to the device authorization Endpoint of ZITADEL.
|
||||
For more information, see [OIDC Proxy](./typescript-repo#oidc-proxy) for the necessary headers.
|
||||
|
||||
:::note
|
||||
The version and the optional custom URI for the available login UI is configurable under the application settings.
|
||||
:::
|
||||
|
||||
The endpoint will return the device authorization response:
|
||||
```json
|
||||
{
|
||||
"device_code": "0jbAZbU3ClK-Mkt0li4U1A",
|
||||
"user_code": "FWRK-JGWK",
|
||||
"verification_uri": "https://login.example.com/device",
|
||||
"verification_uri_complete": "https://login.example.com/device?user_code=FWRK-JGWK",
|
||||
"expires_in": 300,
|
||||
"interval": 5
|
||||
}
|
||||
```
|
||||
|
||||
The device presents the `user_code` and `verification_uri` or maybe even render a QR code with the `verification_uri_complete` for the user to scan.
|
||||
|
||||
Your login will have to provide a page on the `verification_uri` where the user can enter the `user_code`, or it's automatically filled in, if they scan the QR code.
|
||||
|
||||
### Get the Device Authorization Request by User Code
|
||||
|
||||
With the user_code entered by the user you will now be able to get the information of the device authorization request.
|
||||
[Get Device Authorization Request Documentation](/docs/apis/resources/oidc_service_v2/oidc-service-get-device-authorization-request)
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url https://$ZITADEL_DOMAIN/v2/oidc/device_authorization/FWRK-JGWK \
|
||||
--header 'Authorization: Bearer '"$TOKEN"''
|
||||
```
|
||||
|
||||
Response Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"deviceAuthorizationRequest": {
|
||||
"id": "XzNejv6NxqVU8Qur5uxEh7f_Wi1p0qUu4PJTJ6JUIx0xtJ2uqmU",
|
||||
"clientId": "170086824411201793",
|
||||
"scope": [
|
||||
"openid",
|
||||
"profile"
|
||||
],
|
||||
"appName": "TV App",
|
||||
"projectName": "My Project"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Present the user with the information of the device authorization request and allow them to approve or deny the request.
|
||||
|
||||
### Perform Login
|
||||
|
||||
After you have initialized the OIDC flow you can implement the login.
|
||||
Implement all the steps you like the user the go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service/session-service-set-session) the user-session.
|
||||
|
||||
Read the following resources for more information about the different checks:
|
||||
- [Username and Password](./username-password)
|
||||
- [External Identity Provider](./external-login)
|
||||
- [Passkeys](./passkey)
|
||||
- [Multi-Factor](./mfa)
|
||||
|
||||
### Authorize the Device Authorization Request
|
||||
|
||||
To finalize the auth request and connect an existing user session with it, you have to update the auth request with the session token.
|
||||
On the create and update user session request you will always get a session token in the response.
|
||||
|
||||
The latest session token has to be sent to the following request:
|
||||
|
||||
Read more about the [Authorize or Deny Device Authorization Request Documentation](/docs/apis/resources/oidc_service_v2/oidc-service-authorize-device-authorization)
|
||||
|
||||
Make sure that the authorization header is from an account which is permitted to finalize the Auth Request through the `IAM_LOGIN_CLIENT` role.
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url $ZITADEL_DOMAIN/v2/oidc/device_authorization/XzNejv6NxqVU8Qur5uxEh7f_Wi1p0qUu4PJTJ6JUIx0xtJ2uqmU \
|
||||
--header 'Accept: application/json' \
|
||||
--header 'Authorization: Bearer '"$TOKEN"''\
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"session": {
|
||||
"sessionId": "225307381909694508",
|
||||
"sessionToken": "7N5kQCvC4jIf2OuBjwfyWSX2FUKbQqg4iG3uWT-TBngMhlS9miGUwpyUaN0HJ8OcbSzk4QHZy_Bvvv"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
If you don't get any error back, the request succeeded, and you can notify the user that they can close the window now and return to the application.
|
||||
|
||||
### Deny the Device Authorization Request
|
||||
|
||||
If the user denies the device authorization request, you can deny the request by sending the following request:
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url $ZITADEL_DOMAIN/v2/oidc/device_authorization/ \
|
||||
--header 'Accept: application/json' \
|
||||
--header 'Authorization: Bearer '"$TOKEN"''\
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"deny": {}
|
||||
}'
|
||||
```
|
||||
|
||||
If you don't get any error back, the request succeeded, and you can notify the user that they can close the window now and return to the application.
|
||||
|
||||
### Device Authorization Endpoints
|
||||
|
||||
All OAuth2 Device Authorization Grant endpoints are provided by ZITADEL. In your login UI you just have to proxy them through and send them directly to the backend.
|
||||
|
||||
These endpoints are:
|
||||
- Well-known
|
||||
- Device Authorization Endpoint
|
||||
- Token
|
||||
|
||||
Additionally, we recommend you to proxy all the other [OIDC relevant endpoints](./oidc-standard#endpoints).
|
@ -56,7 +56,7 @@ With the ID from the redirect before you will now be able to get the information
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url https://$ZITADEL_DOMAIN/v2/oidc/auth_requests/V2_224908753244265546 \
|
||||
--header 'Authorization: Bearer '"$TOKEN"''\
|
||||
--header 'Authorization: Bearer '"$TOKEN"''
|
||||
```
|
||||
|
||||
Response Example:
|
||||
@ -90,7 +90,7 @@ Read the following resources for more information about the different checks:
|
||||
|
||||
### Finalize Auth Request
|
||||
|
||||
To finalize the auth request and connect an existing user session with it you have to update the auth request with the session token.
|
||||
To finalize the auth request and connect an existing user session with it, you have to update the auth request with the session token.
|
||||
On the create and update user session request you will always get a session token in the response.
|
||||
|
||||
The latest session token has to be sent to the following request:
|
||||
@ -128,7 +128,7 @@ Example Response:
|
||||
|
||||
### OIDC Endpoints
|
||||
|
||||
All OIDC relevant endpoints are provided by ZITADEL. In you login UI you just have to proxy them through and send them directly to the backend.
|
||||
All OIDC relevant endpoints are provided by ZITADEL. In your login UI you just have to proxy them through and send them directly to the backend.
|
||||
|
||||
These are endpoints like:
|
||||
- Userinfo
|
||||
|
@ -56,7 +56,7 @@ With the ID from the redirect before you will now be able to get the information
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url https://$ZITADEL_DOMAIN/v2/saml/saml_requests/V2_224908753244265546 \
|
||||
--header 'Authorization: Bearer '"$TOKEN"''\
|
||||
--header 'Authorization: Bearer '"$TOKEN"''
|
||||
```
|
||||
|
||||
Response Example:
|
||||
@ -87,7 +87,7 @@ Read the following resources for more information about the different checks:
|
||||
|
||||
### Finalize SAML Request
|
||||
|
||||
To finalize the SAML request and connect an existing user session with it you have to update the SAML Request with the session token.
|
||||
To finalize the SAML request and connect an existing user session with it, you have to update the SAML Request with the session token.
|
||||
On the create and update user session request you will always get a session token in the response.
|
||||
|
||||
The latest session token has to be sent to the following request:
|
||||
|
@ -11,7 +11,6 @@ The typescript repository contains all TypeScript and JavaScript packages and ap
|
||||
- **[login](./typescript-repo#new-login-ui)**: The future login UI used by ZITADEL Cloud, powered by Next.js
|
||||
- `@zitadel/proto`: Typescript implementation of Protocol Buffers, suitable for web browsers and Node.js.
|
||||
- `@zitadel/client`: Core components for establishing a client connection
|
||||
- `@zitadel/node`: Core components for establishing a server connection
|
||||
- `@zitadel/tsconfig`: shared `tsconfig.json`s used throughout the monorepo
|
||||
- `eslint-config-zitadel`: ESLint preset
|
||||
|
||||
@ -137,11 +136,10 @@ You can review an example implementation of a middlware [here](https://github.co
|
||||
|
||||
#### Deploy to Vercel
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fzitadel%2Ftypescript&env=ZITADEL_API_URL,ZITADEL_SERVICE_USER_ID,ZITADEL_SERVICE_USER_TOKEN&root-directory=apps/login&envDescription=Setup%20a%20service%20account%20with%20IAM_OWNER%20membership%20on%20your%20instance%20and%20provide%20its%20id%20and%20personal%20access%20token.&project-name=zitadel-login&repository-name=zitadel-login)
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fzitadel%2Ftypescript&env=ZITADEL_API_URL,ZITADEL_SERVICE_USER_TOKEN&root-directory=apps/login&envDescription=Setup%20a%20service%20account%20with%20IAM_LOGIN_CLIENT%20membership%20on%20your%20instance%20and%20provide%20its%20personal%20access%20token.&project-name=zitadel-login&repository-name=zitadel-login)
|
||||
|
||||
To deploy your own version on Vercel, navigate to your instance and create a service user.
|
||||
Copy its id from the overview and set it as `ZITADEL_SERVICE_USER_ID`.
|
||||
Then create a personal access token (PAT), copy and set it as `ZITADEL_SERVICE_USER_TOKEN`, then navigate to Default settings and make sure it gets `IAM_OWNER` permissions.
|
||||
Create a personal access token (PAT) for the user and copy and set it as `ZITADEL_SERVICE_USER_TOKEN`, then navigate to Default settings and make sure it gets `IAM_LOGIN_CLIENT` permissions.
|
||||
Finally set your instance url as `ZITADEL_API_URL`. Make sure to set it without trailing slash.
|
||||
Also ensure your login domain is registered on your instance by adding it as a [trusted domain](/docs/apis/resources/admin/admin-service-add-instance-trusted-domain).
|
||||
|
||||
|
@ -179,12 +179,11 @@ Your contributions will play a crucial role in shaping the future of our login s
|
||||
|
||||
The simplest way to deploy the new login for yourself is by using the [“Deploy” button in our repository](https://github.com/zitadel/typescript?tab=readme-ov-file#deploy-to-vercel) to deploy the login directly to your Vercel.
|
||||
|
||||
1. [Create a service user](https://zitadel.com/docs/guides/integrate/service-users/personal-access-token#create-a-service-user-with-a-pat) (ZITADEL_SERVICE_USER_ID) with a PAT in your instance
|
||||
1. [Create a service user](https://zitadel.com/docs/guides/integrate/service-users/personal-access-token#create-a-service-user-with-a-pat) with a PAT in your instance
|
||||
2. Give the user IAM_LOGIN_CLIENT Permissions in the default settings (YOUR_DOMAIN/ui/console/instance?id=organizations)
|
||||
Note: [Zitadel Manager Guide](https://zitadel.com/docs/guides/manage/console/managers)
|
||||
3. Deploy login to Vercel: You can do so, be directly clicking the [“Deploy” button](https://github.com/zitadel/typescript?tab=readme-ov-file#deploy-to-vercel) at the bottom of the readme in our [repository](https://github.com/zitadel/typescript)
|
||||
4. If you have used the deploy button in the steps before, you will automatically be asked for this step. Enter the environment variables in Vercel
|
||||
- ZITADEL_SERVICE_USER_ID
|
||||
- PAT
|
||||
- ZITADEL_API_URL (Example: https://my-domain.zitadel.cloud, no trailing slash)
|
||||
5. Add the domain where your login UI is hosted to the [trusted domains](https://zitadel.com/docs/apis/resources/admin/admin-service-add-instance-trusted-domain) in Zitadel. (Example: my-new-zitadel-login.vercel.app)
|
||||
|
@ -328,6 +328,7 @@ module.exports = {
|
||||
"guides/integrate/login-ui/logout",
|
||||
"guides/integrate/login-ui/oidc-standard",
|
||||
"guides/integrate/login-ui/saml-standard",
|
||||
"guides/integrate/login-ui/device-auth",
|
||||
"guides/integrate/login-ui/typescript-repo",
|
||||
],
|
||||
},
|
||||
|
BIN
docs/static/img/guides/login-ui/device-auth-flow.png
vendored
Normal file
BIN
docs/static/img/guides/login-ui/device-auth-flow.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 80 KiB |
@ -28,8 +28,7 @@ func (s *Server) ListIAMMembers(ctx context.Context, req *admin_pb.ListIAMMember
|
||||
}
|
||||
return &admin_pb.ListIAMMembersResponse{
|
||||
Details: object.ToListDetails(res.Count, res.Sequence, res.LastRun),
|
||||
//TODO: resource owner of user of the member instead of the membership resource owner
|
||||
Result: member.MembersToPb("", res.Members),
|
||||
Result: member.MembersToPb("", res.Members),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
|
||||
member_pb "github.com/zitadel/zitadel/pkg/grpc/member"
|
||||
)
|
||||
|
||||
func AddIAMMemberToDomain(req *admin_pb.AddIAMMemberRequest) *domain.Member {
|
||||
@ -31,12 +32,29 @@ func ListIAMMembersRequestToQuery(req *admin_pb.ListIAMMembersRequest) (*query.I
|
||||
return &query.IAMMembersQuery{
|
||||
MembersQuery: query.MembersQuery{
|
||||
SearchRequest: query.SearchRequest{
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Asc: asc,
|
||||
// SortingColumn: model.IAMMemberSearchKey, //TOOD: not implemented in proto
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Asc: asc,
|
||||
SortingColumn: fieldNameToMemberColumn(req.SortingColumn),
|
||||
},
|
||||
Queries: queries,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func fieldNameToMemberColumn(fieldName member_pb.MemberFieldColumnName) query.Column {
|
||||
switch fieldName {
|
||||
case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_UNSPECIFIED:
|
||||
return query.InstanceMemberInstanceID
|
||||
case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_USER_ID:
|
||||
return query.InstanceMemberUserID
|
||||
case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_CREATION_DATE:
|
||||
return query.InstanceMemberCreationDate
|
||||
case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_CHANGE_DATE:
|
||||
return query.InstanceMemberChangeDate
|
||||
case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_USER_RESOURCE_OWNER:
|
||||
return query.InstanceMemberResourceOwner
|
||||
default:
|
||||
return query.Column{}
|
||||
}
|
||||
}
|
||||
|
@ -215,6 +215,7 @@ func addGenericOAuthProviderToCommand(req *admin_pb.AddGenericOAuthProviderReque
|
||||
UserEndpoint: req.UserEndpoint,
|
||||
Scopes: req.Scopes,
|
||||
IDAttribute: req.IdAttribute,
|
||||
UsePKCE: req.UsePkce,
|
||||
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
|
||||
}
|
||||
}
|
||||
@ -229,6 +230,7 @@ func updateGenericOAuthProviderToCommand(req *admin_pb.UpdateGenericOAuthProvide
|
||||
UserEndpoint: req.UserEndpoint,
|
||||
Scopes: req.Scopes,
|
||||
IDAttribute: req.IdAttribute,
|
||||
UsePKCE: req.UsePkce,
|
||||
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
|
||||
}
|
||||
}
|
||||
@ -241,6 +243,7 @@ func addGenericOIDCProviderToCommand(req *admin_pb.AddGenericOIDCProviderRequest
|
||||
ClientSecret: req.ClientSecret,
|
||||
Scopes: req.Scopes,
|
||||
IsIDTokenMapping: req.IsIdTokenMapping,
|
||||
UsePKCE: req.UsePkce,
|
||||
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
|
||||
}
|
||||
}
|
||||
@ -253,6 +256,7 @@ func updateGenericOIDCProviderToCommand(req *admin_pb.UpdateGenericOIDCProviderR
|
||||
ClientSecret: req.ClientSecret,
|
||||
Scopes: req.Scopes,
|
||||
IsIDTokenMapping: req.IsIdTokenMapping,
|
||||
UsePKCE: req.UsePkce,
|
||||
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
|
||||
}
|
||||
}
|
||||
|
@ -112,9 +112,6 @@ func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain str
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userIDs := make([]string, len(users.Users))
|
||||
for i, user := range users.Users {
|
||||
userIDs[i] = user.ID
|
||||
|
@ -504,6 +504,7 @@ func oauthConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.OAut
|
||||
UserEndpoint: template.UserEndpoint,
|
||||
Scopes: template.Scopes,
|
||||
IdAttribute: template.IDAttribute,
|
||||
UsePkce: template.UsePKCE,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -515,6 +516,7 @@ func oidcConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.OIDCI
|
||||
Issuer: template.Issuer,
|
||||
Scopes: template.Scopes,
|
||||
IsIdTokenMapping: template.IsIDTokenMapping,
|
||||
UsePkce: template.UsePKCE,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -76,6 +76,7 @@ func TestServer_GetIDPByID(t *testing.T) {
|
||||
name,
|
||||
&object.Details{
|
||||
Sequence: resp.Details.Sequence,
|
||||
CreationDate: resp.Details.CreationDate,
|
||||
ChangeDate: resp.Details.ChangeDate,
|
||||
ResourceOwner: resp.Details.ResourceOwner,
|
||||
}}
|
||||
@ -124,6 +125,7 @@ func TestServer_GetIDPByID(t *testing.T) {
|
||||
name,
|
||||
&object.Details{
|
||||
Sequence: resp.Details.Sequence,
|
||||
CreationDate: resp.Details.CreationDate,
|
||||
ChangeDate: resp.Details.ChangeDate,
|
||||
ResourceOwner: resp.Details.ResourceOwner,
|
||||
}}
|
||||
@ -145,6 +147,7 @@ func TestServer_GetIDPByID(t *testing.T) {
|
||||
name,
|
||||
&object.Details{
|
||||
Sequence: resp.Details.Sequence,
|
||||
CreationDate: resp.Details.CreationDate,
|
||||
ChangeDate: resp.Details.ChangeDate,
|
||||
ResourceOwner: resp.Details.ResourceOwner,
|
||||
}}
|
||||
@ -193,6 +196,7 @@ func TestServer_GetIDPByID(t *testing.T) {
|
||||
name,
|
||||
&object.Details{
|
||||
Sequence: resp.Details.Sequence,
|
||||
CreationDate: resp.Details.CreationDate,
|
||||
ChangeDate: resp.Details.ChangeDate,
|
||||
ResourceOwner: resp.Details.ResourceOwner,
|
||||
}}
|
||||
|
@ -31,6 +31,7 @@ func idpToPb(idp *query.IDPTemplate) *idp_pb.IDP {
|
||||
Sequence: idp.Sequence,
|
||||
EventDate: idp.ChangeDate,
|
||||
ResourceOwner: idp.ResourceOwner,
|
||||
CreationDate: idp.CreationDate,
|
||||
}),
|
||||
State: idpStateToPb(idp.State),
|
||||
Name: idp.Name,
|
||||
|
@ -209,6 +209,7 @@ func addGenericOAuthProviderToCommand(req *mgmt_pb.AddGenericOAuthProviderReques
|
||||
Scopes: req.Scopes,
|
||||
IDAttribute: req.IdAttribute,
|
||||
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
|
||||
UsePKCE: req.UsePkce,
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,6 +224,7 @@ func updateGenericOAuthProviderToCommand(req *mgmt_pb.UpdateGenericOAuthProvider
|
||||
Scopes: req.Scopes,
|
||||
IDAttribute: req.IdAttribute,
|
||||
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
|
||||
UsePKCE: req.UsePkce,
|
||||
}
|
||||
}
|
||||
|
||||
@ -234,6 +236,7 @@ func addGenericOIDCProviderToCommand(req *mgmt_pb.AddGenericOIDCProviderRequest)
|
||||
ClientSecret: req.ClientSecret,
|
||||
Scopes: req.Scopes,
|
||||
IsIDTokenMapping: req.IsIdTokenMapping,
|
||||
UsePKCE: req.UsePkce,
|
||||
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
|
||||
}
|
||||
}
|
||||
@ -246,6 +249,7 @@ func updateGenericOIDCProviderToCommand(req *mgmt_pb.UpdateGenericOIDCProviderRe
|
||||
ClientSecret: req.ClientSecret,
|
||||
Scopes: req.Scopes,
|
||||
IsIDTokenMapping: req.IsIdTokenMapping,
|
||||
UsePKCE: req.UsePkce,
|
||||
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,9 @@ func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details {
|
||||
if !objectDetail.EventDate.IsZero() {
|
||||
details.ChangeDate = timestamppb.New(objectDetail.EventDate)
|
||||
}
|
||||
if !objectDetail.CreationDate.IsZero() {
|
||||
details.CreationDate = timestamppb.New(objectDetail.CreationDate)
|
||||
}
|
||||
return details
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,9 @@ func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details {
|
||||
if !objectDetail.EventDate.IsZero() {
|
||||
details.ChangeDate = timestamppb.New(objectDetail.EventDate)
|
||||
}
|
||||
if !objectDetail.CreationDate.IsZero() {
|
||||
details.CreationDate = timestamppb.New(objectDetail.CreationDate)
|
||||
}
|
||||
return details
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
@ -636,6 +637,230 @@ func TestServer_CreateCallback_Permission(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_GetDeviceAuthorizationRequest(t *testing.T) {
|
||||
project, err := Instance.CreateProject(CTX)
|
||||
require.NoError(t, err)
|
||||
client, err := Instance.CreateOIDCClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dep func() (*oidc.DeviceAuthorizationResponse, error)
|
||||
ctx context.Context
|
||||
want *oidc.DeviceAuthorizationResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Not found",
|
||||
dep: func() (*oidc.DeviceAuthorizationResponse, error) {
|
||||
return &oidc.DeviceAuthorizationResponse{
|
||||
UserCode: "notFound",
|
||||
}, nil
|
||||
},
|
||||
ctx: CTX,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
dep: func() (*oidc.DeviceAuthorizationResponse, error) {
|
||||
return Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid")
|
||||
},
|
||||
ctx: CTX,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
deviceAuth, err := tt.dep()
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := Client.GetDeviceAuthorizationRequest(tt.ctx, &oidc_pb.GetDeviceAuthorizationRequestRequest{
|
||||
UserCode: deviceAuth.UserCode,
|
||||
})
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
authRequest := got.GetDeviceAuthorizationRequest()
|
||||
assert.NotNil(t, authRequest)
|
||||
assert.NotEmpty(t, authRequest.GetId())
|
||||
assert.Equal(t, client.GetClientId(), authRequest.GetClientId())
|
||||
assert.Contains(t, authRequest.GetScope(), "openid")
|
||||
assert.NotEmpty(t, authRequest.GetAppName())
|
||||
assert.NotEmpty(t, authRequest.GetProjectName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) {
|
||||
project, err := Instance.CreateProject(CTX)
|
||||
require.NoError(t, err)
|
||||
client, err := Instance.CreateOIDCClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE)
|
||||
require.NoError(t, err)
|
||||
sessionResp := createSession(t, CTX, Instance.Users[integration.UserTypeOrgOwner].ID)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
req *oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest
|
||||
AuthError string
|
||||
want *oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse
|
||||
wantURL *url.URL
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Not found",
|
||||
ctx: CTX,
|
||||
req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
|
||||
DeviceAuthorizationId: "123",
|
||||
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionResp.GetSessionId(),
|
||||
SessionToken: sessionResp.GetSessionToken(),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "session not found",
|
||||
ctx: CTX,
|
||||
req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
|
||||
DeviceAuthorizationId: func() string {
|
||||
req, err := Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid")
|
||||
require.NoError(t, err)
|
||||
var id string
|
||||
assert.EventuallyWithT(t, func(collectT *assert.CollectT) {
|
||||
resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{
|
||||
UserCode: req.UserCode,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
id = resp.GetDeviceAuthorizationRequest().GetId()
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
return id
|
||||
}(),
|
||||
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: "foo",
|
||||
SessionToken: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "session token invalid",
|
||||
ctx: CTX,
|
||||
req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
|
||||
DeviceAuthorizationId: func() string {
|
||||
req, err := Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid")
|
||||
require.NoError(t, err)
|
||||
var id string
|
||||
assert.EventuallyWithT(t, func(collectT *assert.CollectT) {
|
||||
resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{
|
||||
UserCode: req.UserCode,
|
||||
})
|
||||
assert.NoError(collectT, err)
|
||||
id = resp.GetDeviceAuthorizationRequest().GetId()
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
return id
|
||||
}(),
|
||||
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionResp.GetSessionId(),
|
||||
SessionToken: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "deny device authorization",
|
||||
ctx: CTX,
|
||||
req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
|
||||
DeviceAuthorizationId: func() string {
|
||||
req, err := Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid")
|
||||
require.NoError(t, err)
|
||||
var id string
|
||||
assert.EventuallyWithT(t, func(collectT *assert.CollectT) {
|
||||
resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{
|
||||
UserCode: req.UserCode,
|
||||
})
|
||||
assert.NoError(collectT, err)
|
||||
id = resp.GetDeviceAuthorizationRequest().GetId()
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
return id
|
||||
}(),
|
||||
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Deny{},
|
||||
},
|
||||
want: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "authorize, no permission, error",
|
||||
ctx: CTX,
|
||||
req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
|
||||
DeviceAuthorizationId: func() string {
|
||||
req, err := Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid")
|
||||
require.NoError(t, err)
|
||||
var id string
|
||||
assert.EventuallyWithT(t, func(collectT *assert.CollectT) {
|
||||
resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{
|
||||
UserCode: req.UserCode,
|
||||
})
|
||||
assert.NoError(collectT, err)
|
||||
id = resp.GetDeviceAuthorizationRequest().GetId()
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
return id
|
||||
}(),
|
||||
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionResp.GetSessionId(),
|
||||
SessionToken: sessionResp.GetSessionToken(),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "authorize, with permission",
|
||||
ctx: CTXLoginClient,
|
||||
req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
|
||||
DeviceAuthorizationId: func() string {
|
||||
req, err := Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid")
|
||||
require.NoError(t, err)
|
||||
var id string
|
||||
assert.EventuallyWithT(t, func(collectT *assert.CollectT) {
|
||||
resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{
|
||||
UserCode: req.UserCode,
|
||||
})
|
||||
assert.NoError(collectT, err)
|
||||
id = resp.GetDeviceAuthorizationRequest().GetId()
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
return id
|
||||
}(),
|
||||
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionResp.GetSessionId(),
|
||||
SessionToken: sessionResp.GetSessionToken(),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := Client.AuthorizeOrDenyDeviceAuthorization(tt.ctx, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createSession(t *testing.T, ctx context.Context, userID string) *session.CreateSessionResponse {
|
||||
sessionResp, err := Instance.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
|
@ -2,6 +2,7 @@ package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
@ -28,6 +29,54 @@ func (s *Server) GetAuthRequest(ctx context.Context, req *oidc_pb.GetAuthRequest
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) CreateCallback(ctx context.Context, req *oidc_pb.CreateCallbackRequest) (*oidc_pb.CreateCallbackResponse, error) {
|
||||
switch v := req.GetCallbackKind().(type) {
|
||||
case *oidc_pb.CreateCallbackRequest_Error:
|
||||
return s.failAuthRequest(ctx, req.GetAuthRequestId(), v.Error)
|
||||
case *oidc_pb.CreateCallbackRequest_Session:
|
||||
return s.linkSessionToAuthRequest(ctx, req.GetAuthRequestId(), v.Session)
|
||||
default:
|
||||
return nil, zerrors.ThrowUnimplementedf(nil, "OIDCv2-zee7A", "verification oneOf %T in method CreateCallback not implemented", v)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) GetDeviceAuthorizationRequest(ctx context.Context, req *oidc_pb.GetDeviceAuthorizationRequestRequest) (*oidc_pb.GetDeviceAuthorizationRequestResponse, error) {
|
||||
deviceRequest, err := s.query.DeviceAuthRequestByUserCode(ctx, req.GetUserCode())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encrypted, err := s.encryption.Encrypt([]byte(deviceRequest.DeviceCode))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &oidc_pb.GetDeviceAuthorizationRequestResponse{
|
||||
DeviceAuthorizationRequest: &oidc_pb.DeviceAuthorizationRequest{
|
||||
Id: base64.RawURLEncoding.EncodeToString(encrypted),
|
||||
ClientId: deviceRequest.ClientID,
|
||||
Scope: deviceRequest.Scopes,
|
||||
AppName: deviceRequest.AppName,
|
||||
ProjectName: deviceRequest.ProjectName,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) AuthorizeOrDenyDeviceAuthorization(ctx context.Context, req *oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest) (*oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse, error) {
|
||||
deviceCode, err := s.deviceCodeFromID(req.GetDeviceAuthorizationId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch req.GetDecision().(type) {
|
||||
case *oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session:
|
||||
_, err = s.command.ApproveDeviceAuthWithSession(ctx, deviceCode, req.GetSession().GetSessionId(), req.GetSession().GetSessionToken())
|
||||
case *oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Deny:
|
||||
_, err = s.command.CancelDeviceAuth(ctx, deviceCode, domain.DeviceAuthCanceledDenied)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse{}, nil
|
||||
}
|
||||
|
||||
func authRequestToPb(a *query.AuthRequest) *oidc_pb.AuthRequest {
|
||||
pba := &oidc_pb.AuthRequest{
|
||||
Id: a.ID,
|
||||
@ -87,17 +136,6 @@ func (s *Server) checkPermission(ctx context.Context, clientID string, userID st
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) CreateCallback(ctx context.Context, req *oidc_pb.CreateCallbackRequest) (*oidc_pb.CreateCallbackResponse, error) {
|
||||
switch v := req.GetCallbackKind().(type) {
|
||||
case *oidc_pb.CreateCallbackRequest_Error:
|
||||
return s.failAuthRequest(ctx, req.GetAuthRequestId(), v.Error)
|
||||
case *oidc_pb.CreateCallbackRequest_Session:
|
||||
return s.linkSessionToAuthRequest(ctx, req.GetAuthRequestId(), v.Session)
|
||||
default:
|
||||
return nil, zerrors.ThrowUnimplementedf(nil, "OIDCv2-zee7A", "verification oneOf %T in method CreateCallback not implemented", v)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) failAuthRequest(ctx context.Context, authRequestID string, ae *oidc_pb.AuthorizationError) (*oidc_pb.CreateCallbackResponse, error) {
|
||||
details, aar, err := s.command.FailAuthRequest(ctx, authRequestID, errorReasonToDomain(ae.GetError()))
|
||||
if err != nil {
|
||||
@ -215,3 +253,11 @@ func errorReasonToOIDC(reason oidc_pb.ErrorReason) string {
|
||||
return "server_error"
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) deviceCodeFromID(deviceAuthID string) (string, error) {
|
||||
decoded, err := base64.RawURLEncoding.DecodeString(deviceAuthID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return s.encryption.DecryptString(decoded, s.encryption.EncryptionKeyID())
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||
"github.com/zitadel/zitadel/internal/api/oidc"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
|
||||
)
|
||||
@ -20,6 +21,7 @@ type Server struct {
|
||||
|
||||
op *oidc.Server
|
||||
externalSecure bool
|
||||
encryption crypto.EncryptionAlgorithm
|
||||
}
|
||||
|
||||
type Config struct{}
|
||||
@ -29,12 +31,14 @@ func CreateServer(
|
||||
query *query.Queries,
|
||||
op *oidc.Server,
|
||||
externalSecure bool,
|
||||
encryption crypto.EncryptionAlgorithm,
|
||||
) *Server {
|
||||
return &Server{
|
||||
command: command,
|
||||
query: query,
|
||||
op: op,
|
||||
externalSecure: externalSecure,
|
||||
encryption: encryption,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,16 @@ type orgAttr struct {
|
||||
Details *object.Details
|
||||
}
|
||||
|
||||
func createOrganization(ctx context.Context, name string) orgAttr {
|
||||
orgResp := Instance.CreateOrganization(ctx, name, gofakeit.Email())
|
||||
orgResp.Details.CreationDate = orgResp.Details.ChangeDate
|
||||
return orgAttr{
|
||||
ID: orgResp.GetOrganizationId(),
|
||||
Name: name,
|
||||
Details: orgResp.GetDetails(),
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_ListOrganizations(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@ -63,6 +73,7 @@ func TestServer_ListOrganizations(t *testing.T) {
|
||||
State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE,
|
||||
Details: &object.Details{
|
||||
Sequence: Instance.DefaultOrg.Details.Sequence,
|
||||
CreationDate: Instance.DefaultOrg.Details.CreationDate,
|
||||
ChangeDate: Instance.DefaultOrg.Details.ChangeDate,
|
||||
ResourceOwner: Instance.DefaultOrg.Details.ResourceOwner,
|
||||
},
|
||||
@ -85,12 +96,7 @@ func TestServer_ListOrganizations(t *testing.T) {
|
||||
prefix := fmt.Sprintf("ListOrgs-%s", gofakeit.AppName())
|
||||
for i := 0; i < count; i++ {
|
||||
name := prefix + strconv.Itoa(i)
|
||||
orgResp := Instance.CreateOrganization(ctx, name, gofakeit.Email())
|
||||
orgs[i] = orgAttr{
|
||||
ID: orgResp.GetOrganizationId(),
|
||||
Name: name,
|
||||
Details: orgResp.GetDetails(),
|
||||
}
|
||||
orgs[i] = createOrganization(ctx, name)
|
||||
}
|
||||
request.Queries = []*org.SearchQuery{
|
||||
OrganizationNamePrefixQuery(prefix),
|
||||
@ -140,6 +146,7 @@ func TestServer_ListOrganizations(t *testing.T) {
|
||||
Name: Instance.DefaultOrg.Name,
|
||||
Details: &object.Details{
|
||||
Sequence: Instance.DefaultOrg.Details.Sequence,
|
||||
CreationDate: Instance.DefaultOrg.Details.CreationDate,
|
||||
ChangeDate: Instance.DefaultOrg.Details.ChangeDate,
|
||||
ResourceOwner: Instance.DefaultOrg.Details.ResourceOwner,
|
||||
},
|
||||
@ -172,6 +179,7 @@ func TestServer_ListOrganizations(t *testing.T) {
|
||||
Name: Instance.DefaultOrg.Name,
|
||||
Details: &object.Details{
|
||||
Sequence: Instance.DefaultOrg.Details.Sequence,
|
||||
CreationDate: Instance.DefaultOrg.Details.CreationDate,
|
||||
ChangeDate: Instance.DefaultOrg.Details.ChangeDate,
|
||||
ResourceOwner: Instance.DefaultOrg.Details.ResourceOwner,
|
||||
},
|
||||
@ -204,6 +212,7 @@ func TestServer_ListOrganizations(t *testing.T) {
|
||||
Name: Instance.DefaultOrg.Name,
|
||||
Details: &object.Details{
|
||||
Sequence: Instance.DefaultOrg.Details.Sequence,
|
||||
CreationDate: Instance.DefaultOrg.Details.CreationDate,
|
||||
ChangeDate: Instance.DefaultOrg.Details.ChangeDate,
|
||||
ResourceOwner: Instance.DefaultOrg.Details.ResourceOwner,
|
||||
},
|
||||
@ -221,14 +230,9 @@ func TestServer_ListOrganizations(t *testing.T) {
|
||||
func(ctx context.Context, request *org.ListOrganizationsRequest) ([]orgAttr, error) {
|
||||
orgs := make([]orgAttr, 1)
|
||||
name := fmt.Sprintf("ListOrgs-%s", gofakeit.AppName())
|
||||
orgResp := Instance.CreateOrganization(ctx, name, gofakeit.Email())
|
||||
orgs[0] = orgAttr{
|
||||
ID: orgResp.GetOrganizationId(),
|
||||
Name: name,
|
||||
Details: orgResp.GetDetails(),
|
||||
}
|
||||
orgs[0] = createOrganization(ctx, name)
|
||||
domain := gofakeit.DomainName()
|
||||
_, err := Instance.Client.Mgmt.AddOrgDomain(integration.SetOrgID(ctx, orgResp.GetOrganizationId()), &management.AddOrgDomainRequest{
|
||||
_, err := Instance.Client.Mgmt.AddOrgDomain(integration.SetOrgID(ctx, orgs[0].ID), &management.AddOrgDomainRequest{
|
||||
Domain: domain,
|
||||
})
|
||||
if err != nil {
|
||||
@ -262,18 +266,19 @@ func TestServer_ListOrganizations(t *testing.T) {
|
||||
},
|
||||
func(ctx context.Context, request *org.ListOrganizationsRequest) ([]orgAttr, error) {
|
||||
name := gofakeit.Name()
|
||||
orgResp := Instance.CreateOrganization(ctx, name, gofakeit.Email())
|
||||
deactivateOrgResp := Instance.DeactivateOrganization(ctx, orgResp.GetOrganizationId())
|
||||
orgResp := createOrganization(ctx, name)
|
||||
deactivateOrgResp := Instance.DeactivateOrganization(ctx, orgResp.ID)
|
||||
request.Queries = []*org.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.GetOrganizationId()),
|
||||
OrganizationIdQuery(orgResp.ID),
|
||||
OrganizationStateQuery(org.OrganizationState_ORGANIZATION_STATE_INACTIVE),
|
||||
}
|
||||
return []orgAttr{{
|
||||
ID: orgResp.GetOrganizationId(),
|
||||
ID: orgResp.ID,
|
||||
Name: name,
|
||||
Details: &object.Details{
|
||||
ResourceOwner: deactivateOrgResp.GetDetails().GetResourceOwner(),
|
||||
Sequence: deactivateOrgResp.GetDetails().GetSequence(),
|
||||
CreationDate: orgResp.Details.GetCreationDate(),
|
||||
ChangeDate: deactivateOrgResp.GetDetails().GetChangeDate(),
|
||||
},
|
||||
}}, nil
|
||||
@ -317,6 +322,7 @@ func TestServer_ListOrganizations(t *testing.T) {
|
||||
Name: Instance.DefaultOrg.Name,
|
||||
Details: &object.Details{
|
||||
Sequence: Instance.DefaultOrg.Details.Sequence,
|
||||
CreationDate: Instance.DefaultOrg.Details.ChangeDate,
|
||||
ChangeDate: Instance.DefaultOrg.Details.ChangeDate,
|
||||
ResourceOwner: Instance.DefaultOrg.Details.ResourceOwner,
|
||||
},
|
||||
@ -414,6 +420,7 @@ func TestServer_ListOrganizations(t *testing.T) {
|
||||
Name: Instance.DefaultOrg.Name,
|
||||
Details: &object.Details{
|
||||
Sequence: Instance.DefaultOrg.Details.Sequence,
|
||||
CreationDate: Instance.DefaultOrg.Details.ChangeDate,
|
||||
ChangeDate: Instance.DefaultOrg.Details.ChangeDate,
|
||||
ResourceOwner: Instance.DefaultOrg.Details.ResourceOwner,
|
||||
},
|
||||
|
@ -129,6 +129,7 @@ func organizationToPb(organization *query.Org) *org.Organization {
|
||||
Sequence: organization.Sequence,
|
||||
EventDate: organization.ChangeDate,
|
||||
ResourceOwner: organization.ResourceOwner,
|
||||
CreationDate: organization.CreationDate,
|
||||
}),
|
||||
State: orgStateToPb(organization.State),
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ var (
|
||||
CTX context.Context
|
||||
IAMOwnerCTX context.Context
|
||||
UserCTX context.Context
|
||||
LoginCTX context.Context
|
||||
Instance *integration.Instance
|
||||
Client session.SessionServiceClient
|
||||
User *user.AddHumanUserResponse
|
||||
@ -37,6 +38,7 @@ func TestMain(m *testing.M) {
|
||||
CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)
|
||||
IAMOwnerCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
|
||||
UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeNoPermission)
|
||||
LoginCTX = Instance.WithAuthorization(ctx, integration.UserTypeLogin)
|
||||
User = createFullUser(CTX)
|
||||
DeactivatedUser = createDeactivatedUser(CTX)
|
||||
LockedUser = createLockedUser(CTX)
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/internal/integration/sink"
|
||||
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
@ -339,10 +340,9 @@ func TestServer_CreateSession_webauthn(t *testing.T) {
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactorUserVerified)
|
||||
}
|
||||
|
||||
/*
|
||||
func TestServer_CreateSession_successfulIntent(t *testing.T) {
|
||||
idpID := Instance.AddGenericOAuthProvider(t, CTX)
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId()
|
||||
createResp, err := Client.CreateSession(LoginCTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
@ -354,8 +354,9 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
|
||||
|
||||
intentID, token, _, _ := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID, User.GetUserId(), "id")
|
||||
updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId())
|
||||
require.NoError(t, err)
|
||||
updateResp, err := Client.SetSession(LoginCTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
IdpIntent: &session.CheckIDPIntent{
|
||||
@ -369,9 +370,10 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_CreateSession_successfulIntent_instant(t *testing.T) {
|
||||
idpID := Instance.AddGenericOAuthProvider(t, CTX)
|
||||
idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId()
|
||||
|
||||
intentID, token, _, _ := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID, User.GetUserId(), "id")
|
||||
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId())
|
||||
require.NoError(t, err)
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
@ -390,11 +392,11 @@ func TestServer_CreateSession_successfulIntent_instant(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) {
|
||||
idpID := Instance.AddGenericOAuthProvider(t, CTX)
|
||||
idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId()
|
||||
|
||||
// successful intent without known / linked user
|
||||
idpUserID := "id"
|
||||
intentID, token, _, _ := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID, "", idpUserID)
|
||||
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, idpUserID, "")
|
||||
|
||||
// link the user (with info from intent)
|
||||
Instance.CreateUserIDPlink(CTX, User.GetUserId(), idpUserID, idpID, User.GetUserId())
|
||||
@ -418,7 +420,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) {
|
||||
idpID := Instance.AddGenericOAuthProvider(t, CTX)
|
||||
idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId()
|
||||
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
@ -432,19 +434,18 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
|
||||
|
||||
intentID := Instance.CreateIntent(t, CTX, idpID)
|
||||
intent := Instance.CreateIntent(CTX, idpID)
|
||||
_, err = Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
IdpIntent: &session.CheckIDPIntent{
|
||||
IdpIntentId: intentID,
|
||||
IdpIntentId: intent.GetIdpIntent().GetIdpIntentId(),
|
||||
IdpIntentToken: "false",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
}
|
||||
*/
|
||||
|
||||
func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) {
|
||||
resp, err := Instance.Client.UserV2.RegisterTOTP(ctx, &user.RegisterTOTPRequest{
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/internal/integration/sink"
|
||||
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
|
||||
@ -339,9 +340,8 @@ func TestServer_CreateSession_webauthn(t *testing.T) {
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactorUserVerified)
|
||||
}
|
||||
|
||||
/*
|
||||
func TestServer_CreateSession_successfulIntent(t *testing.T) {
|
||||
idpID := Instance.AddGenericOAuthProvider(t, CTX)
|
||||
idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId()
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
@ -354,7 +354,8 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
|
||||
|
||||
intentID, token, _, _ := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID, User.GetUserId(), "id")
|
||||
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId())
|
||||
require.NoError(t, err)
|
||||
updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
@ -369,9 +370,10 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_CreateSession_successfulIntent_instant(t *testing.T) {
|
||||
idpID := Instance.AddGenericOAuthProvider(t, CTX)
|
||||
idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId()
|
||||
|
||||
intentID, token, _, _ := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID, User.GetUserId(), "id")
|
||||
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId())
|
||||
require.NoError(t, err)
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
@ -390,11 +392,12 @@ func TestServer_CreateSession_successfulIntent_instant(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) {
|
||||
idpID := Instance.AddGenericOAuthProvider(t, CTX)
|
||||
idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId()
|
||||
|
||||
// successful intent without known / linked user
|
||||
idpUserID := "id"
|
||||
intentID, token, _, _ := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID, "", idpUserID)
|
||||
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId())
|
||||
require.NoError(t, err)
|
||||
|
||||
// link the user (with info from intent)
|
||||
Instance.CreateUserIDPlink(CTX, User.GetUserId(), idpUserID, idpID, User.GetUserId())
|
||||
@ -418,7 +421,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) {
|
||||
idpID := Instance.AddGenericOAuthProvider(t, CTX)
|
||||
idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId()
|
||||
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
@ -432,19 +435,18 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
|
||||
|
||||
intentID := Instance.CreateIntent(t, CTX, idpID)
|
||||
intent := Instance.CreateIntent(CTX, idpID)
|
||||
_, err = Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
IdpIntent: &session.CheckIDPIntent{
|
||||
IdpIntentId: intentID,
|
||||
IdpIntentId: intent.GetIdpIntent().GetIdpIntentId(),
|
||||
IdpIntentToken: "false",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
}
|
||||
*/
|
||||
|
||||
func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) {
|
||||
resp, err := Instance.Client.UserV2.RegisterTOTP(ctx, &user.RegisterTOTPRequest{
|
||||
|
@ -23,6 +23,7 @@ func (s *Server) GetLoginSettings(ctx context.Context, req *settings.GetLoginSet
|
||||
Settings: loginSettingsToPb(current),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
CreationDate: timestamppb.New(current.CreationDate),
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.OrgID,
|
||||
},
|
||||
@ -38,6 +39,7 @@ func (s *Server) GetPasswordComplexitySettings(ctx context.Context, req *setting
|
||||
Settings: passwordComplexitySettingsToPb(current),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
CreationDate: timestamppb.New(current.CreationDate),
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.ResourceOwner,
|
||||
},
|
||||
@ -53,6 +55,7 @@ func (s *Server) GetPasswordExpirySettings(ctx context.Context, req *settings.Ge
|
||||
Settings: passwordExpirySettingsToPb(current),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
CreationDate: timestamppb.New(current.CreationDate),
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.ResourceOwner,
|
||||
},
|
||||
@ -68,6 +71,7 @@ func (s *Server) GetBrandingSettings(ctx context.Context, req *settings.GetBrand
|
||||
Settings: brandingSettingsToPb(current, s.assetsAPIDomain(ctx)),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
CreationDate: timestamppb.New(current.CreationDate),
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.ResourceOwner,
|
||||
},
|
||||
@ -83,6 +87,7 @@ func (s *Server) GetDomainSettings(ctx context.Context, req *settings.GetDomainS
|
||||
Settings: domainSettingsToPb(current),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
CreationDate: timestamppb.New(current.CreationDate),
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.ResourceOwner,
|
||||
},
|
||||
@ -98,6 +103,7 @@ func (s *Server) GetLegalAndSupportSettings(ctx context.Context, req *settings.G
|
||||
Settings: legalAndSupportSettingsToPb(current),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
CreationDate: timestamppb.New(current.CreationDate),
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.ResourceOwner,
|
||||
},
|
||||
@ -113,6 +119,7 @@ func (s *Server) GetLockoutSettings(ctx context.Context, req *settings.GetLockou
|
||||
Settings: lockoutSettingsToPb(current),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
CreationDate: timestamppb.New(current.CreationDate),
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.ResourceOwner,
|
||||
},
|
||||
|
@ -23,6 +23,7 @@ func (s *Server) GetLoginSettings(ctx context.Context, req *settings.GetLoginSet
|
||||
Settings: loginSettingsToPb(current),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
CreationDate: timestamppb.New(current.CreationDate),
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.OrgID,
|
||||
},
|
||||
@ -38,6 +39,7 @@ func (s *Server) GetPasswordComplexitySettings(ctx context.Context, req *setting
|
||||
Settings: passwordComplexitySettingsToPb(current),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
CreationDate: timestamppb.New(current.CreationDate),
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.ResourceOwner,
|
||||
},
|
||||
@ -53,6 +55,7 @@ func (s *Server) GetPasswordExpirySettings(ctx context.Context, req *settings.Ge
|
||||
Settings: passwordExpirySettingsToPb(current),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
CreationDate: timestamppb.New(current.CreationDate),
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.ResourceOwner,
|
||||
},
|
||||
@ -68,6 +71,7 @@ func (s *Server) GetBrandingSettings(ctx context.Context, req *settings.GetBrand
|
||||
Settings: brandingSettingsToPb(current, s.assetsAPIDomain(ctx)),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
CreationDate: timestamppb.New(current.CreationDate),
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.ResourceOwner,
|
||||
},
|
||||
@ -83,6 +87,7 @@ func (s *Server) GetDomainSettings(ctx context.Context, req *settings.GetDomainS
|
||||
Settings: domainSettingsToPb(current),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
CreationDate: timestamppb.New(current.CreationDate),
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.ResourceOwner,
|
||||
},
|
||||
@ -98,6 +103,7 @@ func (s *Server) GetLegalAndSupportSettings(ctx context.Context, req *settings.G
|
||||
Settings: legalAndSupportSettingsToPb(current),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
CreationDate: timestamppb.New(current.CreationDate),
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.ResourceOwner,
|
||||
},
|
||||
@ -113,6 +119,7 @@ func (s *Server) GetLockoutSettings(ctx context.Context, req *settings.GetLockou
|
||||
Settings: lockoutSettingsToPb(current),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
CreationDate: timestamppb.New(current.CreationDate),
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.ResourceOwner,
|
||||
},
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
instance_pb "github.com/zitadel/zitadel/pkg/grpc/instance"
|
||||
member_pb "github.com/zitadel/zitadel/pkg/grpc/member"
|
||||
system_pb "github.com/zitadel/zitadel/pkg/grpc/system"
|
||||
)
|
||||
|
||||
@ -271,12 +272,29 @@ func ListIAMMembersRequestToQuery(req *system_pb.ListIAMMembersRequest) (*query.
|
||||
return &query.IAMMembersQuery{
|
||||
MembersQuery: query.MembersQuery{
|
||||
SearchRequest: query.SearchRequest{
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Asc: asc,
|
||||
// SortingColumn: model.IAMMemberSearchKey, //TOOD: not implemented in proto
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Asc: asc,
|
||||
SortingColumn: fieldNameToMemberColumn(req.SortingColumn),
|
||||
},
|
||||
Queries: queries,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func fieldNameToMemberColumn(fieldName member_pb.MemberFieldColumnName) query.Column {
|
||||
switch fieldName {
|
||||
case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_UNSPECIFIED:
|
||||
return query.InstanceMemberInstanceID
|
||||
case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_USER_ID:
|
||||
return query.InstanceMemberUserID
|
||||
case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_CREATION_DATE:
|
||||
return query.InstanceMemberCreationDate
|
||||
case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_CHANGE_DATE:
|
||||
return query.InstanceMemberChangeDate
|
||||
case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_USER_RESOURCE_OWNER:
|
||||
return query.InstanceMemberResourceOwner
|
||||
default:
|
||||
return query.Column{}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ package user_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"testing"
|
||||
@ -16,10 +17,61 @@ import (
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
permissionCheckV2SetFlagInital bool
|
||||
permissionCheckV2SetFlag bool
|
||||
)
|
||||
|
||||
type permissionCheckV2SettingsStruct struct {
|
||||
TestNamePrependString string
|
||||
SetFlag bool
|
||||
}
|
||||
|
||||
var permissionCheckV2Settings []permissionCheckV2SettingsStruct = []permissionCheckV2SettingsStruct{
|
||||
{
|
||||
SetFlag: false,
|
||||
TestNamePrependString: "permission_check_v2 IS NOT SET" + " ",
|
||||
},
|
||||
{
|
||||
SetFlag: true,
|
||||
TestNamePrependString: "permission_check_v2 IS SET" + " ",
|
||||
},
|
||||
}
|
||||
|
||||
func setPermissionCheckV2Flag(t *testing.T, setFlag bool) {
|
||||
if permissionCheckV2SetFlagInital && permissionCheckV2SetFlag == setFlag {
|
||||
return
|
||||
}
|
||||
|
||||
_, err := Instance.Client.FeatureV2.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{
|
||||
PermissionCheckV2: &setFlag,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var flagSet bool
|
||||
for i := 0; !flagSet || i < 6; i++ {
|
||||
res, err := Instance.Client.FeatureV2.GetInstanceFeatures(IamCTX, &feature.GetInstanceFeaturesRequest{})
|
||||
require.NoError(t, err)
|
||||
if res.PermissionCheckV2.Enabled == setFlag {
|
||||
flagSet = true
|
||||
continue
|
||||
}
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
|
||||
if !flagSet {
|
||||
require.NoError(t, errors.New("unable to set permission_check_v2 flag"))
|
||||
}
|
||||
permissionCheckV2SetFlagInital = true
|
||||
permissionCheckV2SetFlag = setFlag
|
||||
}
|
||||
|
||||
func TestServer_GetUserByID(t *testing.T) {
|
||||
orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email())
|
||||
type args struct {
|
||||
@ -98,6 +150,7 @@ func TestServer_GetUserByID(t *testing.T) {
|
||||
},
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
CreationDate: timestamppb.Now(),
|
||||
ResourceOwner: orgResp.OrganizationId,
|
||||
},
|
||||
},
|
||||
@ -143,6 +196,7 @@ func TestServer_GetUserByID(t *testing.T) {
|
||||
},
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
CreationDate: timestamppb.Now(),
|
||||
ResourceOwner: orgResp.OrganizationId,
|
||||
},
|
||||
},
|
||||
@ -230,6 +284,7 @@ func TestServer_GetUserByID_Permission(t *testing.T) {
|
||||
},
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
CreationDate: timestamppb.Now(),
|
||||
ResourceOwner: newOrg.GetOrganizationId(),
|
||||
},
|
||||
},
|
||||
@ -268,6 +323,7 @@ func TestServer_GetUserByID_Permission(t *testing.T) {
|
||||
},
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
CreationDate: timestamppb.Now(),
|
||||
ResourceOwner: newOrg.GetOrganizationId(),
|
||||
},
|
||||
},
|
||||
@ -363,6 +419,8 @@ func createUser(ctx context.Context, orgID string, passwordChangeRequired bool)
|
||||
phone := "+41" + gofakeit.Phone()
|
||||
resp := Instance.CreateHumanUserVerified(ctx, orgID, username, phone)
|
||||
info := userAttr{resp.GetUserId(), username, phone, nil, resp.GetDetails()}
|
||||
// as the change date of the creation is the creation date
|
||||
resp.Details.CreationDate = resp.GetDetails().GetChangeDate()
|
||||
if passwordChangeRequired {
|
||||
details := Instance.SetUserPassword(ctx, resp.GetUserId(), integration.UserPassword, true)
|
||||
info.Changed = details.GetChangeDate()
|
||||
@ -371,6 +429,11 @@ func createUser(ctx context.Context, orgID string, passwordChangeRequired bool)
|
||||
}
|
||||
|
||||
func TestServer_ListUsers(t *testing.T) {
|
||||
defer func() {
|
||||
_, err := Instance.Client.FeatureV2.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{})
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), gofakeit.Email())
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@ -384,7 +447,7 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "list user by id, no permission",
|
||||
name: "list user by id, no permission machine user",
|
||||
args: args{
|
||||
UserCTX,
|
||||
&user.ListUsersRequest{},
|
||||
@ -403,17 +466,77 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
Result: []*user.User{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list user by id, no permission human user",
|
||||
args: func() args {
|
||||
info := createUser(IamCTX, orgResp.OrganizationId, true)
|
||||
// create session to get token
|
||||
userID := info.UserID
|
||||
createResp, err := Instance.Client.SessionV2.CreateSession(IamCTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{UserId: userID},
|
||||
},
|
||||
Password: &session.CheckPassword{
|
||||
Password: integration.UserPassword,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
// use token to get ctx
|
||||
HumanCTX := integration.WithAuthorizationToken(IamCTX, createResp.GetSessionToken())
|
||||
return args{
|
||||
HumanCTX,
|
||||
&user.ListUsersRequest{},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
return []userAttr{info}
|
||||
},
|
||||
}
|
||||
}(),
|
||||
want: &user.ListUsersResponse{ // human user should return itself when calling ListUsers() even if it has no permissions
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 1,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
SortingColumn: 0,
|
||||
Result: []*user.User{
|
||||
{
|
||||
State: user.UserState_USER_STATE_ACTIVE,
|
||||
Type: &user.User_Human{
|
||||
Human: &user.HumanUser{
|
||||
Profile: &user.HumanProfile{
|
||||
GivenName: "Mickey",
|
||||
FamilyName: "Mouse",
|
||||
NickName: gu.Ptr("Mickey"),
|
||||
DisplayName: gu.Ptr("Mickey Mouse"),
|
||||
PreferredLanguage: gu.Ptr("nl"),
|
||||
Gender: user.Gender_GENDER_MALE.Enum(),
|
||||
},
|
||||
Email: &user.HumanEmail{
|
||||
IsVerified: true,
|
||||
},
|
||||
Phone: &user.HumanPhone{
|
||||
IsVerified: true,
|
||||
},
|
||||
PasswordChangeRequired: true,
|
||||
PasswordChanged: timestamppb.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list user by id, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{
|
||||
Queries: []*user.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.OrganizationId),
|
||||
},
|
||||
},
|
||||
&user.ListUsersRequest{},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
info := createUser(ctx, orgResp.OrganizationId, false)
|
||||
request.Queries = []*user.SearchQuery{}
|
||||
request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId))
|
||||
request.Queries = append(request.Queries, InUserIDsQuery([]string{info.UserID}))
|
||||
return []userAttr{info}
|
||||
},
|
||||
@ -453,13 +576,11 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
name: "list user by id, passwordChangeRequired, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{
|
||||
Queries: []*user.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.OrganizationId),
|
||||
},
|
||||
},
|
||||
&user.ListUsersRequest{},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
info := createUser(ctx, orgResp.OrganizationId, true)
|
||||
request.Queries = []*user.SearchQuery{}
|
||||
request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId))
|
||||
request.Queries = append(request.Queries, InUserIDsQuery([]string{info.UserID}))
|
||||
return []userAttr{info}
|
||||
},
|
||||
@ -501,13 +622,11 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
name: "list user by id multiple, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{
|
||||
Queries: []*user.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.OrganizationId),
|
||||
},
|
||||
},
|
||||
&user.ListUsersRequest{},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
infos := createUsers(ctx, orgResp.OrganizationId, 3, false)
|
||||
request.Queries = []*user.SearchQuery{}
|
||||
request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId))
|
||||
request.Queries = append(request.Queries, InUserIDsQuery(infos.userIDs()))
|
||||
return infos
|
||||
},
|
||||
@ -560,7 +679,8 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
},
|
||||
{
|
||||
State: user.UserState_USER_STATE_ACTIVE,
|
||||
Type: &user.User_Human{
|
||||
Human: &user.HumanUser{
|
||||
@ -588,13 +708,11 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
name: "list user by username, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{
|
||||
Queries: []*user.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.OrganizationId),
|
||||
},
|
||||
},
|
||||
&user.ListUsersRequest{},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
info := createUser(ctx, orgResp.OrganizationId, false)
|
||||
request.Queries = []*user.SearchQuery{}
|
||||
request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId))
|
||||
request.Queries = append(request.Queries, UsernameQuery(info.Username))
|
||||
return []userAttr{info}
|
||||
},
|
||||
@ -634,13 +752,11 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
name: "list user in emails, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{
|
||||
Queries: []*user.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.OrganizationId),
|
||||
},
|
||||
},
|
||||
&user.ListUsersRequest{},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
info := createUser(ctx, orgResp.OrganizationId, false)
|
||||
request.Queries = []*user.SearchQuery{}
|
||||
request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId))
|
||||
request.Queries = append(request.Queries, InUserEmailsQuery([]string{info.Username}))
|
||||
return []userAttr{info}
|
||||
},
|
||||
@ -678,189 +794,12 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "list user in emails multiple, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{
|
||||
Queries: []*user.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.OrganizationId),
|
||||
},
|
||||
},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
infos := createUsers(ctx, orgResp.OrganizationId, 3, false)
|
||||
request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails()))
|
||||
return infos
|
||||
},
|
||||
},
|
||||
want: &user.ListUsersResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 3,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
SortingColumn: 0,
|
||||
Result: []*user.User{
|
||||
{
|
||||
State: user.UserState_USER_STATE_ACTIVE,
|
||||
Type: &user.User_Human{
|
||||
Human: &user.HumanUser{
|
||||
Profile: &user.HumanProfile{
|
||||
GivenName: "Mickey",
|
||||
FamilyName: "Mouse",
|
||||
NickName: gu.Ptr("Mickey"),
|
||||
DisplayName: gu.Ptr("Mickey Mouse"),
|
||||
PreferredLanguage: gu.Ptr("nl"),
|
||||
Gender: user.Gender_GENDER_MALE.Enum(),
|
||||
},
|
||||
Email: &user.HumanEmail{
|
||||
IsVerified: true,
|
||||
},
|
||||
Phone: &user.HumanPhone{
|
||||
IsVerified: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
State: user.UserState_USER_STATE_ACTIVE,
|
||||
Type: &user.User_Human{
|
||||
Human: &user.HumanUser{
|
||||
Profile: &user.HumanProfile{
|
||||
GivenName: "Mickey",
|
||||
FamilyName: "Mouse",
|
||||
NickName: gu.Ptr("Mickey"),
|
||||
DisplayName: gu.Ptr("Mickey Mouse"),
|
||||
PreferredLanguage: gu.Ptr("nl"),
|
||||
Gender: user.Gender_GENDER_MALE.Enum(),
|
||||
},
|
||||
Email: &user.HumanEmail{
|
||||
IsVerified: true,
|
||||
},
|
||||
Phone: &user.HumanPhone{
|
||||
IsVerified: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
State: user.UserState_USER_STATE_ACTIVE,
|
||||
Type: &user.User_Human{
|
||||
Human: &user.HumanUser{
|
||||
Profile: &user.HumanProfile{
|
||||
GivenName: "Mickey",
|
||||
FamilyName: "Mouse",
|
||||
NickName: gu.Ptr("Mickey"),
|
||||
DisplayName: gu.Ptr("Mickey Mouse"),
|
||||
PreferredLanguage: gu.Ptr("nl"),
|
||||
Gender: user.Gender_GENDER_MALE.Enum(),
|
||||
},
|
||||
Email: &user.HumanEmail{
|
||||
IsVerified: true,
|
||||
},
|
||||
Phone: &user.HumanPhone{
|
||||
IsVerified: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list user in emails no found, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{Queries: []*user.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.OrganizationId),
|
||||
InUserEmailsQuery([]string{"notfound"}),
|
||||
},
|
||||
},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
return []userAttr{}
|
||||
},
|
||||
},
|
||||
want: &user.ListUsersResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 0,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
SortingColumn: 0,
|
||||
Result: []*user.User{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list user phone, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{
|
||||
Queries: []*user.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.OrganizationId),
|
||||
},
|
||||
},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
info := createUser(ctx, orgResp.OrganizationId, false)
|
||||
request.Queries = append(request.Queries, PhoneQuery(info.Phone))
|
||||
return []userAttr{info}
|
||||
},
|
||||
},
|
||||
want: &user.ListUsersResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 1,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
SortingColumn: 0,
|
||||
Result: []*user.User{
|
||||
{
|
||||
State: user.UserState_USER_STATE_ACTIVE,
|
||||
Type: &user.User_Human{
|
||||
Human: &user.HumanUser{
|
||||
Profile: &user.HumanProfile{
|
||||
GivenName: "Mickey",
|
||||
FamilyName: "Mouse",
|
||||
NickName: gu.Ptr("Mickey"),
|
||||
DisplayName: gu.Ptr("Mickey Mouse"),
|
||||
PreferredLanguage: gu.Ptr("nl"),
|
||||
Gender: user.Gender_GENDER_MALE.Enum(),
|
||||
},
|
||||
Email: &user.HumanEmail{
|
||||
IsVerified: true,
|
||||
},
|
||||
Phone: &user.HumanPhone{
|
||||
IsVerified: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list user in emails no found, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{Queries: []*user.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.OrganizationId),
|
||||
InUserEmailsQuery([]string{"notfound"}),
|
||||
},
|
||||
},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
return []userAttr{}
|
||||
},
|
||||
},
|
||||
want: &user.ListUsersResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 0,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
SortingColumn: 0,
|
||||
Result: []*user.User{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list user resourceowner multiple, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
orgResp := Instance.CreateOrganization(ctx, fmt.Sprintf("ListUsersResourceowner-%s", gofakeit.AppName()), gofakeit.Email())
|
||||
|
||||
infos := createUsers(ctx, orgResp.OrganizationId, 3, false)
|
||||
request.Queries = []*user.SearchQuery{}
|
||||
request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId))
|
||||
request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails()))
|
||||
return infos
|
||||
@ -937,93 +876,355 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list user in emails no found, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{
|
||||
Queries: []*user.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.OrganizationId),
|
||||
InUserEmailsQuery([]string{"notfound"}),
|
||||
},
|
||||
},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
return []userAttr{}
|
||||
},
|
||||
},
|
||||
want: &user.ListUsersResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 0,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
SortingColumn: 0,
|
||||
Result: []*user.User{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list user phone, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
info := createUser(ctx, orgResp.OrganizationId, false)
|
||||
request.Queries = []*user.SearchQuery{}
|
||||
request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId))
|
||||
request.Queries = append(request.Queries, PhoneQuery(info.Phone))
|
||||
return []userAttr{info}
|
||||
},
|
||||
},
|
||||
want: &user.ListUsersResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 1,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
SortingColumn: 0,
|
||||
Result: []*user.User{
|
||||
{
|
||||
State: user.UserState_USER_STATE_ACTIVE,
|
||||
Type: &user.User_Human{
|
||||
Human: &user.HumanUser{
|
||||
Profile: &user.HumanProfile{
|
||||
GivenName: "Mickey",
|
||||
FamilyName: "Mouse",
|
||||
NickName: gu.Ptr("Mickey"),
|
||||
DisplayName: gu.Ptr("Mickey Mouse"),
|
||||
PreferredLanguage: gu.Ptr("nl"),
|
||||
Gender: user.Gender_GENDER_MALE.Enum(),
|
||||
},
|
||||
Email: &user.HumanEmail{
|
||||
IsVerified: true,
|
||||
},
|
||||
Phone: &user.HumanPhone{
|
||||
IsVerified: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list user in emails no found, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{
|
||||
Queries: []*user.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.OrganizationId),
|
||||
InUserEmailsQuery([]string{"notfound"}),
|
||||
},
|
||||
},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
return []userAttr{}
|
||||
},
|
||||
},
|
||||
want: &user.ListUsersResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 0,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
SortingColumn: 0,
|
||||
Result: []*user.User{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list user resourceowner multiple, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
orgResp := Instance.CreateOrganization(ctx, fmt.Sprintf("ListUsersResourceowner-%s", gofakeit.AppName()), gofakeit.Email())
|
||||
|
||||
infos := createUsers(ctx, orgResp.OrganizationId, 3, false)
|
||||
request.Queries = []*user.SearchQuery{}
|
||||
request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId))
|
||||
request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails()))
|
||||
return infos
|
||||
},
|
||||
},
|
||||
want: &user.ListUsersResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 3,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
SortingColumn: 0,
|
||||
Result: []*user.User{
|
||||
{
|
||||
State: user.UserState_USER_STATE_ACTIVE,
|
||||
Type: &user.User_Human{
|
||||
Human: &user.HumanUser{
|
||||
Profile: &user.HumanProfile{
|
||||
GivenName: "Mickey",
|
||||
FamilyName: "Mouse",
|
||||
NickName: gu.Ptr("Mickey"),
|
||||
DisplayName: gu.Ptr("Mickey Mouse"),
|
||||
PreferredLanguage: gu.Ptr("nl"),
|
||||
Gender: user.Gender_GENDER_MALE.Enum(),
|
||||
},
|
||||
Email: &user.HumanEmail{
|
||||
IsVerified: true,
|
||||
},
|
||||
Phone: &user.HumanPhone{
|
||||
IsVerified: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
State: user.UserState_USER_STATE_ACTIVE,
|
||||
Type: &user.User_Human{
|
||||
Human: &user.HumanUser{
|
||||
Profile: &user.HumanProfile{
|
||||
GivenName: "Mickey",
|
||||
FamilyName: "Mouse",
|
||||
NickName: gu.Ptr("Mickey"),
|
||||
DisplayName: gu.Ptr("Mickey Mouse"),
|
||||
PreferredLanguage: gu.Ptr("nl"),
|
||||
Gender: user.Gender_GENDER_MALE.Enum(),
|
||||
},
|
||||
Email: &user.HumanEmail{
|
||||
IsVerified: true,
|
||||
},
|
||||
Phone: &user.HumanPhone{
|
||||
IsVerified: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
State: user.UserState_USER_STATE_ACTIVE,
|
||||
Type: &user.User_Human{
|
||||
Human: &user.HumanUser{
|
||||
Profile: &user.HumanProfile{
|
||||
GivenName: "Mickey",
|
||||
FamilyName: "Mouse",
|
||||
NickName: gu.Ptr("Mickey"),
|
||||
DisplayName: gu.Ptr("Mickey Mouse"),
|
||||
PreferredLanguage: gu.Ptr("nl"),
|
||||
Gender: user.Gender_GENDER_MALE.Enum(),
|
||||
},
|
||||
Email: &user.HumanEmail{
|
||||
IsVerified: true,
|
||||
},
|
||||
Phone: &user.HumanPhone{
|
||||
IsVerified: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list user with org query",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
orgRespForOrgTests := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email())
|
||||
info := createUser(ctx, orgRespForOrgTests.OrganizationId, false)
|
||||
request.Queries = []*user.SearchQuery{}
|
||||
request.Queries = append(request.Queries, OrganizationIdQuery(orgRespForOrgTests.OrganizationId))
|
||||
return []userAttr{info, {}}
|
||||
},
|
||||
},
|
||||
want: &user.ListUsersResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 2,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
SortingColumn: 0,
|
||||
Result: []*user.User{
|
||||
{
|
||||
State: user.UserState_USER_STATE_ACTIVE,
|
||||
Type: &user.User_Human{
|
||||
Human: &user.HumanUser{
|
||||
Profile: &user.HumanProfile{
|
||||
GivenName: "Mickey",
|
||||
FamilyName: "Mouse",
|
||||
NickName: gu.Ptr("Mickey"),
|
||||
DisplayName: gu.Ptr("Mickey Mouse"),
|
||||
PreferredLanguage: gu.Ptr("nl"),
|
||||
Gender: user.Gender_GENDER_MALE.Enum(),
|
||||
},
|
||||
Email: &user.HumanEmail{
|
||||
IsVerified: true,
|
||||
},
|
||||
Phone: &user.HumanPhone{
|
||||
IsVerified: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// this is the admin of the org craated in Instance.CreateOrganization()
|
||||
nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list user with wrong org query",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
orgRespForOrgTests := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email())
|
||||
orgRespForOrgTests2 := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email())
|
||||
// info := createUser(ctx, orgRespForOrgTests.OrganizationId, false)
|
||||
createUser(ctx, orgRespForOrgTests.OrganizationId, false)
|
||||
request.Queries = []*user.SearchQuery{}
|
||||
request.Queries = append(request.Queries, OrganizationIdQuery(orgRespForOrgTests2.OrganizationId))
|
||||
return []userAttr{{}}
|
||||
},
|
||||
},
|
||||
want: &user.ListUsersResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 0,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
SortingColumn: 0,
|
||||
Result: []*user.User{
|
||||
// this is the admin of the org craated in Instance.CreateOrganization()
|
||||
nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
infos := tt.args.dep(IamCTX, tt.args.req)
|
||||
for _, f := range permissionCheckV2Settings {
|
||||
f := f
|
||||
for _, tt := range tests {
|
||||
t.Run(f.TestNamePrependString+tt.name, func(t *testing.T) {
|
||||
setPermissionCheckV2Flag(t, f.SetFlag)
|
||||
infos := tt.args.dep(IamCTX, tt.args.req)
|
||||
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute)
|
||||
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||
got, err := Client.ListUsers(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(ttt, err)
|
||||
return
|
||||
}
|
||||
require.NoError(ttt, err)
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, 10*time.Minute)
|
||||
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||
got, err := Client.ListUsers(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(ttt, err)
|
||||
return
|
||||
}
|
||||
require.NoError(ttt, err)
|
||||
|
||||
// always only give back dependency infos which are required for the response
|
||||
require.Len(ttt, tt.want.Result, len(infos))
|
||||
// always first check length, otherwise its failed anyway
|
||||
if assert.Len(ttt, got.Result, len(tt.want.Result)) {
|
||||
// totalResult is unrelated to the tests here so gets carried over, can vary from the count of results due to permissions
|
||||
tt.want.Details.TotalResult = got.Details.TotalResult
|
||||
// always only give back dependency infos which are required for the response
|
||||
require.Len(ttt, tt.want.Result, len(infos))
|
||||
if assert.Len(ttt, got.Result, len(tt.want.Result)) {
|
||||
tt.want.Details.TotalResult = got.Details.TotalResult
|
||||
|
||||
// fill in userid and username as it is generated
|
||||
for i := range infos {
|
||||
tt.want.Result[i].UserId = infos[i].UserID
|
||||
tt.want.Result[i].Username = infos[i].Username
|
||||
tt.want.Result[i].PreferredLoginName = infos[i].Username
|
||||
tt.want.Result[i].LoginNames = []string{infos[i].Username}
|
||||
if human := tt.want.Result[i].GetHuman(); human != nil {
|
||||
human.Email.Email = infos[i].Username
|
||||
human.Phone.Phone = infos[i].Phone
|
||||
if tt.want.Result[i].GetHuman().GetPasswordChanged() != nil {
|
||||
human.PasswordChanged = infos[i].Changed
|
||||
// fill in userid and username as it is generated
|
||||
for i := range infos {
|
||||
if tt.want.Result[i] == nil {
|
||||
continue
|
||||
}
|
||||
tt.want.Result[i].UserId = infos[i].UserID
|
||||
tt.want.Result[i].Username = infos[i].Username
|
||||
tt.want.Result[i].PreferredLoginName = infos[i].Username
|
||||
tt.want.Result[i].LoginNames = []string{infos[i].Username}
|
||||
if human := tt.want.Result[i].GetHuman(); human != nil {
|
||||
human.Email.Email = infos[i].Username
|
||||
human.Phone.Phone = infos[i].Phone
|
||||
if tt.want.Result[i].GetHuman().GetPasswordChanged() != nil {
|
||||
human.PasswordChanged = infos[i].Changed
|
||||
}
|
||||
}
|
||||
tt.want.Result[i].Details = infos[i].Details
|
||||
}
|
||||
for i := range tt.want.Result {
|
||||
if tt.want.Result[i] == nil {
|
||||
continue
|
||||
}
|
||||
assert.EqualExportedValues(ttt, got.Result[i], tt.want.Result[i])
|
||||
}
|
||||
tt.want.Result[i].Details = infos[i].Details
|
||||
}
|
||||
for i := range tt.want.Result {
|
||||
assert.EqualExportedValues(ttt, got.Result[i], tt.want.Result[i])
|
||||
}
|
||||
}
|
||||
integration.AssertListDetails(ttt, tt.want, got)
|
||||
}, retryDuration, tick, "timeout waiting for expected user result")
|
||||
})
|
||||
integration.AssertListDetails(ttt, tt.want, got)
|
||||
}, retryDuration, tick, "timeout waiting for expected user result")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func InUserIDsQuery(ids []string) *user.SearchQuery {
|
||||
return &user.SearchQuery{Query: &user.SearchQuery_InUserIdsQuery{
|
||||
InUserIdsQuery: &user.InUserIDQuery{
|
||||
UserIds: ids,
|
||||
return &user.SearchQuery{
|
||||
Query: &user.SearchQuery_InUserIdsQuery{
|
||||
InUserIdsQuery: &user.InUserIDQuery{
|
||||
UserIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func InUserEmailsQuery(emails []string) *user.SearchQuery {
|
||||
return &user.SearchQuery{Query: &user.SearchQuery_InUserEmailsQuery{
|
||||
InUserEmailsQuery: &user.InUserEmailsQuery{
|
||||
UserEmails: emails,
|
||||
return &user.SearchQuery{
|
||||
Query: &user.SearchQuery_InUserEmailsQuery{
|
||||
InUserEmailsQuery: &user.InUserEmailsQuery{
|
||||
UserEmails: emails,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func PhoneQuery(number string) *user.SearchQuery {
|
||||
return &user.SearchQuery{Query: &user.SearchQuery_PhoneQuery{
|
||||
PhoneQuery: &user.PhoneQuery{
|
||||
Number: number,
|
||||
return &user.SearchQuery{
|
||||
Query: &user.SearchQuery_PhoneQuery{
|
||||
PhoneQuery: &user.PhoneQuery{
|
||||
Number: number,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func UsernameQuery(username string) *user.SearchQuery {
|
||||
return &user.SearchQuery{Query: &user.SearchQuery_UserNameQuery{
|
||||
UserNameQuery: &user.UserNameQuery{
|
||||
UserName: username,
|
||||
return &user.SearchQuery{
|
||||
Query: &user.SearchQuery_UserNameQuery{
|
||||
UserNameQuery: &user.UserNameQuery{
|
||||
UserName: username,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func OrganizationIdQuery(resourceowner string) *user.SearchQuery {
|
||||
return &user.SearchQuery{Query: &user.SearchQuery_OrganizationIdQuery{
|
||||
OrganizationIdQuery: &user.OrganizationIdQuery{
|
||||
OrganizationId: resourceowner,
|
||||
return &user.SearchQuery{
|
||||
Query: &user.SearchQuery_OrganizationIdQuery{
|
||||
OrganizationIdQuery: &user.OrganizationIdQuery{
|
||||
OrganizationId: resourceowner,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -10,16 +10,20 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/logging"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/internal/integration/sink"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/auth"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/idp"
|
||||
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
@ -2110,15 +2114,20 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
|
||||
idpID := Instance.AddGenericOAuthProvider(t, CTX)
|
||||
intentID := Instance.CreateIntent(t, CTX, idpID)
|
||||
successfulID, token, changeDate, sequence := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID, "", "id")
|
||||
successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID, "user", "id")
|
||||
ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence := Instance.CreateSuccessfulLDAPIntent(t, CTX, idpID, "", "id")
|
||||
ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence := Instance.CreateSuccessfulLDAPIntent(t, CTX, idpID, "user", "id")
|
||||
samlSuccessfulID, samlToken, samlChangeDate, samlSequence := Instance.CreateSuccessfulSAMLIntent(t, CTX, idpID, "", "id")
|
||||
idpID := Instance.AddGenericOAuthProvider(IamCTX, gofakeit.AppName()).GetId()
|
||||
intentID := Instance.CreateIntent(CTX, idpID).GetIdpIntent().GetIdpIntentId()
|
||||
|
||||
successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", "")
|
||||
require.NoError(t, err)
|
||||
successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", "user")
|
||||
require.NoError(t, err)
|
||||
ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), idpID, "id", "")
|
||||
require.NoError(t, err)
|
||||
ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), idpID, "id", "user")
|
||||
require.NoError(t, err)
|
||||
samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), idpID, "id", "")
|
||||
require.NoError(t, err)
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.RetrieveIdentityProviderIntentRequest
|
||||
@ -2369,7 +2378,6 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func ctxFromNewUserWithRegisteredPasswordlessLegacy(t *testing.T) (context.Context, string, *auth.AddMyPasswordlessResponse) {
|
||||
userID := Instance.CreateHumanUser(CTX).GetUserId()
|
||||
|
@ -74,6 +74,7 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
CreationDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
ID: "123",
|
||||
@ -90,6 +91,7 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
CreationDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
ID: "123",
|
||||
@ -104,6 +106,10 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
|
||||
Seconds: 3000,
|
||||
Nanos: 22,
|
||||
},
|
||||
CreationDate: ×tamppb.Timestamp{
|
||||
Seconds: 3000,
|
||||
Nanos: 22,
|
||||
},
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
PasskeyId: "123",
|
||||
@ -150,6 +156,7 @@ func Test_passkeyDetailsToPb(t *testing.T) {
|
||||
details: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
CreationDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
err: nil,
|
||||
@ -161,6 +168,10 @@ func Test_passkeyDetailsToPb(t *testing.T) {
|
||||
Seconds: 3000,
|
||||
Nanos: 22,
|
||||
},
|
||||
CreationDate: ×tamppb.Timestamp{
|
||||
Seconds: 3000,
|
||||
Nanos: 22,
|
||||
},
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
},
|
||||
@ -199,6 +210,7 @@ func Test_passkeyCodeDetailsToPb(t *testing.T) {
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
CreationDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
CodeID: "123",
|
||||
@ -213,6 +225,10 @@ func Test_passkeyCodeDetailsToPb(t *testing.T) {
|
||||
Seconds: 3000,
|
||||
Nanos: 22,
|
||||
},
|
||||
CreationDate: ×tamppb.Timestamp{
|
||||
Seconds: 3000,
|
||||
Nanos: 22,
|
||||
},
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
Code: &user.PasskeyRegistrationCode{
|
||||
|
@ -21,6 +21,7 @@ func (s *Server) GetUserByID(ctx context.Context, req *user.GetUserByIDRequest)
|
||||
return &user.GetUserByIDResponse{
|
||||
Details: object.DomainToDetailsPb(&domain.ObjectDetails{
|
||||
Sequence: resp.Sequence,
|
||||
CreationDate: resp.CreationDate,
|
||||
EventDate: resp.ChangeDate,
|
||||
ResourceOwner: resp.ResourceOwner,
|
||||
}),
|
||||
@ -58,6 +59,7 @@ func userToPb(userQ *query.User, assetPrefix string) *user.User {
|
||||
Sequence: userQ.Sequence,
|
||||
EventDate: userQ.ChangeDate,
|
||||
ResourceOwner: userQ.ResourceOwner,
|
||||
CreationDate: userQ.CreationDate,
|
||||
}),
|
||||
State: userStateToPb(userQ.State),
|
||||
Username: userQ.Username,
|
||||
|
@ -43,6 +43,7 @@ func Test_u2fRegistrationDetailsToPb(t *testing.T) {
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
CreationDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
ID: "123",
|
||||
@ -59,6 +60,7 @@ func Test_u2fRegistrationDetailsToPb(t *testing.T) {
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
CreationDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
ID: "123",
|
||||
@ -73,6 +75,10 @@ func Test_u2fRegistrationDetailsToPb(t *testing.T) {
|
||||
Seconds: 3000,
|
||||
Nanos: 22,
|
||||
},
|
||||
CreationDate: ×tamppb.Timestamp{
|
||||
Seconds: 3000,
|
||||
Nanos: 22,
|
||||
},
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
U2FId: "123",
|
||||
|
@ -368,31 +368,31 @@ func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *user.Star
|
||||
}
|
||||
|
||||
func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*user.StartIdentityProviderIntentResponse, error) {
|
||||
intentWriteModel, details, err := s.command.CreateIntent(ctx, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetInstance(ctx).InstanceID())
|
||||
state, session, err := s.command.AuthFromProvider(ctx, idpID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content, redirect, err := s.command.AuthFromProvider(ctx, idpID, intentWriteModel.AggregateID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID))
|
||||
_, details, err := s.command.CreateIntent(ctx, state, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetInstance(ctx).InstanceID(), session.PersistentParameters())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content, redirect := session.GetAuth(ctx)
|
||||
if redirect {
|
||||
return &user.StartIdentityProviderIntentResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: content},
|
||||
}, nil
|
||||
} else {
|
||||
return &user.StartIdentityProviderIntentResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
NextStep: &user.StartIdentityProviderIntentResponse_PostForm{
|
||||
PostForm: []byte(content),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return &user.StartIdentityProviderIntentResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
NextStep: &user.StartIdentityProviderIntentResponse_PostForm{
|
||||
PostForm: []byte(content),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*user.StartIdentityProviderIntentResponse, error) {
|
||||
intentWriteModel, details, err := s.command.CreateIntent(ctx, idpID, "", "", authz.GetInstance(ctx).InstanceID())
|
||||
intentWriteModel, details, err := s.command.CreateIntent(ctx, "", idpID, "", "", authz.GetInstance(ctx).InstanceID(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ package user_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"testing"
|
||||
@ -16,8 +17,10 @@ import (
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
object_v2beta "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
@ -29,6 +32,55 @@ func detailsV2ToV2beta(obj *object.Details) *object_v2beta.Details {
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
permissionCheckV2SetFlagInital bool
|
||||
permissionCheckV2SetFlag bool
|
||||
)
|
||||
|
||||
type permissionCheckV2SettingsStruct struct {
|
||||
TestNamePrependString string
|
||||
SetFlag bool
|
||||
}
|
||||
|
||||
var permissionCheckV2Settings []permissionCheckV2SettingsStruct = []permissionCheckV2SettingsStruct{
|
||||
{
|
||||
SetFlag: false,
|
||||
TestNamePrependString: "permission_check_v2 IS NOT SET" + " ",
|
||||
},
|
||||
{
|
||||
SetFlag: true,
|
||||
TestNamePrependString: "permission_check_v2 IS SET" + " ",
|
||||
},
|
||||
}
|
||||
|
||||
func setPermissionCheckV2Flag(t *testing.T, setFlag bool) {
|
||||
if permissionCheckV2SetFlagInital && permissionCheckV2SetFlag == setFlag {
|
||||
return
|
||||
}
|
||||
|
||||
_, err := Instance.Client.FeatureV2.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{
|
||||
PermissionCheckV2: &setFlag,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var flagSet bool
|
||||
for i := 0; !flagSet || i < 6; i++ {
|
||||
res, err := Instance.Client.FeatureV2.GetInstanceFeatures(IamCTX, &feature.GetInstanceFeaturesRequest{})
|
||||
require.NoError(t, err)
|
||||
if res.PermissionCheckV2.Enabled == setFlag {
|
||||
flagSet = true
|
||||
continue
|
||||
}
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
|
||||
if !flagSet {
|
||||
require.NoError(t, errors.New("unable to set permission_check_v2 flag"))
|
||||
}
|
||||
permissionCheckV2SetFlagInital = true
|
||||
permissionCheckV2SetFlag = setFlag
|
||||
}
|
||||
|
||||
func TestServer_GetUserByID(t *testing.T) {
|
||||
orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email())
|
||||
type args struct {
|
||||
@ -381,6 +433,11 @@ func createUser(ctx context.Context, orgID string, passwordChangeRequired bool)
|
||||
}
|
||||
|
||||
func TestServer_ListUsers(t *testing.T) {
|
||||
defer func() {
|
||||
_, err := Instance.Client.FeatureV2.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{})
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), gofakeit.Email())
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@ -394,7 +451,7 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "list user by id, no permission",
|
||||
name: "list user by id, no permission machine user",
|
||||
args: args{
|
||||
UserCTX,
|
||||
&user.ListUsersRequest{},
|
||||
@ -413,17 +470,77 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
Result: []*user.User{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list user by id, no permission human user",
|
||||
args: func() args {
|
||||
info := createUser(IamCTX, orgResp.OrganizationId, true)
|
||||
// create session to get token
|
||||
userID := info.UserID
|
||||
createResp, err := Instance.Client.SessionV2.CreateSession(IamCTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{UserId: userID},
|
||||
},
|
||||
Password: &session.CheckPassword{
|
||||
Password: integration.UserPassword,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
// use token to get ctx
|
||||
HumanCTX := integration.WithAuthorizationToken(IamCTX, createResp.GetSessionToken())
|
||||
return args{
|
||||
HumanCTX,
|
||||
&user.ListUsersRequest{},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
return []userAttr{info}
|
||||
},
|
||||
}
|
||||
}(),
|
||||
want: &user.ListUsersResponse{ // human user should return itself when calling ListUsers() even if it has no permissions
|
||||
Details: &object_v2beta.ListDetails{
|
||||
TotalResult: 1,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
SortingColumn: 0,
|
||||
Result: []*user.User{
|
||||
{
|
||||
State: user.UserState_USER_STATE_ACTIVE,
|
||||
Type: &user.User_Human{
|
||||
Human: &user.HumanUser{
|
||||
Profile: &user.HumanProfile{
|
||||
GivenName: "Mickey",
|
||||
FamilyName: "Mouse",
|
||||
NickName: gu.Ptr("Mickey"),
|
||||
DisplayName: gu.Ptr("Mickey Mouse"),
|
||||
PreferredLanguage: gu.Ptr("nl"),
|
||||
Gender: user.Gender_GENDER_MALE.Enum(),
|
||||
},
|
||||
Email: &user.HumanEmail{
|
||||
IsVerified: true,
|
||||
},
|
||||
Phone: &user.HumanPhone{
|
||||
IsVerified: true,
|
||||
},
|
||||
PasswordChangeRequired: true,
|
||||
PasswordChanged: timestamppb.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list user by id, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{
|
||||
Queries: []*user.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.OrganizationId),
|
||||
},
|
||||
},
|
||||
&user.ListUsersRequest{},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
info := createUser(ctx, orgResp.OrganizationId, false)
|
||||
request.Queries = []*user.SearchQuery{}
|
||||
request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId))
|
||||
request.Queries = append(request.Queries, InUserIDsQuery([]string{info.UserID}))
|
||||
return []userAttr{info}
|
||||
},
|
||||
@ -463,13 +580,11 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
name: "list user by id, passwordChangeRequired, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{
|
||||
Queries: []*user.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.OrganizationId),
|
||||
},
|
||||
},
|
||||
&user.ListUsersRequest{},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
info := createUser(ctx, orgResp.OrganizationId, true)
|
||||
request.Queries = []*user.SearchQuery{}
|
||||
request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId))
|
||||
request.Queries = append(request.Queries, InUserIDsQuery([]string{info.UserID}))
|
||||
return []userAttr{info}
|
||||
},
|
||||
@ -511,13 +626,11 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
name: "list user by id multiple, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{
|
||||
Queries: []*user.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.OrganizationId),
|
||||
},
|
||||
},
|
||||
&user.ListUsersRequest{},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
infos := createUsers(ctx, orgResp.OrganizationId, 3, false)
|
||||
request.Queries = []*user.SearchQuery{}
|
||||
request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId))
|
||||
request.Queries = append(request.Queries, InUserIDsQuery(infos.userIDs()))
|
||||
return infos
|
||||
},
|
||||
@ -549,7 +662,8 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
},
|
||||
{
|
||||
State: user.UserState_USER_STATE_ACTIVE,
|
||||
Type: &user.User_Human{
|
||||
Human: &user.HumanUser{
|
||||
@ -569,7 +683,8 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
},
|
||||
{
|
||||
State: user.UserState_USER_STATE_ACTIVE,
|
||||
Type: &user.User_Human{
|
||||
Human: &user.HumanUser{
|
||||
@ -597,13 +712,11 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
name: "list user by username, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{
|
||||
Queries: []*user.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.OrganizationId),
|
||||
},
|
||||
},
|
||||
&user.ListUsersRequest{},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
info := createUser(ctx, orgResp.OrganizationId, false)
|
||||
request.Queries = []*user.SearchQuery{}
|
||||
request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId))
|
||||
request.Queries = append(request.Queries, UsernameQuery(info.Username))
|
||||
return []userAttr{info}
|
||||
},
|
||||
@ -650,6 +763,7 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
info := createUser(ctx, orgResp.OrganizationId, false)
|
||||
request.Queries = []*user.SearchQuery{}
|
||||
request.Queries = append(request.Queries, InUserEmailsQuery([]string{info.Username}))
|
||||
return []userAttr{info}
|
||||
},
|
||||
@ -689,13 +803,11 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
name: "list user in emails multiple, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{
|
||||
Queries: []*user.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.OrganizationId),
|
||||
},
|
||||
},
|
||||
&user.ListUsersRequest{},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
infos := createUsers(ctx, orgResp.OrganizationId, 3, false)
|
||||
request.Queries = []*user.SearchQuery{}
|
||||
request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId))
|
||||
request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails()))
|
||||
return infos
|
||||
},
|
||||
@ -775,10 +887,11 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
name: "list user in emails no found, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{Queries: []*user.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.OrganizationId),
|
||||
InUserEmailsQuery([]string{"notfound"}),
|
||||
},
|
||||
&user.ListUsersRequest{
|
||||
Queries: []*user.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.OrganizationId),
|
||||
InUserEmailsQuery([]string{"notfound"}),
|
||||
},
|
||||
},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
return []userAttr{}
|
||||
@ -797,13 +910,11 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
name: "list user phone, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{
|
||||
Queries: []*user.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.OrganizationId),
|
||||
},
|
||||
},
|
||||
&user.ListUsersRequest{},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
info := createUser(ctx, orgResp.OrganizationId, false)
|
||||
request.Queries = []*user.SearchQuery{}
|
||||
request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId))
|
||||
request.Queries = append(request.Queries, PhoneQuery(info.Phone))
|
||||
return []userAttr{info}
|
||||
},
|
||||
@ -839,6 +950,29 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list user in emails no found, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{
|
||||
Queries: []*user.SearchQuery{
|
||||
OrganizationIdQuery(orgResp.OrganizationId),
|
||||
InUserEmailsQuery([]string{"notfound"}),
|
||||
},
|
||||
},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
return []userAttr{}
|
||||
},
|
||||
},
|
||||
want: &user.ListUsersResponse{
|
||||
Details: &object_v2beta.ListDetails{
|
||||
TotalResult: 0,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
SortingColumn: 0,
|
||||
Result: []*user.User{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list user resourceowner multiple, ok",
|
||||
args: args{
|
||||
@ -848,6 +982,7 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
orgResp := Instance.CreateOrganization(ctx, fmt.Sprintf("ListUsersResourceowner-%s", gofakeit.AppName()), gofakeit.Email())
|
||||
|
||||
infos := createUsers(ctx, orgResp.OrganizationId, 3, false)
|
||||
request.Queries = []*user.SearchQuery{}
|
||||
request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId))
|
||||
request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails()))
|
||||
return infos
|
||||
@ -924,93 +1059,182 @@ func TestServer_ListUsers(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list user with org query",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
orgRespForOrgTests := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email())
|
||||
info := createUser(ctx, orgRespForOrgTests.OrganizationId, false)
|
||||
request.Queries = []*user.SearchQuery{}
|
||||
request.Queries = append(request.Queries, OrganizationIdQuery(orgRespForOrgTests.OrganizationId))
|
||||
return []userAttr{info, {}}
|
||||
},
|
||||
},
|
||||
want: &user.ListUsersResponse{
|
||||
Details: &object_v2beta.ListDetails{
|
||||
TotalResult: 2,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
SortingColumn: 0,
|
||||
Result: []*user.User{
|
||||
{
|
||||
State: user.UserState_USER_STATE_ACTIVE,
|
||||
Type: &user.User_Human{
|
||||
Human: &user.HumanUser{
|
||||
Profile: &user.HumanProfile{
|
||||
GivenName: "Mickey",
|
||||
FamilyName: "Mouse",
|
||||
NickName: gu.Ptr("Mickey"),
|
||||
DisplayName: gu.Ptr("Mickey Mouse"),
|
||||
PreferredLanguage: gu.Ptr("nl"),
|
||||
Gender: user.Gender_GENDER_MALE.Enum(),
|
||||
},
|
||||
Email: &user.HumanEmail{
|
||||
IsVerified: true,
|
||||
},
|
||||
Phone: &user.HumanPhone{
|
||||
IsVerified: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// this is the admin of the org craated in Instance.CreateOrganization()
|
||||
nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list user with wrong org query",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListUsersRequest{},
|
||||
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
|
||||
orgRespForOrgTests := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email())
|
||||
orgRespForOrgTests2 := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email())
|
||||
// info := createUser(ctx, orgRespForOrgTests.OrganizationId, false)
|
||||
createUser(ctx, orgRespForOrgTests.OrganizationId, false)
|
||||
request.Queries = []*user.SearchQuery{}
|
||||
request.Queries = append(request.Queries, OrganizationIdQuery(orgRespForOrgTests2.OrganizationId))
|
||||
return []userAttr{{}}
|
||||
},
|
||||
},
|
||||
want: &user.ListUsersResponse{
|
||||
Details: &object_v2beta.ListDetails{
|
||||
TotalResult: 0,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
SortingColumn: 0,
|
||||
Result: []*user.User{
|
||||
// this is the admin of the org craated in Instance.CreateOrganization()
|
||||
nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
infos := tt.args.dep(IamCTX, tt.args.req)
|
||||
for _, f := range permissionCheckV2Settings {
|
||||
f := f
|
||||
for _, tt := range tests {
|
||||
t.Run(f.TestNamePrependString+tt.name, func(t *testing.T) {
|
||||
setPermissionCheckV2Flag(t, f.SetFlag)
|
||||
infos := tt.args.dep(IamCTX, tt.args.req)
|
||||
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute)
|
||||
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||
got, err := Client.ListUsers(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(ttt, err)
|
||||
return
|
||||
}
|
||||
require.NoError(ttt, err)
|
||||
// retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, 10*time.Minute)
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, 20*time.Second)
|
||||
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||
got, err := Client.ListUsers(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(ttt, err)
|
||||
return
|
||||
}
|
||||
require.NoError(ttt, err)
|
||||
|
||||
// always only give back dependency infos which are required for the response
|
||||
require.Len(ttt, tt.want.Result, len(infos))
|
||||
// always first check length, otherwise its failed anyway
|
||||
if assert.Len(ttt, got.Result, len(tt.want.Result)) {
|
||||
// totalResult is unrelated to the tests here so gets carried over, can vary from the count of results due to permissions
|
||||
tt.want.Details.TotalResult = got.Details.TotalResult
|
||||
// always only give back dependency infos which are required for the response
|
||||
require.Len(ttt, tt.want.Result, len(infos))
|
||||
// always first check length, otherwise its failed anyway
|
||||
if assert.Len(ttt, got.Result, len(tt.want.Result)) {
|
||||
// totalResult is unrelated to the tests here so gets carried over, can vary from the count of results due to permissions
|
||||
tt.want.Details.TotalResult = got.Details.TotalResult
|
||||
|
||||
// fill in userid and username as it is generated
|
||||
for i := range infos {
|
||||
tt.want.Result[i].UserId = infos[i].UserID
|
||||
tt.want.Result[i].Username = infos[i].Username
|
||||
tt.want.Result[i].PreferredLoginName = infos[i].Username
|
||||
tt.want.Result[i].LoginNames = []string{infos[i].Username}
|
||||
if human := tt.want.Result[i].GetHuman(); human != nil {
|
||||
human.Email.Email = infos[i].Username
|
||||
human.Phone.Phone = infos[i].Phone
|
||||
if tt.want.Result[i].GetHuman().GetPasswordChanged() != nil {
|
||||
human.PasswordChanged = infos[i].Changed
|
||||
// fill in userid and username as it is generated
|
||||
for i := range infos {
|
||||
if tt.want.Result[i] == nil {
|
||||
continue
|
||||
}
|
||||
tt.want.Result[i].UserId = infos[i].UserID
|
||||
tt.want.Result[i].Username = infos[i].Username
|
||||
tt.want.Result[i].PreferredLoginName = infos[i].Username
|
||||
tt.want.Result[i].LoginNames = []string{infos[i].Username}
|
||||
if human := tt.want.Result[i].GetHuman(); human != nil {
|
||||
human.Email.Email = infos[i].Username
|
||||
human.Phone.Phone = infos[i].Phone
|
||||
if tt.want.Result[i].GetHuman().GetPasswordChanged() != nil {
|
||||
human.PasswordChanged = infos[i].Changed
|
||||
}
|
||||
}
|
||||
tt.want.Result[i].Details = detailsV2ToV2beta(infos[i].Details)
|
||||
}
|
||||
for i := range tt.want.Result {
|
||||
if tt.want.Result[i] == nil {
|
||||
continue
|
||||
}
|
||||
assert.EqualExportedValues(ttt, got.Result[i], tt.want.Result[i])
|
||||
}
|
||||
tt.want.Result[i].Details = detailsV2ToV2beta(infos[i].Details)
|
||||
}
|
||||
for i := range tt.want.Result {
|
||||
assert.EqualExportedValues(ttt, got.Result[i], tt.want.Result[i])
|
||||
}
|
||||
}
|
||||
integration.AssertListDetails(ttt, tt.want, got)
|
||||
}, retryDuration, tick, "timeout waiting for expected user result")
|
||||
})
|
||||
integration.AssertListDetails(ttt, tt.want, got)
|
||||
}, retryDuration, tick, "timeout waiting for expected user result")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func InUserIDsQuery(ids []string) *user.SearchQuery {
|
||||
return &user.SearchQuery{Query: &user.SearchQuery_InUserIdsQuery{
|
||||
InUserIdsQuery: &user.InUserIDQuery{
|
||||
UserIds: ids,
|
||||
return &user.SearchQuery{
|
||||
Query: &user.SearchQuery_InUserIdsQuery{
|
||||
InUserIdsQuery: &user.InUserIDQuery{
|
||||
UserIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func InUserEmailsQuery(emails []string) *user.SearchQuery {
|
||||
return &user.SearchQuery{Query: &user.SearchQuery_InUserEmailsQuery{
|
||||
InUserEmailsQuery: &user.InUserEmailsQuery{
|
||||
UserEmails: emails,
|
||||
return &user.SearchQuery{
|
||||
Query: &user.SearchQuery_InUserEmailsQuery{
|
||||
InUserEmailsQuery: &user.InUserEmailsQuery{
|
||||
UserEmails: emails,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func PhoneQuery(number string) *user.SearchQuery {
|
||||
return &user.SearchQuery{Query: &user.SearchQuery_PhoneQuery{
|
||||
PhoneQuery: &user.PhoneQuery{
|
||||
Number: number,
|
||||
return &user.SearchQuery{
|
||||
Query: &user.SearchQuery_PhoneQuery{
|
||||
PhoneQuery: &user.PhoneQuery{
|
||||
Number: number,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func UsernameQuery(username string) *user.SearchQuery {
|
||||
return &user.SearchQuery{Query: &user.SearchQuery_UserNameQuery{
|
||||
UserNameQuery: &user.UserNameQuery{
|
||||
UserName: username,
|
||||
return &user.SearchQuery{
|
||||
Query: &user.SearchQuery_UserNameQuery{
|
||||
UserNameQuery: &user.UserNameQuery{
|
||||
UserName: username,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func OrganizationIdQuery(resourceowner string) *user.SearchQuery {
|
||||
return &user.SearchQuery{Query: &user.SearchQuery_OrganizationIdQuery{
|
||||
OrganizationIdQuery: &user.OrganizationIdQuery{
|
||||
OrganizationId: resourceowner,
|
||||
return &user.SearchQuery{
|
||||
Query: &user.SearchQuery_OrganizationIdQuery{
|
||||
OrganizationIdQuery: &user.OrganizationIdQuery{
|
||||
OrganizationId: resourceowner,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -16,9 +16,12 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/internal/integration/sink"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/idp"
|
||||
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
@ -2142,15 +2145,19 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
|
||||
idpID := Instance.AddGenericOAuthProvider(t, CTX)
|
||||
intentID := Instance.CreateIntent(t, CTX, idpID)
|
||||
successfulID, token, changeDate, sequence := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID.Id, "", "id")
|
||||
successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID.Id, "user", "id")
|
||||
ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence := Instance.CreateSuccessfulLDAPIntent(t, CTX, idpID.Id, "", "id")
|
||||
ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence := Instance.CreateSuccessfulLDAPIntent(t, CTX, idpID.Id, "user", "id")
|
||||
samlSuccessfulID, samlToken, samlChangeDate, samlSequence := Instance.CreateSuccessfulSAMLIntent(t, CTX, idpID.Id, "", "id")
|
||||
idpID := Instance.AddGenericOAuthProvider(IamCTX, gofakeit.AppName()).GetId()
|
||||
intentID := Instance.CreateIntent(CTX, idpID).GetIdpIntent().GetIdpIntentId()
|
||||
successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", "")
|
||||
require.NoError(t, err)
|
||||
successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", "user")
|
||||
require.NoError(t, err)
|
||||
ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), idpID, "id", "")
|
||||
require.NoError(t, err)
|
||||
ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), idpID, "id", "user")
|
||||
require.NoError(t, err)
|
||||
samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), idpID, "id", "")
|
||||
require.NoError(t, err)
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.RetrieveIdentityProviderIntentRequest
|
||||
@ -2205,7 +2212,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
|
||||
IdToken: gu.Ptr("idToken"),
|
||||
},
|
||||
},
|
||||
IdpId: idpID.Id,
|
||||
IdpId: idpID,
|
||||
UserId: "id",
|
||||
UserName: "username",
|
||||
RawInformation: func() *structpb.Struct {
|
||||
@ -2243,7 +2250,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
|
||||
IdToken: gu.Ptr("idToken"),
|
||||
},
|
||||
},
|
||||
IdpId: idpID.Id,
|
||||
IdpId: idpID,
|
||||
UserId: "id",
|
||||
UserName: "username",
|
||||
RawInformation: func() *structpb.Struct {
|
||||
@ -2287,7 +2294,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
|
||||
}(),
|
||||
},
|
||||
},
|
||||
IdpId: idpID.Id,
|
||||
IdpId: idpID,
|
||||
UserId: "id",
|
||||
UserName: "username",
|
||||
RawInformation: func() *structpb.Struct {
|
||||
@ -2333,7 +2340,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
|
||||
}(),
|
||||
},
|
||||
},
|
||||
IdpId: idpID.Id,
|
||||
IdpId: idpID,
|
||||
UserId: "id",
|
||||
UserName: "username",
|
||||
RawInformation: func() *structpb.Struct {
|
||||
@ -2370,7 +2377,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
|
||||
Assertion: []byte("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"id\" IssueInstant=\"0001-01-01T00:00:00Z\" Version=\"\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" NameQualifier=\"\" SPNameQualifier=\"\" Format=\"\" SPProvidedID=\"\"></Issuer></Assertion>"),
|
||||
},
|
||||
},
|
||||
IdpId: idpID.Id,
|
||||
IdpId: idpID,
|
||||
UserId: "id",
|
||||
UserName: "",
|
||||
RawInformation: func() *structpb.Struct {
|
||||
@ -2401,7 +2408,6 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func TestServer_ListAuthenticationMethodTypes(t *testing.T) {
|
||||
userIDWithoutAuth := Instance.CreateHumanUser(CTX).GetUserId()
|
||||
|
@ -74,6 +74,7 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
CreationDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
ID: "123",
|
||||
@ -90,6 +91,7 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
CreationDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
ID: "123",
|
||||
@ -100,6 +102,10 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
|
||||
want: &user.RegisterPasskeyResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 22,
|
||||
CreationDate: ×tamppb.Timestamp{
|
||||
Seconds: 3000,
|
||||
Nanos: 22,
|
||||
},
|
||||
ChangeDate: ×tamppb.Timestamp{
|
||||
Seconds: 3000,
|
||||
Nanos: 22,
|
||||
@ -150,6 +156,7 @@ func Test_passkeyDetailsToPb(t *testing.T) {
|
||||
details: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
CreationDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
err: nil,
|
||||
@ -157,6 +164,10 @@ func Test_passkeyDetailsToPb(t *testing.T) {
|
||||
want: &user.CreatePasskeyRegistrationLinkResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 22,
|
||||
CreationDate: ×tamppb.Timestamp{
|
||||
Seconds: 3000,
|
||||
Nanos: 22,
|
||||
},
|
||||
ChangeDate: ×tamppb.Timestamp{
|
||||
Seconds: 3000,
|
||||
Nanos: 22,
|
||||
@ -199,6 +210,7 @@ func Test_passkeyCodeDetailsToPb(t *testing.T) {
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
CreationDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
CodeID: "123",
|
||||
@ -209,6 +221,10 @@ func Test_passkeyCodeDetailsToPb(t *testing.T) {
|
||||
want: &user.CreatePasskeyRegistrationLinkResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 22,
|
||||
CreationDate: ×tamppb.Timestamp{
|
||||
Seconds: 3000,
|
||||
Nanos: 22,
|
||||
},
|
||||
ChangeDate: ×tamppb.Timestamp{
|
||||
Seconds: 3000,
|
||||
Nanos: 22,
|
||||
|
@ -43,6 +43,7 @@ func Test_u2fRegistrationDetailsToPb(t *testing.T) {
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
CreationDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
ID: "123",
|
||||
@ -59,6 +60,7 @@ func Test_u2fRegistrationDetailsToPb(t *testing.T) {
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
CreationDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
ID: "123",
|
||||
@ -69,6 +71,10 @@ func Test_u2fRegistrationDetailsToPb(t *testing.T) {
|
||||
want: &user.RegisterU2FResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 22,
|
||||
CreationDate: ×tamppb.Timestamp{
|
||||
Seconds: 3000,
|
||||
Nanos: 22,
|
||||
},
|
||||
ChangeDate: ×tamppb.Timestamp{
|
||||
Seconds: 3000,
|
||||
Nanos: 22,
|
||||
|
@ -371,31 +371,31 @@ func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *user.Star
|
||||
}
|
||||
|
||||
func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*user.StartIdentityProviderIntentResponse, error) {
|
||||
intentWriteModel, details, err := s.command.CreateIntent(ctx, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetInstance(ctx).InstanceID())
|
||||
state, session, err := s.command.AuthFromProvider(ctx, idpID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content, redirect, err := s.command.AuthFromProvider(ctx, idpID, intentWriteModel.AggregateID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID))
|
||||
_, details, err := s.command.CreateIntent(ctx, state, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetInstance(ctx).InstanceID(), session.PersistentParameters())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content, redirect := session.GetAuth(ctx)
|
||||
if redirect {
|
||||
return &user.StartIdentityProviderIntentResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: content},
|
||||
}, nil
|
||||
} else {
|
||||
return &user.StartIdentityProviderIntentResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
NextStep: &user.StartIdentityProviderIntentResponse_PostForm{
|
||||
PostForm: []byte(content),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return &user.StartIdentityProviderIntentResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
NextStep: &user.StartIdentityProviderIntentResponse_PostForm{
|
||||
PostForm: []byte(content),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*user.StartIdentityProviderIntentResponse, error) {
|
||||
intentWriteModel, details, err := s.command.CreateIntent(ctx, idpID, "", "", authz.GetInstance(ctx).InstanceID())
|
||||
intentWriteModel, details, err := s.command.CreateIntent(ctx, "", idpID, "", "", authz.GetInstance(ctx).InstanceID(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -328,7 +328,7 @@ func (h *Handler) handleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
idpUser, idpSession, err := h.fetchIDPUserFromCode(ctx, provider, data.Code, data.User)
|
||||
idpUser, idpSession, err := h.fetchIDPUserFromCode(ctx, provider, data.Code, data.User, intent.IDPArguments)
|
||||
if err != nil {
|
||||
cmdErr := h.commands.FailIDPIntent(ctx, intent, err.Error())
|
||||
logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent")
|
||||
@ -410,23 +410,23 @@ func redirectToFailureURL(w http.ResponseWriter, r *http.Request, i *command.IDP
|
||||
http.Redirect(w, r, i.FailureURL.String(), http.StatusFound)
|
||||
}
|
||||
|
||||
func (h *Handler) fetchIDPUserFromCode(ctx context.Context, identityProvider idp.Provider, code string, appleUser string) (user idp.User, idpTokens idp.Session, err error) {
|
||||
func (h *Handler) fetchIDPUserFromCode(ctx context.Context, identityProvider idp.Provider, code string, appleUser string, idpArguments map[string]any) (user idp.User, idpTokens idp.Session, err error) {
|
||||
var session idp.Session
|
||||
switch provider := identityProvider.(type) {
|
||||
case *oauth.Provider:
|
||||
session = &oauth.Session{Provider: provider, Code: code}
|
||||
session = oauth.NewSession(provider, code, idpArguments)
|
||||
case *openid.Provider:
|
||||
session = &openid.Session{Provider: provider, Code: code}
|
||||
session = openid.NewSession(provider, code, idpArguments)
|
||||
case *azuread.Provider:
|
||||
session = &azuread.Session{Provider: provider, Code: code}
|
||||
session = azuread.NewSession(provider, code)
|
||||
case *github.Provider:
|
||||
session = &oauth.Session{Provider: provider.Provider, Code: code}
|
||||
session = oauth.NewSession(provider.Provider, code, idpArguments)
|
||||
case *gitlab.Provider:
|
||||
session = &openid.Session{Provider: provider.Provider, Code: code}
|
||||
session = openid.NewSession(provider.Provider, code, idpArguments)
|
||||
case *google.Provider:
|
||||
session = &openid.Session{Provider: provider.Provider, Code: code}
|
||||
session = openid.NewSession(provider.Provider, code, idpArguments)
|
||||
case *apple.Provider:
|
||||
session = &apple.Session{Session: &openid.Session{Provider: provider.Provider, Code: code}, UserFormValue: appleUser}
|
||||
session = apple.NewSession(provider, code, appleUser)
|
||||
case *jwt.Provider, *ldap.Provider, *saml2.Provider:
|
||||
return nil, nil, zerrors.ThrowInvalidArgument(nil, "IDP-52jmn", "Errors.ExternalIDP.IDPTypeNotImplemented")
|
||||
default:
|
||||
|
127
internal/api/oidc/integration_test/token_device_test.go
Normal file
127
internal/api/oidc/integration_test/token_device_test.go
Normal file
@ -0,0 +1,127 @@
|
||||
//go:build integration
|
||||
|
||||
package oidc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/app"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/auth"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
|
||||
)
|
||||
|
||||
func TestServer_DeviceAuth(t *testing.T) {
|
||||
project, err := Instance.CreateProject(CTX)
|
||||
require.NoError(t, err)
|
||||
client, err := Instance.CreateOIDCClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
scope []string
|
||||
decision func(t *testing.T, id string)
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "authorized",
|
||||
scope: []string{},
|
||||
decision: func(t *testing.T, id string) {
|
||||
sessionID, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
|
||||
_, err = Instance.Client.OIDCv2.AuthorizeOrDenyDeviceAuthorization(CTXLOGIN, &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
|
||||
DeviceAuthorizationId: id,
|
||||
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionID,
|
||||
SessionToken: sessionToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "authorized, with ZITADEL",
|
||||
scope: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, domain.ProjectScopeZITADEL},
|
||||
decision: func(t *testing.T, id string) {
|
||||
sessionID, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
|
||||
_, err = Instance.Client.OIDCv2.AuthorizeOrDenyDeviceAuthorization(CTXLOGIN, &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
|
||||
DeviceAuthorizationId: id,
|
||||
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionID,
|
||||
SessionToken: sessionToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "denied",
|
||||
scope: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, domain.ProjectScopeZITADEL},
|
||||
decision: func(t *testing.T, id string) {
|
||||
_, err = Instance.Client.OIDCv2.AuthorizeOrDenyDeviceAuthorization(CTXLOGIN, &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
|
||||
DeviceAuthorizationId: id,
|
||||
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Deny{
|
||||
Deny: &oidc_pb.Deny{},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
},
|
||||
wantErr: oidc.ErrAccessDenied(),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
provider, err := rp.NewRelyingPartyOIDC(CTX, Instance.OIDCIssuer(), client.GetClientId(), "", "", tt.scope)
|
||||
require.NoError(t, err)
|
||||
deviceAuthorization, err := rp.DeviceAuthorization(CTX, tt.scope, provider, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
relyingPartyDone := make(chan struct{})
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(CTX, 1*time.Minute)
|
||||
defer func() {
|
||||
cancel()
|
||||
relyingPartyDone <- struct{}{}
|
||||
}()
|
||||
tokens, err := rp.DeviceAccessToken(ctx, deviceAuthorization.DeviceCode, time.Duration(deviceAuthorization.Interval)*time.Second, provider)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
|
||||
if tokens == nil {
|
||||
return
|
||||
}
|
||||
_, err = Instance.Client.Auth.GetMyUser(integration.WithAuthorizationToken(CTX, tokens.AccessToken), &auth.GetMyUserRequest{})
|
||||
if slices.Contains(tt.scope, domain.ProjectScopeZITADEL) {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
}
|
||||
}()
|
||||
|
||||
var req *oidc_pb.GetDeviceAuthorizationRequestResponse
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
|
||||
assert.EventuallyWithT(t, func(collectT *assert.CollectT) {
|
||||
req, err = Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{
|
||||
UserCode: deviceAuthorization.UserCode,
|
||||
})
|
||||
assert.NoError(collectT, err)
|
||||
}, retryDuration, tick)
|
||||
|
||||
tt.decision(t, req.GetDeviceAuthorizationRequest().GetId())
|
||||
|
||||
<-relyingPartyDone
|
||||
})
|
||||
}
|
||||
}
|
@ -42,6 +42,9 @@ func (s *Server) DeviceToken(ctx context.Context, r *op.ClientRequest[oidc.Devic
|
||||
if state == domain.DeviceAuthStateExpired {
|
||||
return nil, oidc.ErrExpiredDeviceCode()
|
||||
}
|
||||
if state == domain.DeviceAuthStateDenied {
|
||||
return nil, oidc.ErrAccessDenied()
|
||||
}
|
||||
}
|
||||
return nil, oidc.ErrAccessDenied().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError)
|
||||
return nil, oidc.ErrInvalidGrant().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError)
|
||||
}
|
||||
|
@ -149,14 +149,7 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
|
||||
err = l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, identityProvider.ID, userAgentID)
|
||||
if err != nil {
|
||||
l.externalAuthFailed(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
var provider idp.Provider
|
||||
|
||||
switch identityProvider.Type {
|
||||
case domain.IDPTypeOAuth:
|
||||
provider, err = l.oauthProvider(r.Context(), identityProvider)
|
||||
@ -199,6 +192,13 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai
|
||||
return
|
||||
}
|
||||
|
||||
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
|
||||
err = l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, identityProvider.ID, userAgentID, session.PersistentParameters())
|
||||
if err != nil {
|
||||
l.externalAuthFailed(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
|
||||
content, redirect := session.GetAuth(r.Context())
|
||||
if redirect {
|
||||
http.Redirect(w, r, content, http.StatusFound)
|
||||
@ -271,79 +271,78 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque
|
||||
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
|
||||
return
|
||||
}
|
||||
var provider idp.Provider
|
||||
var session idp.Session
|
||||
switch identityProvider.Type {
|
||||
case domain.IDPTypeOAuth:
|
||||
provider, err = l.oauthProvider(r.Context(), identityProvider)
|
||||
provider, err := l.oauthProvider(r.Context(), identityProvider)
|
||||
if err != nil {
|
||||
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
|
||||
return
|
||||
}
|
||||
session = &oauth.Session{Provider: provider.(*oauth.Provider), Code: data.Code}
|
||||
session = oauth.NewSession(provider, data.Code, authReq.SelectedIDPConfigArgs)
|
||||
case domain.IDPTypeOIDC:
|
||||
provider, err = l.oidcProvider(r.Context(), identityProvider)
|
||||
provider, err := l.oidcProvider(r.Context(), identityProvider)
|
||||
if err != nil {
|
||||
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
|
||||
return
|
||||
}
|
||||
session = &openid.Session{Provider: provider.(*openid.Provider), Code: data.Code}
|
||||
session = openid.NewSession(provider, data.Code, authReq.SelectedIDPConfigArgs)
|
||||
case domain.IDPTypeAzureAD:
|
||||
provider, err = l.azureProvider(r.Context(), identityProvider)
|
||||
provider, err := l.azureProvider(r.Context(), identityProvider)
|
||||
if err != nil {
|
||||
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
|
||||
return
|
||||
}
|
||||
session = &azuread.Session{Provider: provider.(*azuread.Provider), Code: data.Code}
|
||||
session = azuread.NewSession(provider, data.Code)
|
||||
case domain.IDPTypeGitHub:
|
||||
provider, err = l.githubProvider(r.Context(), identityProvider)
|
||||
provider, err := l.githubProvider(r.Context(), identityProvider)
|
||||
if err != nil {
|
||||
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
|
||||
return
|
||||
}
|
||||
session = &oauth.Session{Provider: provider.(*github.Provider).Provider, Code: data.Code}
|
||||
session = oauth.NewSession(provider.Provider, data.Code, authReq.SelectedIDPConfigArgs)
|
||||
case domain.IDPTypeGitHubEnterprise:
|
||||
provider, err = l.githubEnterpriseProvider(r.Context(), identityProvider)
|
||||
provider, err := l.githubEnterpriseProvider(r.Context(), identityProvider)
|
||||
if err != nil {
|
||||
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
|
||||
return
|
||||
}
|
||||
session = &oauth.Session{Provider: provider.(*github.Provider).Provider, Code: data.Code}
|
||||
session = oauth.NewSession(provider.Provider, data.Code, authReq.SelectedIDPConfigArgs)
|
||||
case domain.IDPTypeGitLab:
|
||||
provider, err = l.gitlabProvider(r.Context(), identityProvider)
|
||||
provider, err := l.gitlabProvider(r.Context(), identityProvider)
|
||||
if err != nil {
|
||||
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
|
||||
return
|
||||
}
|
||||
session = &openid.Session{Provider: provider.(*gitlab.Provider).Provider, Code: data.Code}
|
||||
session = openid.NewSession(provider.Provider, data.Code, authReq.SelectedIDPConfigArgs)
|
||||
case domain.IDPTypeGitLabSelfHosted:
|
||||
provider, err = l.gitlabSelfHostedProvider(r.Context(), identityProvider)
|
||||
provider, err := l.gitlabSelfHostedProvider(r.Context(), identityProvider)
|
||||
if err != nil {
|
||||
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
|
||||
return
|
||||
}
|
||||
session = &openid.Session{Provider: provider.(*gitlab.Provider).Provider, Code: data.Code}
|
||||
session = openid.NewSession(provider.Provider, data.Code, authReq.SelectedIDPConfigArgs)
|
||||
case domain.IDPTypeGoogle:
|
||||
provider, err = l.googleProvider(r.Context(), identityProvider)
|
||||
provider, err := l.googleProvider(r.Context(), identityProvider)
|
||||
if err != nil {
|
||||
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
|
||||
return
|
||||
}
|
||||
session = &openid.Session{Provider: provider.(*google.Provider).Provider, Code: data.Code}
|
||||
session = openid.NewSession(provider.Provider, data.Code, authReq.SelectedIDPConfigArgs)
|
||||
case domain.IDPTypeApple:
|
||||
provider, err = l.appleProvider(r.Context(), identityProvider)
|
||||
provider, err := l.appleProvider(r.Context(), identityProvider)
|
||||
if err != nil {
|
||||
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
|
||||
return
|
||||
}
|
||||
session = &apple.Session{Session: &openid.Session{Provider: provider.(*apple.Provider).Provider, Code: data.Code}, UserFormValue: data.User}
|
||||
session = apple.NewSession(provider, data.Code, data.User)
|
||||
case domain.IDPTypeSAML:
|
||||
provider, err = l.samlProvider(r.Context(), identityProvider)
|
||||
provider, err := l.samlProvider(r.Context(), identityProvider)
|
||||
if err != nil {
|
||||
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
|
||||
return
|
||||
}
|
||||
session, err = saml.NewSession(provider.(*saml.Provider), authReq.SAMLRequestID, r)
|
||||
session, err = saml.NewSession(provider, authReq.SAMLRequestID, r)
|
||||
if err != nil {
|
||||
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
|
||||
return
|
||||
@ -440,7 +439,7 @@ func (l *Login) handleExternalUserAuthenticated(
|
||||
) {
|
||||
externalUser := mapIDPUserToExternalUser(user, provider.ID)
|
||||
// ensure the linked IDP is added to the login policy
|
||||
if err := l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, provider.ID, authReq.AgentID); err != nil {
|
||||
if err := l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, provider.ID, authReq.AgentID, authReq.SelectedIDPConfigArgs); err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
@ -1005,11 +1004,17 @@ func (l *Login) oidcProvider(ctx context.Context, identityProvider *query.IDPTem
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts := make([]openid.ProviderOpts, 1, 2)
|
||||
opts := make([]openid.ProviderOpts, 1, 3)
|
||||
opts[0] = openid.WithSelectAccount()
|
||||
if identityProvider.OIDCIDPTemplate.IsIDTokenMapping {
|
||||
opts = append(opts, openid.WithIDTokenMapping())
|
||||
}
|
||||
|
||||
if identityProvider.OIDCIDPTemplate.UsePKCE {
|
||||
// we do not pass any cookie handler, since we store the verifier internally, rather than in a cookie
|
||||
opts = append(opts, openid.WithRelyingPartyOption(rp.WithPKCE(nil)))
|
||||
}
|
||||
|
||||
return openid.New(identityProvider.Name,
|
||||
identityProvider.OIDCIDPTemplate.Issuer,
|
||||
identityProvider.OIDCIDPTemplate.ClientID,
|
||||
@ -1047,6 +1052,12 @@ func (l *Login) oauthProvider(ctx context.Context, identityProvider *query.IDPTe
|
||||
RedirectURL: l.baseURL(ctx) + EndpointExternalLoginCallback,
|
||||
Scopes: identityProvider.OAuthIDPTemplate.Scopes,
|
||||
}
|
||||
|
||||
opts := make([]oauth.ProviderOpts, 0, 1)
|
||||
if identityProvider.OAuthIDPTemplate.UsePKCE {
|
||||
// we do not pass any cookie handler, since we store the verifier internally, rather than in a cookie
|
||||
opts = append(opts, oauth.WithRelyingPartyOption(rp.WithPKCE(nil)))
|
||||
}
|
||||
return oauth.New(
|
||||
config,
|
||||
identityProvider.Name,
|
||||
@ -1054,6 +1065,7 @@ func (l *Login) oauthProvider(ctx context.Context, identityProvider *query.IDPTe
|
||||
func() idp.User {
|
||||
return oauth.NewUserMapper(identityProvider.OAuthIDPTemplate.IDAttribute)
|
||||
},
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -81,7 +81,7 @@ func (l *Login) handleJWTExtraction(w http.ResponseWriter, r *http.Request, auth
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
session := &jwt.Session{Provider: provider, Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{IDToken: token, Token: &oauth2.Token{}}}
|
||||
session := jwt.NewSession(provider, &oidc.Tokens[*oidc.IDTokenClaims]{IDToken: token, Token: &oauth2.Token{}})
|
||||
user, err := session.FetchUser(r.Context())
|
||||
if err != nil {
|
||||
if _, _, actionErr := l.runPostExternalAuthenticationActions(new(domain.ExternalUser), tokens(session), authReq, r, user, err); actionErr != nil {
|
||||
|
@ -66,7 +66,7 @@ func (l *Login) handleLDAPCallback(w http.ResponseWriter, r *http.Request) {
|
||||
l.renderLDAPLogin(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
session := &ldap.Session{Provider: provider, User: data.Username, Password: data.Password}
|
||||
session := ldap.NewSession(provider, data.Username, data.Password)
|
||||
|
||||
user, err := session.FetchUser(r.Context())
|
||||
if err != nil {
|
||||
|
@ -11,6 +11,13 @@ Login:
|
||||
RegisterButtonText: 登録
|
||||
NextButtonText: 次へ
|
||||
|
||||
LDAP:
|
||||
Title: ようこそ!
|
||||
Description: LDAPのログイン情報を入力してください
|
||||
LoginNameLabel: ログイン名
|
||||
PasswordLabel: パスワード
|
||||
NextButtonText: 次へ
|
||||
|
||||
SelectAccount:
|
||||
Title: アカウントの選択
|
||||
Description: ZITADELアカウントを使用します。
|
||||
@ -198,6 +205,7 @@ PasswordChange:
|
||||
NewPasswordConfirmLabel: 新パスワードの確認
|
||||
CancelButtonText: キャンセル
|
||||
NextButtonText: 次へ
|
||||
Footer: フッター
|
||||
|
||||
PasswordChangeDone:
|
||||
Title: パスワードの変更完了
|
||||
@ -401,6 +409,7 @@ Footer:
|
||||
Tos: TOS
|
||||
PrivacyPolicy: プライバシーポリシー
|
||||
Help: ヘルプ
|
||||
SupportEmail: サポートメール
|
||||
|
||||
SignIn: '{{.Provider}} でサインイン'
|
||||
|
||||
@ -415,6 +424,7 @@ Errors:
|
||||
MissingParameters: 必要なパラメーターが不足しています
|
||||
User:
|
||||
NotFound: ユーザーが見つかりません
|
||||
AlreadyExists: ユーザーは既に存在します
|
||||
Inactive: ユーザーは非アクティブです
|
||||
NotFoundOnOrg: ユーザーは、選択した組織で見つけることができませんでした
|
||||
NotAllowedOrg: ユーザーは必要な組織のメンバーではありません
|
||||
@ -423,6 +433,33 @@ Errors:
|
||||
Invalid: 無効なユーザーデータです
|
||||
DomainNotAllowedAsUsername: このドメインはすでに予約されているため使用できません
|
||||
NotAllowedToLink: このユーザーは外部ログインプロバイダーにリンクすることを許可されていません
|
||||
Profile:
|
||||
NotFound: プロファイルが見つかりません
|
||||
NotChanged: プロファイルが変更されていません
|
||||
Empty: プロファイルが空です
|
||||
FirstNameEmpty: 名前が空です
|
||||
LastNameEmpty: 姓が空です
|
||||
IDMissing: プロファイルIDが不足しています
|
||||
Email:
|
||||
NotFound: メールアドレスが見つかりません
|
||||
Invalid: メールアドレスが無効です
|
||||
AlreadyVerified: メールアドレスはすでに登録されています
|
||||
NotChanged: メールアドレスが変更されていません
|
||||
Empty: メールアドレスが空です
|
||||
IDMissing: メールアドレスIDが不足しています
|
||||
Phone:
|
||||
NotFound: 電話番号が見つかりません
|
||||
Invalid: 電話番号が無効です
|
||||
AlreadyVerified: 電話番号はすでに登録されています
|
||||
Empty: 電話番号が空です
|
||||
NotChanged: 電話番号が変更されていません
|
||||
Address:
|
||||
NotFound: 住所が見つかりません
|
||||
NotChanged: 住所が変更されていません
|
||||
Username:
|
||||
AlreadyExists: ユーザー名はすでに使用されています
|
||||
Reserved: ユーザー名はすでに使用されています
|
||||
Empty: ユーザー名が空です
|
||||
Password:
|
||||
ConfirmationWrong: 確認用パスワードが間違っています
|
||||
Empty: パスワードが空です
|
||||
@ -480,6 +517,9 @@ Errors:
|
||||
IAM:
|
||||
LockoutPolicy:
|
||||
NotExisting: ロックアウトポリシーが存在しません
|
||||
Org:
|
||||
LoginPolicy:
|
||||
RegistrationNotAllowed: 新規登録は許可されていません
|
||||
DeviceAuth:
|
||||
NotExisting: ユーザーコードが存在しません
|
||||
|
||||
|
@ -20,7 +20,7 @@ type AuthRequestRepository interface {
|
||||
SetExternalUserLogin(ctx context.Context, authReqID, userAgentID string, user *domain.ExternalUser) error
|
||||
SetLinkingUser(ctx context.Context, request *domain.AuthRequest, externalUser *domain.ExternalUser) error
|
||||
SelectUser(ctx context.Context, authReqID, userID, userAgentID string) error
|
||||
SelectExternalIDP(ctx context.Context, authReqID, idpConfigID, userAgentID string) error
|
||||
SelectExternalIDP(ctx context.Context, authReqID, idpConfigID, userAgentID string, idpArguments map[string]any) error
|
||||
VerifyPassword(ctx context.Context, id, userID, resourceOwner, password, userAgentID string, info *domain.BrowserInfo) error
|
||||
|
||||
VerifyMFAOTP(ctx context.Context, authRequestID, userID, resourceOwner, code, userAgentID string, info *domain.BrowserInfo) error
|
||||
|
@ -255,14 +255,14 @@ func (repo *AuthRequestRepo) CheckLoginName(ctx context.Context, id, loginName,
|
||||
return repo.AuthRequests.UpdateAuthRequest(ctx, request)
|
||||
}
|
||||
|
||||
func (repo *AuthRequestRepo) SelectExternalIDP(ctx context.Context, authReqID, idpConfigID, userAgentID string) (err error) {
|
||||
func (repo *AuthRequestRepo) SelectExternalIDP(ctx context.Context, authReqID, idpConfigID, userAgentID string, idpArguments map[string]any) (err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
request, err := repo.getAuthRequest(ctx, authReqID, userAgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = repo.checkSelectedExternalIDP(request, idpConfigID)
|
||||
err = repo.checkSelectedExternalIDP(request, idpConfigID, idpArguments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -984,10 +984,11 @@ func queryLoginPolicyToDomain(policy *query.LoginPolicy) *domain.LoginPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *AuthRequestRepo) checkSelectedExternalIDP(request *domain.AuthRequest, idpConfigID string) error {
|
||||
func (repo *AuthRequestRepo) checkSelectedExternalIDP(request *domain.AuthRequest, idpConfigID string, idpArguments map[string]any) error {
|
||||
for _, externalIDP := range request.AllowedExternalIDPs {
|
||||
if externalIDP.IDPConfigID == idpConfigID {
|
||||
request.SelectedIDPConfigID = idpConfigID
|
||||
request.SelectedIDPConfigArgs = idpArguments
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,9 @@ func (c *Commands) ApproveDeviceAuth(
|
||||
if !model.State.Exists() {
|
||||
return nil, zerrors.ThrowNotFound(nil, "COMMAND-Hief9", "Errors.DeviceAuth.NotFound")
|
||||
}
|
||||
if model.State != domain.DeviceAuthStateInitiated {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-GEJL3", "Errors.DeviceAuth.AlreadyHandled")
|
||||
}
|
||||
pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewApprovedEvent(ctx, model.aggregate, userID, userOrgID, authMethods, authTime, preferredLanguage, userAgent, sessionID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -71,6 +74,60 @@ func (c *Commands) ApproveDeviceAuth(
|
||||
return writeModelToObjectDetails(&model.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) ApproveDeviceAuthWithSession(
|
||||
ctx context.Context,
|
||||
deviceCode,
|
||||
sessionID,
|
||||
sessionToken string,
|
||||
) (*domain.ObjectDetails, error) {
|
||||
model, err := c.getDeviceAuthWriteModelByDeviceCode(ctx, deviceCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !model.State.Exists() {
|
||||
return nil, zerrors.ThrowNotFound(nil, "COMMAND-D2hf2", "Errors.DeviceAuth.NotFound")
|
||||
}
|
||||
if model.State != domain.DeviceAuthStateInitiated {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-D30Jf", "Errors.DeviceAuth.AlreadyHandled")
|
||||
}
|
||||
if err := c.checkPermission(ctx, domain.PermissionSessionLink, model.ResourceOwner, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetInstance(ctx).InstanceID())
|
||||
err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = sessionWriteModel.CheckIsActive(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.sessionTokenVerifier(ctx, sessionToken, sessionWriteModel.AggregateID, sessionWriteModel.TokenID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewApprovedEvent(
|
||||
ctx,
|
||||
model.aggregate,
|
||||
sessionWriteModel.UserID,
|
||||
sessionWriteModel.UserResourceOwner,
|
||||
sessionWriteModel.AuthMethodTypes(),
|
||||
sessionWriteModel.AuthenticationTime(),
|
||||
sessionWriteModel.PreferredLanguage,
|
||||
sessionWriteModel.UserAgent,
|
||||
sessionID,
|
||||
))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = AppendAndReduce(model, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return writeModelToObjectDetails(&model.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) CancelDeviceAuth(ctx context.Context, id string, reason domain.DeviceAuthCanceled) (*domain.ObjectDetails, error) {
|
||||
model, err := c.getDeviceAuthWriteModelByDeviceCode(ctx, id)
|
||||
if err != nil {
|
||||
|
@ -82,6 +82,7 @@ func (m *DeviceAuthWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
deviceauth.AddedEventType,
|
||||
deviceauth.ApprovedEventType,
|
||||
deviceauth.CanceledEventType,
|
||||
deviceauth.DoneEventType,
|
||||
).
|
||||
Builder()
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/id/mock"
|
||||
"github.com/zitadel/zitadel/internal/repository/deviceauth"
|
||||
"github.com/zitadel/zitadel/internal/repository/oidcsession"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
@ -265,6 +266,310 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_ApproveDeviceAuthFromSession(t *testing.T) {
|
||||
ctx := authz.WithInstanceID(context.Background(), "instance1")
|
||||
now := time.Now()
|
||||
pushErr := errors.New("pushErr")
|
||||
|
||||
type fields struct {
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
tokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error)
|
||||
checkPermission domain.PermissionCheck
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
deviceCode string
|
||||
sessionID string
|
||||
sessionToken string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantDetails *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "not found error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx,
|
||||
"notfound",
|
||||
"sessionID",
|
||||
"sessionToken",
|
||||
},
|
||||
wantErr: zerrors.ThrowNotFound(nil, "COMMAND-D2hf2", "Errors.DeviceAuth.NotFound"),
|
||||
},
|
||||
{
|
||||
name: "not initialized, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("deviceCode", "instance1"),
|
||||
"client_id", "deviceCode", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
[]string{"projectID", "clientID"}, true,
|
||||
),
|
||||
),
|
||||
eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewCanceledEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("deviceCode", "instance1"),
|
||||
domain.DeviceAuthCanceledDenied,
|
||||
)),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx,
|
||||
"deviceCode",
|
||||
"sessionID",
|
||||
"sessionToken",
|
||||
},
|
||||
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-D30Jf", "Errors.DeviceAuth.AlreadyHandled"),
|
||||
},
|
||||
{
|
||||
name: "missing permission, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("deviceCode", "instance1"),
|
||||
"client_id", "deviceCode", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
[]string{"projectID", "clientID"}, true,
|
||||
),
|
||||
)),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx,
|
||||
"deviceCode",
|
||||
"sessionID",
|
||||
"sessionToken",
|
||||
},
|
||||
wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
|
||||
},
|
||||
{
|
||||
name: "session not active, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("deviceCode", "instance1"),
|
||||
"client_id", "deviceCode", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
[]string{"projectID", "clientID"}, true,
|
||||
),
|
||||
)),
|
||||
expectFilter(),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx,
|
||||
"deviceCode",
|
||||
"sessionID",
|
||||
"sessionToken",
|
||||
},
|
||||
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Flk38", "Errors.Session.NotExisting"),
|
||||
},
|
||||
{
|
||||
name: "invalid session token, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("deviceCode", "instance1"),
|
||||
"client_id", "deviceCode", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
[]string{"projectID", "clientID"}, true,
|
||||
),
|
||||
)),
|
||||
expectFilter(eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
session.NewAddedEvent(ctx,
|
||||
&session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
&domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
)),
|
||||
)),
|
||||
tokenVerifier: newMockTokenVerifierInvalid(),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx,
|
||||
"deviceCode",
|
||||
"sessionID",
|
||||
"invalidToken",
|
||||
},
|
||||
wantErr: zerrors.ThrowPermissionDenied(nil, "COMMAND-sGr42", "Errors.Session.Token.Invalid"),
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("deviceCode", "instance1"),
|
||||
"client_id", "deviceCode", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
[]string{"projectID", "clientID"}, true,
|
||||
),
|
||||
)),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(ctx,
|
||||
&session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
&domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
session.NewUserCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
"userID", "orgID", testNow, &language.Afrikaans),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
session.NewPasswordCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
testNow),
|
||||
),
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
session.NewLifetimeSetEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
2*time.Minute),
|
||||
),
|
||||
),
|
||||
expectPushFailed(pushErr,
|
||||
deviceauth.NewApprovedEvent(
|
||||
ctx, deviceauth.NewAggregate("deviceCode", "instance1"), "userID", "orgID",
|
||||
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||
testNow, &language.Afrikaans, &domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
"sessionID",
|
||||
),
|
||||
),
|
||||
),
|
||||
tokenVerifier: newMockTokenVerifierValid(),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx,
|
||||
"deviceCode",
|
||||
"sessionID",
|
||||
"sessionToken",
|
||||
},
|
||||
wantErr: pushErr,
|
||||
},
|
||||
{
|
||||
name: "authorized",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("deviceCode", "instance1"),
|
||||
"client_id", "deviceCode", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
[]string{"projectID", "clientID"}, true,
|
||||
),
|
||||
)),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(ctx,
|
||||
&session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
&domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
session.NewUserCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
"userID", "orgID", testNow, &language.Afrikaans),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
session.NewPasswordCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
testNow),
|
||||
),
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
session.NewLifetimeSetEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
2*time.Minute),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
deviceauth.NewApprovedEvent(
|
||||
ctx, deviceauth.NewAggregate("deviceCode", "instance1"), "userID", "orgID",
|
||||
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||
testNow, &language.Afrikaans, &domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
"sessionID",
|
||||
),
|
||||
),
|
||||
),
|
||||
tokenVerifier: newMockTokenVerifierValid(),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx,
|
||||
"deviceCode",
|
||||
"sessionID",
|
||||
"sessionToken",
|
||||
},
|
||||
wantDetails: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
sessionTokenVerifier: tt.fields.tokenVerifier,
|
||||
checkPermission: tt.fields.checkPermission,
|
||||
}
|
||||
gotDetails, err := c.ApproveDeviceAuthWithSession(tt.args.ctx, tt.args.deviceCode, tt.args.sessionID, tt.args.sessionToken)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assertObjectDetails(t, tt.wantDetails, gotDetails)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_CancelDeviceAuth(t *testing.T) {
|
||||
ctx := authz.WithInstanceID(context.Background(), "instance1")
|
||||
now := time.Now()
|
||||
|
@ -20,6 +20,7 @@ type GenericOAuthProvider struct {
|
||||
UserEndpoint string
|
||||
Scopes []string
|
||||
IDAttribute string
|
||||
UsePKCE bool
|
||||
IDPOptions idp.Options
|
||||
}
|
||||
|
||||
@ -30,6 +31,7 @@ type GenericOIDCProvider struct {
|
||||
ClientSecret string
|
||||
Scopes []string
|
||||
IsIDTokenMapping bool
|
||||
UsePKCE bool
|
||||
IDPOptions idp.Options
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func (c *Commands) prepareCreateIntent(writeModel *IDPIntentWriteModel, idpID, successURL, failureURL string) preparation.Validation {
|
||||
func (c *Commands) prepareCreateIntent(writeModel *IDPIntentWriteModel, idpID, successURL, failureURL string, idpArguments map[string]any) preparation.Validation {
|
||||
return func() (_ preparation.CreateCommands, err error) {
|
||||
if idpID == "" {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-x8j2bk", "Errors.Intent.IDPMissing")
|
||||
@ -53,24 +53,25 @@ func (c *Commands) prepareCreateIntent(writeModel *IDPIntentWriteModel, idpID, s
|
||||
successURL,
|
||||
failureURL,
|
||||
idpID,
|
||||
idpArguments,
|
||||
),
|
||||
}, nil
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Commands) CreateIntent(ctx context.Context, idpID, successURL, failureURL, resourceOwner string) (*IDPIntentWriteModel, *domain.ObjectDetails, error) {
|
||||
id, err := c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
writeModel := NewIDPIntentWriteModel(id, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
func (c *Commands) CreateIntent(ctx context.Context, intentID, idpID, successURL, failureURL, resourceOwner string, idpArguments map[string]any) (*IDPIntentWriteModel, *domain.ObjectDetails, error) {
|
||||
if intentID == "" {
|
||||
var err error
|
||||
intentID, err = c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
writeModel := NewIDPIntentWriteModel(intentID, resourceOwner)
|
||||
|
||||
//nolint: staticcheck
|
||||
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareCreateIntent(writeModel, idpID, successURL, failureURL))
|
||||
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareCreateIntent(writeModel, idpID, successURL, failureURL, idpArguments))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@ -132,18 +133,17 @@ func (c *Commands) GetActiveIntent(ctx context.Context, intentID string) (*IDPIn
|
||||
return intent, nil
|
||||
}
|
||||
|
||||
func (c *Commands) AuthFromProvider(ctx context.Context, idpID, state string, idpCallback, samlRootURL string) (string, bool, error) {
|
||||
func (c *Commands) AuthFromProvider(ctx context.Context, idpID, idpCallback, samlRootURL string) (state string, session idp.Session, err error) {
|
||||
state, err = c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
provider, err := c.GetProvider(ctx, idpID, idpCallback, samlRootURL)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
return "", nil, err
|
||||
}
|
||||
session, err := provider.BeginAuth(ctx, state)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
content, redirect := session.GetAuth(ctx)
|
||||
return content, redirect, nil
|
||||
session, err = provider.BeginAuth(ctx, state)
|
||||
return state, session, err
|
||||
}
|
||||
|
||||
func getIDPIntentWriteModel(ctx context.Context, writeModel *IDPIntentWriteModel, filter preparation.FilterToQueryReducer) error {
|
||||
|
@ -12,13 +12,14 @@ import (
|
||||
type IDPIntentWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
SuccessURL *url.URL
|
||||
FailureURL *url.URL
|
||||
IDPID string
|
||||
IDPUser []byte
|
||||
IDPUserID string
|
||||
IDPUserName string
|
||||
UserID string
|
||||
SuccessURL *url.URL
|
||||
FailureURL *url.URL
|
||||
IDPID string
|
||||
IDPArguments map[string]any
|
||||
IDPUser []byte
|
||||
IDPUserID string
|
||||
IDPUserName string
|
||||
UserID string
|
||||
|
||||
IDPAccessToken *crypto.CryptoValue
|
||||
IDPIDToken string
|
||||
@ -81,6 +82,7 @@ func (wm *IDPIntentWriteModel) reduceStartedEvent(e *idpintent.StartedEvent) {
|
||||
wm.SuccessURL = e.SuccessURL
|
||||
wm.FailureURL = e.FailureURL
|
||||
wm.IDPID = e.IDPID
|
||||
wm.IDPArguments = e.IDPArguments
|
||||
wm.State = domain.IDPIntentStateStarted
|
||||
}
|
||||
|
||||
|
@ -39,11 +39,13 @@ func TestCommands_CreateIntent(t *testing.T) {
|
||||
idGenerator id.Generator
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
idpID string
|
||||
successURL string
|
||||
failureURL string
|
||||
instanceID string
|
||||
ctx context.Context
|
||||
intentID string
|
||||
idpID string
|
||||
successURL string
|
||||
failureURL string
|
||||
instanceID string
|
||||
idpArguments map[string]any
|
||||
}
|
||||
type res struct {
|
||||
intentID string
|
||||
@ -182,6 +184,7 @@ func TestCommands_CreateIntent(t *testing.T) {
|
||||
"user",
|
||||
"idAttribute",
|
||||
nil,
|
||||
true,
|
||||
rep_idp.Options{},
|
||||
)),
|
||||
),
|
||||
@ -195,6 +198,7 @@ func TestCommands_CreateIntent(t *testing.T) {
|
||||
success,
|
||||
failure,
|
||||
"idp",
|
||||
nil,
|
||||
)
|
||||
}(),
|
||||
),
|
||||
@ -235,6 +239,7 @@ func TestCommands_CreateIntent(t *testing.T) {
|
||||
"user",
|
||||
"idAttribute",
|
||||
nil,
|
||||
true,
|
||||
rep_idp.Options{},
|
||||
)),
|
||||
),
|
||||
@ -248,6 +253,9 @@ func TestCommands_CreateIntent(t *testing.T) {
|
||||
success,
|
||||
failure,
|
||||
"idp",
|
||||
map[string]interface{}{
|
||||
"verifier": "pkceOAuthVerifier",
|
||||
},
|
||||
)
|
||||
}(),
|
||||
),
|
||||
@ -260,6 +268,9 @@ func TestCommands_CreateIntent(t *testing.T) {
|
||||
idpID: "idp",
|
||||
successURL: "https://success.url",
|
||||
failureURL: "https://failure.url",
|
||||
idpArguments: map[string]interface{}{
|
||||
"verifier": "pkceOAuthVerifier",
|
||||
},
|
||||
},
|
||||
res{
|
||||
intentID: "id",
|
||||
@ -288,6 +299,7 @@ func TestCommands_CreateIntent(t *testing.T) {
|
||||
"user",
|
||||
"idAttribute",
|
||||
nil,
|
||||
true,
|
||||
rep_idp.Options{},
|
||||
)),
|
||||
),
|
||||
@ -301,6 +313,9 @@ func TestCommands_CreateIntent(t *testing.T) {
|
||||
success,
|
||||
failure,
|
||||
"idp",
|
||||
map[string]interface{}{
|
||||
"verifier": "pkceOAuthVerifier",
|
||||
},
|
||||
)
|
||||
}(),
|
||||
),
|
||||
@ -313,6 +328,69 @@ func TestCommands_CreateIntent(t *testing.T) {
|
||||
idpID: "idp",
|
||||
successURL: "https://success.url",
|
||||
failureURL: "https://failure.url",
|
||||
idpArguments: map[string]interface{}{
|
||||
"verifier": "pkceOAuthVerifier",
|
||||
},
|
||||
},
|
||||
res{
|
||||
intentID: "id",
|
||||
details: &domain.ObjectDetails{ResourceOwner: "instance"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"push, with id",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate,
|
||||
"idp",
|
||||
"name",
|
||||
"clientID",
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("clientSecret"),
|
||||
},
|
||||
"auth",
|
||||
"token",
|
||||
"user",
|
||||
"idAttribute",
|
||||
nil,
|
||||
true,
|
||||
rep_idp.Options{},
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
func() eventstore.Command {
|
||||
success, _ := url.Parse("https://success.url")
|
||||
failure, _ := url.Parse("https://failure.url")
|
||||
return idpintent.NewStartedEvent(
|
||||
context.Background(),
|
||||
&idpintent.NewAggregate("id", "instance").Aggregate,
|
||||
success,
|
||||
failure,
|
||||
"idp",
|
||||
map[string]interface{}{
|
||||
"verifier": "pkceOAuthVerifier",
|
||||
},
|
||||
)
|
||||
}(),
|
||||
),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
instanceID: "instance",
|
||||
intentID: "id",
|
||||
idpID: "idp",
|
||||
successURL: "https://success.url",
|
||||
failureURL: "https://failure.url",
|
||||
idpArguments: map[string]interface{}{
|
||||
"verifier": "pkceOAuthVerifier",
|
||||
},
|
||||
},
|
||||
res{
|
||||
intentID: "id",
|
||||
@ -326,7 +404,7 @@ func TestCommands_CreateIntent(t *testing.T) {
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
}
|
||||
intentWriteModel, details, err := c.CreateIntent(tt.args.ctx, tt.args.idpID, tt.args.successURL, tt.args.failureURL, tt.args.instanceID)
|
||||
intentWriteModel, details, err := c.CreateIntent(tt.args.ctx, tt.args.intentID, tt.args.idpID, tt.args.successURL, tt.args.failureURL, tt.args.instanceID, tt.args.idpArguments)
|
||||
require.ErrorIs(t, err, tt.res.err)
|
||||
if intentWriteModel != nil {
|
||||
assert.Equal(t, tt.res.intentID, intentWriteModel.AggregateID)
|
||||
@ -342,11 +420,11 @@ func TestCommands_AuthFromProvider(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
secretCrypto crypto.EncryptionAlgorithm
|
||||
idGenerator id.Generator
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
idpID string
|
||||
state string
|
||||
callbackURL string
|
||||
samlRootURL string
|
||||
}
|
||||
@ -361,6 +439,22 @@ func TestCommands_AuthFromProvider(t *testing.T) {
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"error no id generator",
|
||||
fields{
|
||||
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
eventstore: expectEventstore(),
|
||||
idGenerator: mock.NewIDGeneratorExpectError(t, zerrors.ThrowInternal(nil, "", "error id")),
|
||||
},
|
||||
args{
|
||||
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
||||
idpID: "idp",
|
||||
callbackURL: "url",
|
||||
},
|
||||
res{
|
||||
err: zerrors.ThrowInternal(nil, "", "error id"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"idp not existing",
|
||||
fields{
|
||||
@ -368,11 +462,11 @@ func TestCommands_AuthFromProvider(t *testing.T) {
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
idGenerator: mock.ExpectID(t, "id"),
|
||||
},
|
||||
args{
|
||||
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
||||
idpID: "idp",
|
||||
state: "state",
|
||||
callbackURL: "url",
|
||||
},
|
||||
res{
|
||||
@ -402,6 +496,7 @@ func TestCommands_AuthFromProvider(t *testing.T) {
|
||||
"user",
|
||||
"idAttribute",
|
||||
nil,
|
||||
true,
|
||||
rep_idp.Options{},
|
||||
)),
|
||||
eventFromEventPusherWithInstanceID(
|
||||
@ -412,11 +507,11 @@ func TestCommands_AuthFromProvider(t *testing.T) {
|
||||
),
|
||||
),
|
||||
),
|
||||
idGenerator: mock.ExpectID(t, "id"),
|
||||
},
|
||||
args{
|
||||
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
||||
idpID: "idp",
|
||||
state: "state",
|
||||
callbackURL: "url",
|
||||
},
|
||||
res{
|
||||
@ -446,6 +541,7 @@ func TestCommands_AuthFromProvider(t *testing.T) {
|
||||
"user",
|
||||
"idAttribute",
|
||||
nil,
|
||||
true,
|
||||
rep_idp.Options{},
|
||||
)),
|
||||
),
|
||||
@ -467,19 +563,20 @@ func TestCommands_AuthFromProvider(t *testing.T) {
|
||||
"user",
|
||||
"idAttribute",
|
||||
nil,
|
||||
true,
|
||||
rep_idp.Options{},
|
||||
)),
|
||||
),
|
||||
),
|
||||
idGenerator: mock.ExpectID(t, "id"),
|
||||
},
|
||||
args{
|
||||
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
||||
idpID: "idp",
|
||||
state: "state",
|
||||
callbackURL: "url",
|
||||
},
|
||||
res{
|
||||
content: "auth?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&state=state",
|
||||
content: "auth?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&state=id",
|
||||
redirect: true,
|
||||
},
|
||||
},
|
||||
@ -504,6 +601,7 @@ func TestCommands_AuthFromProvider(t *testing.T) {
|
||||
},
|
||||
[]string{"openid", "profile", "User.Read"},
|
||||
false,
|
||||
true,
|
||||
rep_idp.Options{},
|
||||
)),
|
||||
eventFromEventPusherWithInstanceID(
|
||||
@ -540,6 +638,7 @@ func TestCommands_AuthFromProvider(t *testing.T) {
|
||||
},
|
||||
[]string{"openid", "profile", "User.Read"},
|
||||
false,
|
||||
true,
|
||||
rep_idp.Options{},
|
||||
)),
|
||||
eventFromEventPusherWithInstanceID(
|
||||
@ -561,15 +660,15 @@ func TestCommands_AuthFromProvider(t *testing.T) {
|
||||
)),
|
||||
),
|
||||
),
|
||||
idGenerator: mock.ExpectID(t, "id"),
|
||||
},
|
||||
args{
|
||||
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
||||
idpID: "idp",
|
||||
state: "state",
|
||||
callbackURL: "url",
|
||||
},
|
||||
res{
|
||||
content: "https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&scope=openid+profile+User.Read&state=state",
|
||||
content: "https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&scope=openid+profile+User.Read&state=id",
|
||||
redirect: true,
|
||||
},
|
||||
},
|
||||
@ -579,9 +678,16 @@ func TestCommands_AuthFromProvider(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
idpConfigEncryption: tt.fields.secretCrypto,
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
}
|
||||
content, redirect, err := c.AuthFromProvider(tt.args.ctx, tt.args.idpID, tt.args.state, tt.args.callbackURL, tt.args.samlRootURL)
|
||||
_, session, err := c.AuthFromProvider(tt.args.ctx, tt.args.idpID, tt.args.callbackURL, tt.args.samlRootURL)
|
||||
require.ErrorIs(t, err, tt.res.err)
|
||||
|
||||
var content string
|
||||
var redirect bool
|
||||
if err == nil {
|
||||
content, redirect = session.GetAuth(tt.args.ctx)
|
||||
}
|
||||
assert.Equal(t, tt.res.redirect, redirect)
|
||||
assert.Equal(t, tt.res.content, content)
|
||||
})
|
||||
@ -592,11 +698,11 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
secretCrypto crypto.EncryptionAlgorithm
|
||||
idGenerator id.Generator
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
idpID string
|
||||
state string
|
||||
callbackURL string
|
||||
samlRootURL string
|
||||
}
|
||||
@ -669,6 +775,7 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) {
|
||||
success,
|
||||
failure,
|
||||
"idp",
|
||||
nil,
|
||||
)
|
||||
}(),
|
||||
),
|
||||
@ -683,11 +790,11 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) {
|
||||
},
|
||||
),
|
||||
),
|
||||
idGenerator: mock.ExpectID(t, "id"),
|
||||
},
|
||||
args{
|
||||
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
||||
idpID: "idp",
|
||||
state: "id",
|
||||
callbackURL: "url",
|
||||
samlRootURL: "samlurl",
|
||||
},
|
||||
@ -705,10 +812,12 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
idpConfigEncryption: tt.fields.secretCrypto,
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
}
|
||||
content, _, err := c.AuthFromProvider(tt.args.ctx, tt.args.idpID, tt.args.state, tt.args.callbackURL, tt.args.samlRootURL)
|
||||
_, session, err := c.AuthFromProvider(tt.args.ctx, tt.args.idpID, tt.args.callbackURL, tt.args.samlRootURL)
|
||||
require.ErrorIs(t, err, tt.res.err)
|
||||
|
||||
content, _ := session.GetAuth(tt.args.ctx)
|
||||
authURL, err := url.Parse(content)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user