import { Injectable } from '@angular/core'; import { SortDirection } from '@angular/material/sort'; import { OAuthService } from 'angular-oauth2-oidc'; import { BehaviorSubject, combineLatestWith, EMPTY, identity, mergeWith, NEVER, Observable, of, shareReplay, Subject, } from 'rxjs'; import { catchError, distinctUntilChanged, filter, finalize, map, startWith, switchMap, tap, timeout } from 'rxjs/operators'; import { AddMyAuthFactorOTPEmailRequest, AddMyAuthFactorOTPEmailResponse, AddMyAuthFactorOTPRequest, AddMyAuthFactorOTPResponse, AddMyAuthFactorOTPSMSRequest, AddMyAuthFactorOTPSMSResponse, AddMyAuthFactorU2FRequest, AddMyAuthFactorU2FResponse, AddMyPasswordlessLinkRequest, AddMyPasswordlessLinkResponse, AddMyPasswordlessRequest, AddMyPasswordlessResponse, GetMyEmailRequest, GetMyEmailResponse, GetMyLabelPolicyRequest, GetMyLoginPolicyRequest, GetMyLoginPolicyResponse, GetMyPasswordComplexityPolicyRequest, GetMyPasswordComplexityPolicyResponse, GetMyPhoneRequest, GetMyPhoneResponse, GetMyPrivacyPolicyRequest, GetMyProfileRequest, GetMyProfileResponse, GetMyUserRequest, GetMyUserResponse, ListMyAuthFactorsRequest, ListMyAuthFactorsResponse, ListMyLinkedIDPsRequest, ListMyLinkedIDPsResponse, ListMyMembershipsRequest, ListMyMembershipsResponse, ListMyMetadataRequest, ListMyMetadataResponse, ListMyPasswordlessRequest, ListMyPasswordlessResponse, ListMyProjectOrgsRequest, ListMyProjectOrgsResponse, ListMyUserChangesRequest, ListMyUserChangesResponse, ListMyUserGrantsRequest, ListMyUserGrantsResponse, ListMyUserSessionsRequest, ListMyUserSessionsResponse, ListMyZitadelPermissionsRequest, ListMyZitadelPermissionsResponse, RemoveMyAuthFactorOTPEmailRequest, RemoveMyAuthFactorOTPEmailResponse, RemoveMyAuthFactorOTPRequest, RemoveMyAuthFactorOTPResponse, RemoveMyAuthFactorOTPSMSRequest, RemoveMyAuthFactorOTPSMSResponse, 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'; import { ListQuery } from '../proto/generated/zitadel/object_pb'; import { Org, OrgFieldName, OrgIDQuery, OrgQuery } from '../proto/generated/zitadel/org_pb'; import { LabelPolicy, PrivacyPolicy } from '../proto/generated/zitadel/policy_pb'; import { Gender, MembershipQuery, User, WebAuthNVerification } from '../proto/generated/zitadel/user_pb'; import { GrpcService } from './grpc.service'; import { StorageKey, StorageLocation, StorageService } from './storage.service'; const ORG_LIMIT = 10; @Injectable({ providedIn: 'root', }) export class GrpcAuthService { private _activeOrgChanged: Subject = new Subject(); public user: Observable; private triggerPermissionsRefresh: Subject = new Subject(); public zitadelPermissions: Observable; public labelpolicy$!: Observable; labelPolicyLoading$: BehaviorSubject = new BehaviorSubject(true); public privacypolicy$: Observable; public privacypolicy: BehaviorSubject = new BehaviorSubject< PrivacyPolicy.AsObject | undefined >(undefined); public cachedOrgs: BehaviorSubject = new BehaviorSubject([]); private cachedLabelPolicies: { [orgId: string]: LabelPolicy.AsObject } = {}; private cachedPrivacyPolicies: { [orgId: string]: PrivacyPolicy.AsObject } = {}; constructor( private readonly grpcService: GrpcService, private oauthService: OAuthService, private storage: StorageService, ) { this.labelpolicy$ = this.activeOrgChanged.pipe( tap(() => this.labelPolicyLoading$.next(true)), switchMap((org) => this.getMyLabelPolicy(org ? org.id : '')), tap(() => this.labelPolicyLoading$.next(false)), finalize(() => this.labelPolicyLoading$.next(false)), filter((policy) => !!policy), shareReplay({ refCount: true, bufferSize: 1 }), ); this.privacypolicy$ = this.activeOrgChanged.pipe( switchMap((org) => this.getMyPrivacyPolicy(org ? org.id : '')), filter((policy) => !!policy), catchError((err) => { console.error(err); return EMPTY; }), shareReplay({ refCount: true, bufferSize: 1 }), ); this.user = this.oauthService.events.pipe( filter((e) => e.type === 'token_received'), map(() => this.oauthService.getAccessToken()), startWith(this.oauthService.getAccessToken()), filter(Boolean), distinctUntilChanged(), switchMap(() => this.getMyUser()), map((user) => user.user), shareReplay({ refCount: true, bufferSize: 1 }), ); this.zitadelPermissions = this.user.pipe( combineLatestWith(this.activeOrgChanged), // ignore errors from observables catchError(() => of(true)), // make sure observable never completes mergeWith(NEVER), switchMap(() => this.listMyZitadelPermissions() .then((resp) => resp.resultList) .catch(() => []), ), distinctUntilChanged((a, b) => { return JSON.stringify(a.sort()) === JSON.stringify(b.sort()); }), shareReplay({ refCount: true, bufferSize: 1 }), ); } public listMyMetadata( offset?: number, limit?: number, queryList?: MetadataQuery[], ): Promise { const req = new ListMyMetadataRequest(); const metadata = new ListQuery(); if (offset) { metadata.setOffset(offset); } if (limit) { metadata.setLimit(limit); } if (queryList) { req.setQueriesList(queryList); } return this.grpcService.auth.listMyMetadata(req, null).then((resp) => resp.toObject()); } public async getActiveOrg(id?: string): Promise { if (id) { const find = this.cachedOrgs.getValue().find((tmp) => tmp.id === id); if (find) { this.setActiveOrg(find); return Promise.resolve(find); } else { const orgQuery = new OrgQuery(); const orgIdQuery = new OrgIDQuery(); orgIdQuery.setId(id); orgQuery.setIdQuery(orgIdQuery); const orgs = (await this.listMyProjectOrgs(ORG_LIMIT, 0, [orgQuery])).resultList; if (orgs.length === 1) { this.setActiveOrg(orgs[0]); return Promise.resolve(orgs[0]); } else { // throw error if the org was specifically requested but not found return Promise.reject(new Error('requested organization not found')); } } } else { let orgs = this.cachedOrgs.getValue(); const org = this.storage.getItem(StorageKey.organization, StorageLocation.local); if (org) { orgs = (await this.listMyProjectOrgs(ORG_LIMIT, 0)).resultList; this.cachedOrgs.next(orgs); const find = this.cachedOrgs.getValue().find((tmp) => tmp.id === id); if (find) { this.setActiveOrg(find); return Promise.resolve(find); } else { const orgQuery = new OrgQuery(); const orgIdQuery = new OrgIDQuery(); orgIdQuery.setId(org.id); orgQuery.setIdQuery(orgIdQuery); const specificOrg = (await this.listMyProjectOrgs(ORG_LIMIT, 0, [orgQuery])).resultList; if (specificOrg.length === 1) { this.setActiveOrg(specificOrg[0]); return Promise.resolve(specificOrg[0]); } } } else { orgs = (await this.listMyProjectOrgs(ORG_LIMIT, 0)).resultList; this.cachedOrgs.next(orgs); } if (orgs.length === 0) { this._activeOrgChanged.next(undefined); return Promise.reject(new Error('No organizations found!')); } const orgToSet = orgs.find((element) => element.id !== '0' && element.name !== ''); if (orgToSet) { this.setActiveOrg(orgToSet); return Promise.resolve(orgToSet); } return Promise.resolve(orgs[0]); } } public get activeOrgChanged(): Observable { return this._activeOrgChanged.asObservable(); } public setActiveOrg(org: Org.AsObject): void { // Set organization in localstorage to get the last used organization in a new tab this.storage.setItem(StorageKey.organization, org, StorageLocation.local); this.storage.setItem(StorageKey.organization, org, StorageLocation.session); this._activeOrgChanged.next(org); } private loadPermissions(): void { this.triggerPermissionsRefresh.next(); } /** * returns true if user has one of the provided roles * @param roles roles of the user */ public isAllowed(roles: string[] | RegExp[], requiresAll: boolean = false): Observable { if (!roles?.length) { return of(false); } return this.zitadelPermissions.pipe( map((permissions) => this.hasRoles(permissions, roles, requiresAll)), distinctUntilChanged(), ); } /** * filters objects based on roles * @param objects array of objects * @param mapper mapping function which maps to a string[] or Regexp[] of roles * @param requiresAll wheter all, or just a single roles is required to fulfill */ public isAllowedMapper( objects: T[], mapper: (attr: any) => string[] | RegExp[], requiresAll: boolean = false, ): Observable { return this.zitadelPermissions.pipe( filter((permissions) => !!permissions.length), map((permissions) => objects.filter((obj) => { const roles = mapper(obj); return this.hasRoles(permissions, roles, requiresAll); }), ), ); } /** * returns true if user has one of the provided roles * @param userRoles roles of the user * @param requestedRoles required roles for accessing the respective component * @param requiresAll true - if all regexes must match, false - if only one regex must match */ public hasRoles(userRoles: string[], requestedRoles: string[] | RegExp[], requiresAll: boolean = false): boolean { const test = (reqRegexp: string | RegExp) => userRoles.some((role) => { return new RegExp(reqRegexp).test(role); }); const allCheck = requestedRoles.map(test).every(identity); const oneCheck = requestedRoles.some(test); return requiresAll ? allCheck : oneCheck; } public getMyProfile(): Promise { return this.grpcService.auth.getMyProfile(new GetMyProfileRequest(), null).then((resp) => resp.toObject()); } public getMyPasswordComplexityPolicy(): Promise { return this.grpcService.auth .getMyPasswordComplexityPolicy(new GetMyPasswordComplexityPolicyRequest(), null) .then((resp) => resp.toObject()); } public getMyUser(): Promise { return this.grpcService.auth.getMyUser(new GetMyUserRequest(), null).then((resp) => resp.toObject()); } public listMyMultiFactors(): Promise { return this.grpcService.auth.listMyAuthFactors(new ListMyAuthFactorsRequest(), null).then((resp) => resp.toObject()); } public async revalidateOrgs() { const orgs = (await this.listMyProjectOrgs(ORG_LIMIT, 0)).resultList; this.cachedOrgs.next(orgs); } public listMyProjectOrgs( limit?: number, offset?: number, queryList?: OrgQuery[], sortingColumn?: OrgFieldName, sortingDirection?: SortDirection, ): Promise { const req = new ListMyProjectOrgsRequest(); const query = new ListQuery(); if (offset) { query.setOffset(offset); } if (limit) { query.setLimit(limit); } if (queryList) { req.setQueriesList(queryList); } if (sortingDirection) { query.setAsc(sortingDirection === 'asc'); } if (sortingColumn) { req.setSortingColumn(sortingColumn); } req.setQuery(query); return this.grpcService.auth.listMyProjectOrgs(req, null).then((resp) => resp.toObject()); } public updateMyProfile( firstName?: string, lastName?: string, nickName?: string, displayName?: string, preferredLanguage?: string, gender?: Gender, ): Promise { const req = new UpdateMyProfileRequest(); if (firstName) { req.setFirstName(firstName); } if (lastName) { req.setLastName(lastName); } if (nickName) { req.setNickName(nickName); } if (displayName) { req.setDisplayName(displayName); } if (gender) { req.setGender(gender); } if (preferredLanguage) { req.setPreferredLanguage(preferredLanguage); } return this.grpcService.auth.updateMyProfile(req, null).then((resp) => resp.toObject()); } public listMyUserSessions(): Promise { const req = new ListMyUserSessionsRequest(); return this.grpcService.auth.listMyUserSessions(req, null).then((resp) => resp.toObject()); } public listMyUserGrants(limit?: number, offset?: number, asc?: boolean): Promise { const req = new ListMyUserGrantsRequest(); const query = new ListQuery(); if (limit) { query.setLimit(limit); } if (offset) { query.setOffset(offset); } if (asc !== undefined) { query.setAsc(asc); } req.setQuery(query); return this.grpcService.auth.listMyUserGrants(req, null).then((resp) => resp.toObject()); } public listMyMemberships( limit: number, offset: number, queryList?: MembershipQuery[], ): Promise { const req = new ListMyMembershipsRequest(); const metadata = new ListQuery(); if (limit) { metadata.setLimit(limit); } if (offset) { metadata.setOffset(offset); } if (queryList) { req.setQueriesList(queryList); } req.setQuery(metadata); return this.grpcService.auth.listMyMemberships(req, null).then((resp) => resp.toObject()); } public RemoveMyUser(): Promise { const req = new RemoveMyUserRequest(); return this.grpcService.auth.removeMyUser(req, null).then((resp) => resp.toObject()); } public getMyEmail(): Promise { const req = new GetMyEmailRequest(); return this.grpcService.auth.getMyEmail(req, null).then((resp) => resp.toObject()); } public setMyEmail(email: string): Promise { const req = new SetMyEmailRequest(); req.setEmail(email); return this.grpcService.auth.setMyEmail(req, null).then((resp) => resp.toObject()); } public resendMyEmailVerification(): Promise { const req = new ResendMyEmailVerificationRequest(); return this.grpcService.auth.resendMyEmailVerification(req, null).then((resp) => resp.toObject()); } public getMyLoginPolicy(): Promise { const req = new GetMyLoginPolicyRequest(); return this.grpcService.auth.getMyLoginPolicy(req, null).then((resp) => resp.toObject()); } public removeMyPhone(): Promise { return this.grpcService.auth.removeMyPhone(new RemoveMyPhoneRequest(), null).then((resp) => resp.toObject()); } public updateMyUserName(username: string): Promise { const req = new UpdateMyUserNameRequest(); req.setUserName(username); return this.grpcService.auth.updateMyUserName(req, null).then((resp) => resp.toObject()); } public listMyZitadelPermissions(): Promise { return this.grpcService.auth .listMyZitadelPermissions(new ListMyZitadelPermissionsRequest(), null) .then((resp) => resp.toObject()); } public getMyPhone(): Promise { return this.grpcService.auth.getMyPhone(new GetMyPhoneRequest(), null).then((resp) => resp.toObject()); } public setMyPhone(phone: string): Promise { const req = new SetMyPhoneRequest(); req.setPhone(phone); return this.grpcService.auth.setMyPhone(req, null).then((resp) => resp.toObject()); } public resendMyPhoneVerification(): Promise { const req = new ResendMyPhoneVerificationRequest(); return this.grpcService.auth.resendMyPhoneVerification(req, null).then((resp) => resp.toObject()); } public updateMyPassword(oldPassword: string, newPassword: string): Promise { const req = new UpdateMyPasswordRequest(); req.setOldPassword(oldPassword); req.setNewPassword(newPassword); return this.grpcService.auth.updateMyPassword(req, null).then((resp) => resp.toObject()); } public removeMyLinkedIDP(idpId: string, linkedUserId: string): Promise { const req = new RemoveMyLinkedIDPRequest(); req.setLinkedUserId(linkedUserId); req.setIdpId(idpId); return this.grpcService.auth.removeMyLinkedIDP(req, null).then((resp) => resp.toObject()); } public removeMyAvatar(): Promise { const req = new RemoveMyAvatarRequest(); return this.grpcService.auth.removeMyAvatar(req, null).then((resp) => resp.toObject()); } public listMyLinkedIDPs(limit: number, offset: number): Promise { const req = new ListMyLinkedIDPsRequest(); const metadata = new ListQuery(); if (limit) { metadata.setLimit(limit); } if (offset) { metadata.setOffset(offset); } req.setQuery(metadata); return this.grpcService.auth.listMyLinkedIDPs(req, null).then((resp) => resp.toObject()); } public addMyMultiFactorOTP(): Promise { return this.grpcService.auth.addMyAuthFactorOTP(new AddMyAuthFactorOTPRequest(), null).then((resp) => resp.toObject()); } public addMyAuthFactorOTPSMS(): Promise { return this.grpcService.auth .addMyAuthFactorOTPSMS(new AddMyAuthFactorOTPSMSRequest(), null) .then((resp) => resp.toObject()); } public addMyAuthFactorOTPEmail(): Promise { return this.grpcService.auth .addMyAuthFactorOTPEmail(new AddMyAuthFactorOTPEmailRequest(), null) .then((resp) => resp.toObject()); } public addMyMultiFactorU2F(): Promise { return this.grpcService.auth.addMyAuthFactorU2F(new AddMyAuthFactorU2FRequest(), null).then((resp) => resp.toObject()); } public removeMyMultiFactorU2F(tokenId: string): Promise { const req = new RemoveMyAuthFactorU2FRequest(); req.setTokenId(tokenId); return this.grpcService.auth.removeMyAuthFactorU2F(req, null).then((resp) => resp.toObject()); } public verifyMyMultiFactorU2F(credential: string, tokenname: string): Promise { const req = new VerifyMyAuthFactorU2FRequest(); const verification = new WebAuthNVerification(); verification.setPublicKeyCredential(credential); verification.setTokenName(tokenname); req.setVerification(verification); return this.grpcService.auth.verifyMyAuthFactorU2F(req, null).then((resp) => resp.toObject()); } public listMyPasswordless(): Promise { return this.grpcService.auth.listMyPasswordless(new ListMyPasswordlessRequest(), null).then((resp) => resp.toObject()); } public addMyPasswordless(): Promise { return this.grpcService.auth.addMyPasswordless(new AddMyPasswordlessRequest(), null).then((resp) => resp.toObject()); } public removeMyPasswordless(tokenId: string): Promise { const req = new RemoveMyPasswordlessRequest(); req.setTokenId(tokenId); return this.grpcService.auth.removeMyPasswordless(req, null).then((resp) => resp.toObject()); } public verifyMyPasswordless(credential: string, tokenname: string): Promise { const req = new VerifyMyPasswordlessRequest(); const verification = new WebAuthNVerification(); verification.setTokenName(tokenname); verification.setPublicKeyCredential(credential); req.setVerification(verification); return this.grpcService.auth.verifyMyPasswordless(req, null).then((resp) => resp.toObject()); } public sendMyPasswordlessLink(): Promise { const req = new SendMyPasswordlessLinkRequest(); return this.grpcService.auth.sendMyPasswordlessLink(req, null).then((resp) => resp.toObject()); } public addMyPasswordlessLink(): Promise { const req = new AddMyPasswordlessLinkRequest(); return this.grpcService.auth.addMyPasswordlessLink(req, null).then((resp) => resp.toObject()); } public removeMyMultiFactorOTP(): Promise { return this.grpcService.auth .removeMyAuthFactorOTP(new RemoveMyAuthFactorOTPRequest(), null) .then((resp) => resp.toObject()); } public removeMyAuthFactorOTPSMS(): Promise { return this.grpcService.auth .removeMyAuthFactorOTPSMS(new RemoveMyAuthFactorOTPSMSRequest(), null) .then((resp) => resp.toObject()); } public removeMyAuthFactorOTPEmail(): Promise { return this.grpcService.auth .removeMyAuthFactorOTPEmail(new RemoveMyAuthFactorOTPEmailRequest(), null) .then((resp) => resp.toObject()); } public verifyMyMultiFactorOTP(code: string): Promise { const req = new VerifyMyAuthFactorOTPRequest(); req.setCode(code); return this.grpcService.auth.verifyMyAuthFactorOTP(req, null).then((resp) => resp.toObject()); } public verifyMyPhone(code: string): Promise { const req = new VerifyMyPhoneRequest(); req.setCode(code); return this.grpcService.auth.verifyMyPhone(req, null).then((resp) => resp.toObject()); } public listMyUserChanges(limit: number, sequence: number): Promise { const req = new ListMyUserChangesRequest(); const query = new ChangeQuery(); if (limit) { query.setLimit(limit); } if (sequence) { query.setSequence(sequence); } req.setQuery(query); return this.grpcService.auth.listMyUserChanges(req, null).then((resp) => resp.toObject()); } public getMyLabelPolicy(orgIdForCache?: string): Promise { if (orgIdForCache && this.cachedLabelPolicies[orgIdForCache]) { return Promise.resolve(this.cachedLabelPolicies[orgIdForCache]); } return this.grpcService.auth .getMyLabelPolicy(new GetMyLabelPolicyRequest(), null) .then((resp) => resp.toObject()) .then((resp) => { if (!resp.policy) { return Promise.reject(); } if (orgIdForCache) { this.cachedLabelPolicies[orgIdForCache] = resp.policy; } return resp.policy; }); } public getMyPrivacyPolicy(orgIdForCache?: string): Promise { if (orgIdForCache && this.cachedPrivacyPolicies[orgIdForCache]) { return Promise.resolve(this.cachedPrivacyPolicies[orgIdForCache]); } return this.grpcService.auth .getMyPrivacyPolicy(new GetMyPrivacyPolicyRequest(), null) .then((resp) => resp.toObject()) .then((resp) => { if (!resp.policy) { return Promise.reject(); } if (orgIdForCache) { this.cachedPrivacyPolicies[orgIdForCache] = resp.policy; } return resp.policy; }); } }