diff --git a/.github/workflows/test-code.yml b/.github/workflows/test-code.yml index 7c1e51df71..273e22105e 100644 --- a/.github/workflows/test-code.yml +++ b/.github/workflows/test-code.yml @@ -42,7 +42,7 @@ jobs: - name: Build Docker Image run: docker build -t zitadel:pr --file build/Dockerfile .artifacts/zitadel - name: Run E2E Tests - run: docker compose run e2e + run: docker compose run e2e --browser chrome working-directory: e2e env: ZITADEL_IMAGE: zitadel:pr diff --git a/console/src/app/app.component.ts b/console/src/app/app.component.ts index 6ebff5402f..9c7ea3da7c 100644 --- a/console/src/app/app.component.ts +++ b/console/src/app/app.component.ts @@ -265,7 +265,7 @@ export class AppComponent implements OnDestroy { public changedOrg(org: Org.AsObject): void { this.themeService.loadPrivateLabelling(); - this.authService.zitadelPermissionsChanged.pipe(take(1)).subscribe(() => { + this.authService.zitadelPermissions$.pipe(take(1)).subscribe(() => { this.router.navigate(['/org'], { fragment: org.id }); }); } diff --git a/console/src/app/guards/role.guard.ts b/console/src/app/guards/role.guard.ts index 29eafa4dfb..24a9cf8da7 100644 --- a/console/src/app/guards/role.guard.ts +++ b/console/src/app/guards/role.guard.ts @@ -12,8 +12,6 @@ export class RoleGuard implements CanActivate { constructor(private authService: GrpcAuthService) {} public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.authService.fetchedZitadelPermissions - .pipe(filter((permissionsFetched) => !!permissionsFetched)) - .pipe(switchMap((_) => this.authService.isAllowed(route.data['roles'], route.data['requiresAll']))); + return this.authService.isAllowed(route.data['roles'], route.data['requiresAll']); } } diff --git a/console/src/app/modules/org-table/org-table.component.ts b/console/src/app/modules/org-table/org-table.component.ts index c61a6260b4..6a16a29046 100644 --- a/console/src/app/modules/org-table/org-table.component.ts +++ b/console/src/app/modules/org-table/org-table.component.ts @@ -97,7 +97,7 @@ export class OrgTableComponent { public selectOrg(item: Org.AsObject, event?: any): void { this.authService.setActiveOrg(item); this.themeService.loadPrivateLabelling(); - this.authService.zitadelPermissionsChanged.pipe(take(1)).subscribe(() => { + this.authService.zitadelPermissions$.pipe(take(1)).subscribe(() => { this.router.navigate(['/org'], { fragment: item.id }); }); } @@ -146,7 +146,7 @@ export class OrgTableComponent { public setAndNavigateToOrg(org: Org.AsObject): void { this.authService.setActiveOrg(org); this.themeService.loadPrivateLabelling(); - this.authService.zitadelPermissionsChanged.pipe(take(1)).subscribe(() => { + this.authService.zitadelPermissions$.pipe(take(1)).subscribe(() => { this.router.navigate(['/org'], { fragment: org.id }); }); } diff --git a/console/src/app/pages/users/user-list/user-table/user-table.component.html b/console/src/app/pages/users/user-list/user-table/user-table.component.html index 0a9ad7a412..57d4c33fcf 100644 --- a/console/src/app/pages/users/user-list/user-table/user-table.component.html +++ b/console/src/app/pages/users/user-list/user-table/user-table.component.html @@ -52,12 +52,12 @@ (filterOpen)="filterOpen = $event" > add diff --git a/console/src/app/services/grpc-auth.service.ts b/console/src/app/services/grpc-auth.service.ts index 1ed212e900..09c86c84f7 100644 --- a/console/src/app/services/grpc-auth.service.ts +++ b/console/src/app/services/grpc-auth.service.ts @@ -2,91 +2,102 @@ import { Injectable } from '@angular/core'; import { SortDirection } from '@angular/material/sort'; import { OAuthService } from 'angular-oauth2-oidc'; import { BehaviorSubject, from, merge, Observable, of, Subject } from 'rxjs'; -import { catchError, filter, finalize, map, mergeMap, switchMap, take, timeout } from 'rxjs/operators'; +import { + catchError, + distinctUntilChanged, + filter, + finalize, + map, + mergeMap, + switchMap, + take, + timeout, + withLatestFrom, +} from 'rxjs/operators'; import { - AddMyAuthFactorOTPRequest, - AddMyAuthFactorOTPResponse, - AddMyAuthFactorU2FRequest, - AddMyAuthFactorU2FResponse, - AddMyPasswordlessLinkRequest, - AddMyPasswordlessLinkResponse, - AddMyPasswordlessRequest, - AddMyPasswordlessResponse, - GetMyEmailRequest, - GetMyEmailResponse, - GetMyLabelPolicyRequest, - GetMyLabelPolicyResponse, - GetMyPasswordComplexityPolicyRequest, - GetMyPasswordComplexityPolicyResponse, - GetMyPhoneRequest, - GetMyPhoneResponse, - GetMyPrivacyPolicyRequest, - GetMyPrivacyPolicyResponse, - GetMyProfileRequest, - GetMyProfileResponse, - GetMyUserRequest, - GetMyUserResponse, - GetSupportedLanguagesRequest, - GetSupportedLanguagesResponse, - ListMyAuthFactorsRequest, - ListMyAuthFactorsResponse, - ListMyLinkedIDPsRequest, - ListMyLinkedIDPsResponse, - ListMyMembershipsRequest, - ListMyMembershipsResponse, - ListMyMetadataRequest, - ListMyMetadataResponse, - ListMyPasswordlessRequest, - ListMyPasswordlessResponse, - ListMyProjectOrgsRequest, - ListMyProjectOrgsResponse, - ListMyUserChangesRequest, - ListMyUserChangesResponse, - ListMyUserGrantsRequest, - ListMyUserGrantsResponse, - ListMyUserSessionsRequest, - ListMyUserSessionsResponse, - ListMyZitadelPermissionsRequest, - ListMyZitadelPermissionsResponse, - RemoveMyAuthFactorOTPRequest, - RemoveMyAuthFactorOTPResponse, - RemoveMyAuthFactorU2FRequest, - RemoveMyAuthFactorU2FResponse, - RemoveMyAvatarRequest, - RemoveMyAvatarResponse, - RemoveMyLinkedIDPRequest, - RemoveMyLinkedIDPResponse, - RemoveMyPasswordlessRequest, - RemoveMyPasswordlessResponse, - RemoveMyPhoneRequest, - RemoveMyPhoneResponse, - RemoveMyUserRequest, - RemoveMyUserResponse, - ResendMyEmailVerificationRequest, - ResendMyEmailVerificationResponse, - ResendMyPhoneVerificationRequest, - ResendMyPhoneVerificationResponse, - SendMyPasswordlessLinkRequest, - SendMyPasswordlessLinkResponse, - SetMyEmailRequest, - SetMyEmailResponse, - SetMyPhoneRequest, - SetMyPhoneResponse, - UpdateMyPasswordRequest, - UpdateMyPasswordResponse, - UpdateMyProfileRequest, - UpdateMyProfileResponse, - UpdateMyUserNameRequest, - UpdateMyUserNameResponse, - VerifyMyAuthFactorOTPRequest, - VerifyMyAuthFactorOTPResponse, - VerifyMyAuthFactorU2FRequest, - VerifyMyAuthFactorU2FResponse, - VerifyMyPasswordlessRequest, - VerifyMyPasswordlessResponse, - VerifyMyPhoneRequest, - VerifyMyPhoneResponse, + AddMyAuthFactorOTPRequest, + AddMyAuthFactorOTPResponse, + AddMyAuthFactorU2FRequest, + AddMyAuthFactorU2FResponse, + AddMyPasswordlessLinkRequest, + AddMyPasswordlessLinkResponse, + AddMyPasswordlessRequest, + AddMyPasswordlessResponse, + GetMyEmailRequest, + GetMyEmailResponse, + GetMyLabelPolicyRequest, + GetMyLabelPolicyResponse, + GetMyPasswordComplexityPolicyRequest, + GetMyPasswordComplexityPolicyResponse, + GetMyPhoneRequest, + GetMyPhoneResponse, + GetMyPrivacyPolicyRequest, + GetMyPrivacyPolicyResponse, + GetMyProfileRequest, + GetMyProfileResponse, + GetMyUserRequest, + GetMyUserResponse, + GetSupportedLanguagesRequest, + GetSupportedLanguagesResponse, + ListMyAuthFactorsRequest, + ListMyAuthFactorsResponse, + ListMyLinkedIDPsRequest, + ListMyLinkedIDPsResponse, + ListMyMembershipsRequest, + ListMyMembershipsResponse, + ListMyMetadataRequest, + ListMyMetadataResponse, + ListMyPasswordlessRequest, + ListMyPasswordlessResponse, + ListMyProjectOrgsRequest, + ListMyProjectOrgsResponse, + ListMyUserChangesRequest, + ListMyUserChangesResponse, + ListMyUserGrantsRequest, + ListMyUserGrantsResponse, + ListMyUserSessionsRequest, + ListMyUserSessionsResponse, + ListMyZitadelPermissionsRequest, + ListMyZitadelPermissionsResponse, + RemoveMyAuthFactorOTPRequest, + RemoveMyAuthFactorOTPResponse, + RemoveMyAuthFactorU2FRequest, + RemoveMyAuthFactorU2FResponse, + RemoveMyAvatarRequest, + RemoveMyAvatarResponse, + RemoveMyLinkedIDPRequest, + RemoveMyLinkedIDPResponse, + RemoveMyPasswordlessRequest, + RemoveMyPasswordlessResponse, + RemoveMyPhoneRequest, + RemoveMyPhoneResponse, + RemoveMyUserRequest, + RemoveMyUserResponse, + ResendMyEmailVerificationRequest, + ResendMyEmailVerificationResponse, + ResendMyPhoneVerificationRequest, + ResendMyPhoneVerificationResponse, + SendMyPasswordlessLinkRequest, + SendMyPasswordlessLinkResponse, + SetMyEmailRequest, + SetMyEmailResponse, + SetMyPhoneRequest, + SetMyPhoneResponse, + UpdateMyPasswordRequest, + UpdateMyPasswordResponse, + UpdateMyProfileRequest, + UpdateMyProfileResponse, + UpdateMyUserNameRequest, + UpdateMyUserNameResponse, + VerifyMyAuthFactorOTPRequest, + VerifyMyAuthFactorOTPResponse, + VerifyMyAuthFactorU2FRequest, + VerifyMyAuthFactorU2FResponse, + VerifyMyPasswordlessRequest, + VerifyMyPasswordlessResponse, + VerifyMyPhoneRequest, + VerifyMyPhoneResponse, } from '../proto/generated/zitadel/auth_pb'; import { ChangeQuery } from '../proto/generated/zitadel/change_pb'; import { MetadataQuery } from '../proto/generated/zitadel/metadata_pb'; @@ -103,9 +114,26 @@ export class GrpcAuthService { private _activeOrgChanged: Subject = new Subject(); public user!: Observable; public userSubject: BehaviorSubject = new BehaviorSubject(undefined); - private zitadelPermissions: BehaviorSubject = new BehaviorSubject(['user.resourceowner']); - - public readonly fetchedZitadelPermissions: BehaviorSubject = new BehaviorSubject(false as boolean); + private triggerPermissionsRefresh: Subject = new Subject(); + public zitadelPermissions$: Observable = this.triggerPermissionsRefresh.pipe( + switchMap(() => + from(this.listMyZitadelPermissions()).pipe( + map((rolesResp) => rolesResp.resultList), + filter((roles) => !!roles.length), + catchError((_) => { + return of([]); + }), + distinctUntilChanged((a, b) => { + return JSON.stringify(a.sort()) === JSON.stringify(b.sort()); + }), + finalize(() => { + this.fetchedZitadelPermissions.next(true); + }), + ), + ), + ); + private zitadelPermissions: BehaviorSubject = new BehaviorSubject([]); + public readonly fetchedZitadelPermissions: BehaviorSubject = new BehaviorSubject(false); private cachedOrgs: Org.AsObject[] = []; @@ -114,6 +142,8 @@ export class GrpcAuthService { private oauthService: OAuthService, private storage: StorageService, ) { + this.zitadelPermissions$.subscribe(this.zitadelPermissions); + this.user = merge( of(this.oauthService.getAccessToken()).pipe(filter((token) => (token ? true : false))), this.oauthService.events.pipe( @@ -223,19 +253,7 @@ export class GrpcAuthService { } private loadPermissions(): void { - from(this.listMyZitadelPermissions()) - .pipe( - map((rolesResp) => rolesResp.resultList), - catchError((_) => { - return of([]); - }), - finalize(() => { - this.fetchedZitadelPermissions.next(true); - }), - ) - .subscribe((roles) => { - this.zitadelPermissions.next(roles); - }); + this.triggerPermissionsRefresh.next(); } /** @@ -244,7 +262,17 @@ export class GrpcAuthService { */ public isAllowed(roles: string[] | RegExp[], requiresAll: boolean = false): Observable { if (roles && roles.length > 0) { - return this.zitadelPermissions.pipe(switchMap((zroles) => of(this.hasRoles(zroles, roles, requiresAll)))); + return this.fetchedZitadelPermissions.pipe( + withLatestFrom(this.zitadelPermissions), + filter(([hL, p]) => { + return hL === true && !!p.length; + }), + map(([_, zroles]) => { + const what = this.hasRoles(zroles, roles, requiresAll); + return what; + }), + distinctUntilChanged(), + ); } else { return of(false); } @@ -356,10 +384,6 @@ export class GrpcAuthService { return this.grpcService.auth.updateMyProfile(req, null).then((resp) => resp.toObject()); } - public get zitadelPermissionsChanged(): Observable { - return this.zitadelPermissions; - } - public listMyUserSessions(): Promise { const req = new ListMyUserSessionsRequest(); return this.grpcService.auth.listMyUserSessions(req, null).then((resp) => resp.toObject()); diff --git a/e2e/cypress/e2e/humans/humans.cy.ts b/e2e/cypress/e2e/humans/humans.cy.ts index 153eb42b44..478ff201df 100644 --- a/e2e/cypress/e2e/humans/humans.cy.ts +++ b/e2e/cypress/e2e/humans/humans.cy.ts @@ -5,7 +5,7 @@ import { } from "../../support/api/users"; import { loginname } from "../../support/login/users"; -describe.skip("humans", () => { +describe("humans", () => { const humansPath = `/users?type=human`; const testHumanUserNameAdd = "e2ehumanusernameadd"; const testHumanUserNameRemove = "e2ehumanusernameremove"; @@ -22,18 +22,20 @@ describe.skip("humans", () => { }); it("should add a user", () => { - cy.get('[data-e2e="action-key-add"]') - .parents('[data-e2e="create-user-button"]') + cy.get('[data-e2e="create-user-button"]') .click(); cy.url().should("contain", "users/create"); - cy.get('[formcontrolname="email"]').type(loginname("e2ehuman")); + cy.get('[formcontrolname="email"]') + .type(loginname("e2ehuman", Cypress.env("ORGANIZATION"))); //force needed due to the prefilled username prefix - cy.get('[formcontrolname="userName"]').type(testHumanUserNameAdd, { - force: true, - }); - cy.get('[formcontrolname="firstName"]').type("e2ehumanfirstname"); - cy.get('[formcontrolname="lastName"]').type("e2ehumanlastname"); - cy.get('[formcontrolname="phone"]').type("+41 123456789"); + cy.get('[formcontrolname="userName"]') + .type(testHumanUserNameAdd); + cy.get('[formcontrolname="firstName"]') + .type("e2ehumanfirstname"); + cy.get('[formcontrolname="lastName"]') + .type("e2ehumanlastname"); + cy.get('[formcontrolname="phone"]') + .type("+41 123456789"); cy.get('[data-e2e="create-button"]').click(); cy.get(".data-e2e-success"); cy.wait(200); @@ -55,10 +57,10 @@ describe.skip("humans", () => { // doesn't work, need to force click. // .trigger('mouseover') .find('[data-e2e="enabled-delete-button"]') - .click({ force: true }); + .click({force: true}); cy.get('[data-e2e="confirm-dialog-input"]') - .click() - .type(loginname(testHumanUserNameRemove, Cypress.env("org"))); + .focus() + .type(loginname(testHumanUserNameRemove, Cypress.env("ORGANIZATION"))); cy.get('[data-e2e="confirm-dialog-button"]').click(); cy.get(".data-e2e-success"); cy.wait(200); diff --git a/e2e/cypress/e2e/machines/machines.cy.ts b/e2e/cypress/e2e/machines/machines.cy.ts index 5447e30f0c..2ea052f138 100644 --- a/e2e/cypress/e2e/machines/machines.cy.ts +++ b/e2e/cypress/e2e/machines/machines.cy.ts @@ -5,7 +5,7 @@ import { } from "../../support/api/users"; import { loginname } from "../../support/login/users"; -describe.skip("machines", () => { +describe("machines", () => { const machinesPath = `/users?type=machine`; const testMachineUserNameAdd = "e2emachineusernameadd"; const testMachineUserNameRemove = "e2emachineusernameremove"; @@ -22,16 +22,16 @@ describe.skip("machines", () => { }); it("should add a machine", () => { - cy.get('[data-e2e="action-key-add"]') - .parents('[data-e2e="create-user-button"]') + cy.get('[data-e2e="create-user-button"]') .click(); cy.url().should("contain", "users/create-machine"); //force needed due to the prefilled username prefix - cy.get('[formcontrolname="userName"]').type(testMachineUserNameAdd, { - force: true, - }); - cy.get('[formcontrolname="name"]').type("e2emachinename"); - cy.get('[formcontrolname="description"]').type("e2emachinedescription"); + cy.get('[formcontrolname="userName"]') + .type(testMachineUserNameAdd); + cy.get('[formcontrolname="name"]') + .type("e2emachinename"); + cy.get('[formcontrolname="description"]') + .type("e2emachinedescription"); cy.get('[data-e2e="create-button"]').click(); cy.get(".data-e2e-success"); cy.wait(200); @@ -49,14 +49,14 @@ describe.skip("machines", () => { }); it("should delete a machine", () => { - cy.contains("tr", testMachineUserNameRemove, { timeout: 1000 }) + cy.contains("tr", testMachineUserNameRemove) // doesn't work, need to force click. // .trigger('mouseover') .find('[data-e2e="enabled-delete-button"]') - .click({ force: true }); + .click({force: true}); cy.get('[data-e2e="confirm-dialog-input"]') - .click() - .type(loginname(testMachineUserNameRemove, Cypress.env("org"))); + .focus() + .type(loginname(testMachineUserNameRemove, Cypress.env("ORGANIZATION"))); cy.get('[data-e2e="confirm-dialog-button"]').click(); cy.get(".data-e2e-success"); cy.wait(200); diff --git a/e2e/cypress/e2e/permissions/permissions.cy.ts b/e2e/cypress/e2e/permissions/permissions.cy.ts index 4894f9e212..17a97f375a 100644 --- a/e2e/cypress/e2e/permissions/permissions.cy.ts +++ b/e2e/cypress/e2e/permissions/permissions.cy.ts @@ -1,7 +1,7 @@ import { apiAuth } from "../../support/api/apiauth"; import { ensureProjectExists, ensureProjectResourceDoesntExist, Roles } from "../../support/api/projects"; -describe.skip('permissions', () => { +describe('permissions', () => { const testProjectName = 'e2eprojectpermission' const testAppName = 'e2eapppermission' @@ -28,12 +28,16 @@ describe.skip('permissions', () => { }) }) - it('should add a role', () => { + it('should add a role', () => { cy.get('[data-e2e="add-new-role"]').click() - cy.get('[formcontrolname="key"]').type(testRoleName) - cy.get('[formcontrolname="displayName"]').type(testRoleDisplay) - cy.get('[formcontrolname="group"]').type(testRoleGroup) - cy.get('[data-e2e="save-button"]').click() + cy.get('[formcontrolname="key"]') + .type(testRoleName) + cy.get('[formcontrolname="displayName"]') + .type(testRoleDisplay) + cy.get('[formcontrolname="group"]') + .type(testRoleGroup) + cy.get('[data-e2e="save-button"]') + .click() cy.get('.data-e2e-success') cy.wait(200) cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist') diff --git a/e2e/cypress/e2e/projects/projects.cy.ts b/e2e/cypress/e2e/projects/projects.cy.ts index b84525805b..5a7a8daeb3 100644 --- a/e2e/cypress/e2e/projects/projects.cy.ts +++ b/e2e/cypress/e2e/projects/projects.cy.ts @@ -42,9 +42,9 @@ describe("projects", () => { cy.contains("tr", testProjectNameDeleteList, { timeout: 1000 }) .find('[data-e2e="delete-project-button"]') .click({ force: true }); - cy.get('[data-e2e="confirm-dialog-input"]').focus().type( - testProjectNameDeleteList - ); + cy.get('[data-e2e="confirm-dialog-input"]') + .focus() + .type(testProjectNameDeleteList); cy.get('[data-e2e="confirm-dialog-button"]').click(); cy.get(".data-e2e-success"); cy.wait(200); @@ -63,11 +63,10 @@ describe("projects", () => { it("removes the project", () => { cy.contains('[data-e2e="grid-card"]', testProjectNameDeleteGrid) .find('[data-e2e="delete-project-button"]') - .trigger("mouseover") - .click(); - cy.get('[data-e2e="confirm-dialog-input"]').focus().type( - testProjectNameDeleteGrid - ); + .click({force: true}); + cy.get('[data-e2e="confirm-dialog-input"]') + .focus() + .type(testProjectNameDeleteGrid); cy.get('[data-e2e="confirm-dialog-button"]').click(); cy.get(".data-e2e-success"); cy.wait(200);