mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
[PM-5951] Migrate org invite state (#9014)
* use deep linked url for org invite instead of separate state * remove organization invite state & fix tests * clear login redirect for SSO JIT users since they are accepted when setting MP * create accept org invite service and consolidate components in module * finish switch to accept org invite service * move logic to accept org service * the rest of the owl * clear org invite along with deep linked route * pr feedback * fix test and add error to catch null invite * pr feedback * clear stored invite if it doesn't match provided one
This commit is contained in:
@@ -1,244 +0,0 @@
|
|||||||
import { Component } from "@angular/core";
|
|
||||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
|
||||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
|
||||||
import {
|
|
||||||
OrganizationUserAcceptInitRequest,
|
|
||||||
OrganizationUserAcceptRequest,
|
|
||||||
} from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
|
|
||||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
|
||||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
|
||||||
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
|
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
|
||||||
import { OrgKey } from "@bitwarden/common/types/key";
|
|
||||||
|
|
||||||
import { BaseAcceptComponent } from "../common/base.accept.component";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: "app-accept-organization",
|
|
||||||
templateUrl: "accept-organization.component.html",
|
|
||||||
})
|
|
||||||
export class AcceptOrganizationComponent extends BaseAcceptComponent {
|
|
||||||
orgName: string;
|
|
||||||
|
|
||||||
protected requiredParameters: string[] = ["organizationId", "organizationUserId", "token"];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
router: Router,
|
|
||||||
platformUtilsService: PlatformUtilsService,
|
|
||||||
i18nService: I18nService,
|
|
||||||
route: ActivatedRoute,
|
|
||||||
stateService: StateService,
|
|
||||||
private cryptoService: CryptoService,
|
|
||||||
private policyApiService: PolicyApiServiceAbstraction,
|
|
||||||
private policyService: PolicyService,
|
|
||||||
private logService: LogService,
|
|
||||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
|
||||||
private organizationUserService: OrganizationUserService,
|
|
||||||
private messagingService: MessagingService,
|
|
||||||
private apiService: ApiService,
|
|
||||||
) {
|
|
||||||
super(router, platformUtilsService, i18nService, route, stateService);
|
|
||||||
}
|
|
||||||
|
|
||||||
async authedHandler(qParams: Params): Promise<void> {
|
|
||||||
const initOrganization =
|
|
||||||
qParams.initOrganization != null && qParams.initOrganization.toLocaleLowerCase() === "true";
|
|
||||||
if (initOrganization) {
|
|
||||||
this.actionPromise = this.acceptInitOrganizationFlow(qParams);
|
|
||||||
} else {
|
|
||||||
const needsReAuth = (await this.stateService.getOrganizationInvitation()) == null;
|
|
||||||
if (needsReAuth) {
|
|
||||||
// Accepting an org invite requires authentication from a logged out state
|
|
||||||
this.messagingService.send("logout", { redirect: false });
|
|
||||||
await this.prepareOrganizationInvitation(qParams);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// User has already logged in and passed the Master Password policy check
|
|
||||||
this.actionPromise = this.acceptFlow(qParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.actionPromise;
|
|
||||||
await this.apiService.refreshIdentityToken();
|
|
||||||
await this.stateService.setOrganizationInvitation(null);
|
|
||||||
this.platformUtilService.showToast(
|
|
||||||
"success",
|
|
||||||
this.i18nService.t("inviteAccepted"),
|
|
||||||
initOrganization
|
|
||||||
? this.i18nService.t("inviteInitAcceptedDesc")
|
|
||||||
: this.i18nService.t("inviteAcceptedDesc"),
|
|
||||||
{ timeout: 10000 },
|
|
||||||
);
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.router.navigate(["/vault"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async unauthedHandler(qParams: Params): Promise<void> {
|
|
||||||
await this.prepareOrganizationInvitation(qParams);
|
|
||||||
|
|
||||||
// In certain scenarios, we want to accelerate the user through the accept org invite process
|
|
||||||
// For example, if the user has a BW account already, we want them to be taken to login instead of creation.
|
|
||||||
await this.accelerateInviteAcceptIfPossible(qParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async acceptInitOrganizationFlow(qParams: Params): Promise<any> {
|
|
||||||
return this.prepareAcceptInitRequest(qParams).then((request) =>
|
|
||||||
this.organizationUserService.postOrganizationUserAcceptInit(
|
|
||||||
qParams.organizationId,
|
|
||||||
qParams.organizationUserId,
|
|
||||||
request,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async acceptFlow(qParams: Params): Promise<any> {
|
|
||||||
return this.prepareAcceptRequest(qParams).then((request) =>
|
|
||||||
this.organizationUserService.postOrganizationUserAccept(
|
|
||||||
qParams.organizationId,
|
|
||||||
qParams.organizationUserId,
|
|
||||||
request,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async prepareAcceptInitRequest(
|
|
||||||
qParams: Params,
|
|
||||||
): Promise<OrganizationUserAcceptInitRequest> {
|
|
||||||
const request = new OrganizationUserAcceptInitRequest();
|
|
||||||
request.token = qParams.token;
|
|
||||||
|
|
||||||
const [encryptedOrgKey, orgKey] = await this.cryptoService.makeOrgKey<OrgKey>();
|
|
||||||
const [orgPublicKey, encryptedOrgPrivateKey] = await this.cryptoService.makeKeyPair(orgKey);
|
|
||||||
const collection = await this.cryptoService.encrypt(
|
|
||||||
this.i18nService.t("defaultCollection"),
|
|
||||||
orgKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
request.key = encryptedOrgKey.encryptedString;
|
|
||||||
request.keys = new OrganizationKeysRequest(
|
|
||||||
orgPublicKey,
|
|
||||||
encryptedOrgPrivateKey.encryptedString,
|
|
||||||
);
|
|
||||||
request.collectionName = collection.encryptedString;
|
|
||||||
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async prepareAcceptRequest(qParams: Params): Promise<OrganizationUserAcceptRequest> {
|
|
||||||
const request = new OrganizationUserAcceptRequest();
|
|
||||||
request.token = qParams.token;
|
|
||||||
|
|
||||||
if (await this.performResetPasswordAutoEnroll(qParams)) {
|
|
||||||
const response = await this.organizationApiService.getKeys(qParams.organizationId);
|
|
||||||
|
|
||||||
if (response == null) {
|
|
||||||
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const publicKey = Utils.fromB64ToArray(response.publicKey);
|
|
||||||
|
|
||||||
// RSA Encrypt user's encKey.key with organization public key
|
|
||||||
const userKey = await this.cryptoService.getUserKey();
|
|
||||||
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey);
|
|
||||||
|
|
||||||
// Add reset password key to accept request
|
|
||||||
request.resetPasswordKey = encryptedKey.encryptedString;
|
|
||||||
}
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async performResetPasswordAutoEnroll(qParams: Params): Promise<boolean> {
|
|
||||||
let policyList: Policy[] = null;
|
|
||||||
try {
|
|
||||||
const policies = await this.policyApiService.getPoliciesByToken(
|
|
||||||
qParams.organizationId,
|
|
||||||
qParams.token,
|
|
||||||
qParams.email,
|
|
||||||
qParams.organizationUserId,
|
|
||||||
);
|
|
||||||
policyList = Policy.fromListResponse(policies);
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (policyList != null) {
|
|
||||||
const result = this.policyService.getResetPasswordPolicyOptions(
|
|
||||||
policyList,
|
|
||||||
qParams.organizationId,
|
|
||||||
);
|
|
||||||
// Return true if policy enabled and auto-enroll enabled
|
|
||||||
return result[1] && result[0].autoEnrollEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async prepareOrganizationInvitation(qParams: Params): Promise<void> {
|
|
||||||
this.orgName = qParams.organizationName;
|
|
||||||
if (this.orgName != null) {
|
|
||||||
// Fix URL encoding of space issue with Angular
|
|
||||||
this.orgName = this.orgName.replace(/\+/g, " ");
|
|
||||||
}
|
|
||||||
await this.stateService.setOrganizationInvitation(qParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async accelerateInviteAcceptIfPossible(qParams: Params): Promise<void> {
|
|
||||||
// Extract the query params we need to make routing acceleration decisions
|
|
||||||
const orgSsoIdentifier = qParams.orgSsoIdentifier;
|
|
||||||
const orgUserHasExistingUser = this.stringToNullOrBool(qParams.orgUserHasExistingUser);
|
|
||||||
|
|
||||||
// if orgUserHasExistingUser is null, short circuit for backwards compatibility w/ older servers
|
|
||||||
if (orgUserHasExistingUser == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if user exists, send user to login
|
|
||||||
if (orgUserHasExistingUser) {
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.router.navigate(["/login"], {
|
|
||||||
queryParams: { email: qParams.email },
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// no user exists; so either sign in via SSO and JIT provision one or simply register.
|
|
||||||
|
|
||||||
if (orgSsoIdentifier) {
|
|
||||||
// We only send sso org identifier if the org has SSO enabled and the SSO policy required.
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.router.navigate(["/sso"], {
|
|
||||||
queryParams: { email: qParams.email, identifier: orgSsoIdentifier },
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if SSO is disabled OR if sso is enabled but the SSO login required policy is not enabled
|
|
||||||
// then send user to create account
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.router.navigate(["/register"], {
|
|
||||||
queryParams: { email: qParams.email, fromOrgInvite: true },
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
private stringToNullOrBool(s: string | undefined): boolean | null {
|
|
||||||
if (s === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return s.toLowerCase() === "true";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { AcceptOrganizationInviteModule } from "./organization-invite/accept-organization.module";
|
||||||
import { AuthSettingsModule } from "./settings/settings.module";
|
import { AuthSettingsModule } from "./settings/settings.module";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [AuthSettingsModule],
|
imports: [AuthSettingsModule, AcceptOrganizationInviteModule],
|
||||||
declarations: [],
|
declarations: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
exports: [AuthSettingsModule],
|
exports: [AuthSettingsModule],
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||||
|
|
||||||
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
|
|
||||||
import { BaseAcceptComponent } from "../../../common/base.accept.component";
|
import { BaseAcceptComponent } from "../../../common/base.accept.component";
|
||||||
import { SharedModule } from "../../../shared";
|
import { SharedModule } from "../../../shared";
|
||||||
@@ -27,10 +27,10 @@ export class AcceptEmergencyComponent extends BaseAcceptComponent {
|
|||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
route: ActivatedRoute,
|
route: ActivatedRoute,
|
||||||
stateService: StateService,
|
authService: AuthService,
|
||||||
private emergencyAccessService: EmergencyAccessService,
|
private emergencyAccessService: EmergencyAccessService,
|
||||||
) {
|
) {
|
||||||
super(router, platformUtilsService, i18nService, route, stateService);
|
super(router, platformUtilsService, i18nService, route, authService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async authedHandler(qParams: Params): Promise<void> {
|
async authedHandler(qParams: Params): Promise<void> {
|
||||||
|
|||||||
@@ -1,14 +1,27 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component, inject } from "@angular/core";
|
||||||
|
|
||||||
import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component";
|
import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component";
|
||||||
|
|
||||||
|
import { RouterService } from "../../../core";
|
||||||
|
import { AcceptOrganizationInviteService } from "../../organization-invite/accept-organization.service";
|
||||||
@Component({
|
@Component({
|
||||||
selector: "web-login-decryption-options",
|
selector: "web-login-decryption-options",
|
||||||
templateUrl: "login-decryption-options.component.html",
|
templateUrl: "login-decryption-options.component.html",
|
||||||
})
|
})
|
||||||
export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent {
|
export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent {
|
||||||
|
protected routerService = inject(RouterService);
|
||||||
|
protected acceptOrganizationInviteService = inject(AcceptOrganizationInviteService);
|
||||||
|
|
||||||
override async createUser(): Promise<void> {
|
override async createUser(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await super.createUser();
|
await super.createUser();
|
||||||
|
|
||||||
|
// Invites from TDE orgs go through here, but the invite is
|
||||||
|
// accepted while being enrolled in admin recovery. So we need to clear
|
||||||
|
// the redirect and stored org invite.
|
||||||
|
await this.routerService.getAndClearLoginRedirectUrl();
|
||||||
|
await this.acceptOrganizationInviteService.clearOrganizationInvitation();
|
||||||
|
|
||||||
await this.router.navigate(["/vault"]);
|
await this.router.navigate(["/vault"]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.validationService.showError(error);
|
this.validationService.showError(error);
|
||||||
|
|||||||
@@ -15,12 +15,10 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
|
|||||||
import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data";
|
import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data";
|
||||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
|
||||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||||
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
|
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
|
||||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
|
||||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
@@ -32,6 +30,8 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass
|
|||||||
|
|
||||||
import { flagEnabled } from "../../../utils/flags";
|
import { flagEnabled } from "../../../utils/flags";
|
||||||
import { RouterService, StateService } from "../../core";
|
import { RouterService, StateService } from "../../core";
|
||||||
|
import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service";
|
||||||
|
import { OrganizationInvite } from "../organization-invite/organization-invite";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-login",
|
selector: "app-login",
|
||||||
@@ -41,10 +41,11 @@ import { RouterService, StateService } from "../../core";
|
|||||||
export class LoginComponent extends BaseLoginComponent implements OnInit {
|
export class LoginComponent extends BaseLoginComponent implements OnInit {
|
||||||
showResetPasswordAutoEnrollWarning = false;
|
showResetPasswordAutoEnrollWarning = false;
|
||||||
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
||||||
policies: ListResponse<PolicyResponse>;
|
policies: Policy[];
|
||||||
showPasswordless = false;
|
showPasswordless = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private acceptOrganizationInviteService: AcceptOrganizationInviteService,
|
||||||
devicesApiService: DevicesApiServiceAbstraction,
|
devicesApiService: DevicesApiServiceAbstraction,
|
||||||
appIdService: AppIdService,
|
appIdService: AppIdService,
|
||||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||||
@@ -112,37 +113,10 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
|
|||||||
await super.ngOnInit();
|
await super.ngOnInit();
|
||||||
});
|
});
|
||||||
|
|
||||||
const invite = await this.stateService.getOrganizationInvitation();
|
// If there's an existing org invite, use it to get the password policies
|
||||||
if (invite != null) {
|
const orgInvite = await this.acceptOrganizationInviteService.getOrganizationInvite();
|
||||||
let policyList: Policy[] = null;
|
if (orgInvite != null) {
|
||||||
try {
|
await this.initPasswordPolicies(orgInvite);
|
||||||
this.policies = await this.policyApiService.getPoliciesByToken(
|
|
||||||
invite.organizationId,
|
|
||||||
invite.token,
|
|
||||||
invite.email,
|
|
||||||
invite.organizationUserId,
|
|
||||||
);
|
|
||||||
policyList = Policy.fromListResponse(this.policies);
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (policyList != null) {
|
|
||||||
const resetPasswordPolicy = this.policyService.getResetPasswordPolicyOptions(
|
|
||||||
policyList,
|
|
||||||
invite.organizationId,
|
|
||||||
);
|
|
||||||
// Set to true if policy enabled and auto-enroll enabled
|
|
||||||
this.showResetPasswordAutoEnrollWarning =
|
|
||||||
resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled;
|
|
||||||
|
|
||||||
this.policyService
|
|
||||||
.masterPasswordPolicyOptions$(policyList)
|
|
||||||
.pipe(takeUntil(this.destroy$))
|
|
||||||
.subscribe((enforcedPasswordPolicyOptions) => {
|
|
||||||
this.enforcedPasswordPolicyOptions = enforcedPasswordPolicyOptions;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,50 +140,69 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
const policiesData: { [id: string]: PolicyData } = {};
|
const policiesData: { [id: string]: PolicyData } = {};
|
||||||
this.policies.data.map((p) => (policiesData[p.id] = new PolicyData(p)));
|
this.policies.map((p) => (policiesData[p.id] = PolicyData.fromPolicy(p)));
|
||||||
await this.policyService.replace(policiesData);
|
await this.policyService.replace(policiesData);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
await this.router.navigate(["update-password"]);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.router.navigate(["update-password"]);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loginEmailService.clearValues();
|
this.loginEmailService.clearValues();
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
await this.router.navigate([this.successRoute]);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.router.navigate([this.successRoute]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
goToHint() {
|
async goToHint() {
|
||||||
this.setLoginEmailValues();
|
this.setLoginEmailValues();
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
await this.router.navigateByUrl("/hint");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.router.navigateByUrl("/hint");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
goToRegister() {
|
async goToRegister() {
|
||||||
const email = this.formGroup.value.email;
|
const email = this.formGroup.value.email;
|
||||||
|
|
||||||
if (email) {
|
if (email) {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
await this.router.navigate(["/register"], { queryParams: { email: email } });
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.router.navigate(["/register"], { queryParams: { email: email } });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
await this.router.navigate(["/register"]);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.router.navigate(["/register"]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override handleMigrateEncryptionKey(result: AuthResult): boolean {
|
protected override async handleMigrateEncryptionKey(result: AuthResult): Promise<boolean> {
|
||||||
if (!result.requiresEncryptionKeyMigration) {
|
if (!result.requiresEncryptionKeyMigration) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
await this.router.navigate(["migrate-legacy-encryption"]);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.router.navigate(["migrate-legacy-encryption"]);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async initPasswordPolicies(invite: OrganizationInvite): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.policies = await this.policyApiService.getPoliciesByToken(
|
||||||
|
invite.organizationId,
|
||||||
|
invite.token,
|
||||||
|
invite.email,
|
||||||
|
invite.organizationUserId,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.policies == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const resetPasswordPolicy = this.policyService.getResetPasswordPolicyOptions(
|
||||||
|
this.policies,
|
||||||
|
invite.organizationId,
|
||||||
|
);
|
||||||
|
// Set to true if policy enabled and auto-enroll enabled
|
||||||
|
this.showResetPasswordAutoEnrollWarning =
|
||||||
|
resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled;
|
||||||
|
|
||||||
|
this.policyService
|
||||||
|
.masterPasswordPolicyOptions$(this.policies)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe((enforcedPasswordPolicyOptions) => {
|
||||||
|
this.enforcedPasswordPolicyOptions = enforcedPasswordPolicyOptions;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<div class="card d-block">
|
<div class="card d-block">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
{{ orgName }}
|
{{ orgName$ | async }}
|
||||||
<strong class="d-block mt-2">{{ email }}</strong>
|
<strong class="d-block mt-2">{{ email }}</strong>
|
||||||
</p>
|
</p>
|
||||||
<p>{{ "joinOrganizationDesc" | i18n }}</p>
|
<p>{{ "joinOrganizationDesc" | i18n }}</p>
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||||
|
|
||||||
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
|
||||||
|
import { BaseAcceptComponent } from "../../common/base.accept.component";
|
||||||
|
|
||||||
|
import { AcceptOrganizationInviteService } from "./accept-organization.service";
|
||||||
|
import { OrganizationInvite } from "./organization-invite";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "accept-organization.component.html",
|
||||||
|
})
|
||||||
|
export class AcceptOrganizationComponent extends BaseAcceptComponent {
|
||||||
|
orgName$ = this.acceptOrganizationInviteService.orgName$;
|
||||||
|
protected requiredParameters: string[] = ["organizationId", "organizationUserId", "token"];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
router: Router,
|
||||||
|
platformUtilsService: PlatformUtilsService,
|
||||||
|
i18nService: I18nService,
|
||||||
|
route: ActivatedRoute,
|
||||||
|
authService: AuthService,
|
||||||
|
private acceptOrganizationInviteService: AcceptOrganizationInviteService,
|
||||||
|
) {
|
||||||
|
super(router, platformUtilsService, i18nService, route, authService);
|
||||||
|
}
|
||||||
|
|
||||||
|
async authedHandler(qParams: Params): Promise<void> {
|
||||||
|
const invite = OrganizationInvite.fromParams(qParams);
|
||||||
|
const success = await this.acceptOrganizationInviteService.validateAndAcceptInvite(invite);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.platformUtilService.showToast(
|
||||||
|
"success",
|
||||||
|
this.i18nService.t("inviteAccepted"),
|
||||||
|
invite.initOrganization
|
||||||
|
? this.i18nService.t("inviteInitAcceptedDesc")
|
||||||
|
: this.i18nService.t("inviteAcceptedDesc"),
|
||||||
|
{ timeout: 10000 },
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.router.navigate(["/vault"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async unauthedHandler(qParams: Params): Promise<void> {
|
||||||
|
const invite = OrganizationInvite.fromParams(qParams);
|
||||||
|
await this.acceptOrganizationInviteService.setOrganizationInvitation(invite);
|
||||||
|
await this.accelerateInviteAcceptIfPossible(invite);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In certain scenarios, we want to accelerate the user through the accept org invite process
|
||||||
|
* For example, if the user has a BW account already, we want them to be taken to login instead of creation.
|
||||||
|
*/
|
||||||
|
private async accelerateInviteAcceptIfPossible(invite: OrganizationInvite): Promise<void> {
|
||||||
|
// if orgUserHasExistingUser is null, we can't determine the user's status
|
||||||
|
// so we don't want to accelerate the process
|
||||||
|
if (invite.orgUserHasExistingUser == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if user exists, send user to login
|
||||||
|
if (invite.orgUserHasExistingUser) {
|
||||||
|
await this.router.navigate(["/login"], {
|
||||||
|
queryParams: { email: invite.email },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invite.orgSsoIdentifier) {
|
||||||
|
// We only send sso org identifier if the org has SSO enabled and the SSO policy required.
|
||||||
|
// Will JIT provision the user.
|
||||||
|
// Note: If the organization has Admin Recovery enabled, the user will be accepted into the org
|
||||||
|
// upon enrollment. The user should not be returned here.
|
||||||
|
await this.router.navigate(["/sso"], {
|
||||||
|
queryParams: { email: invite.email, identifier: invite.orgSsoIdentifier },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if SSO is disabled OR if sso is enabled but the SSO login required policy is not enabled
|
||||||
|
// then send user to create account
|
||||||
|
await this.router.navigate(["/register"], {
|
||||||
|
queryParams: { email: invite.email, fromOrgInvite: true },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { SharedModule } from "../../shared";
|
||||||
|
|
||||||
|
import { AcceptOrganizationComponent } from "./accept-organization.component";
|
||||||
|
import { AcceptOrganizationInviteService } from "./accept-organization.service";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AcceptOrganizationComponent],
|
||||||
|
imports: [SharedModule],
|
||||||
|
providers: [AcceptOrganizationInviteService],
|
||||||
|
})
|
||||||
|
export class AcceptOrganizationInviteModule {}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import { FakeGlobalStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
|
||||||
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
|
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||||
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||||
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
|
import { ResetPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/reset-password-policy-options";
|
||||||
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
|
import { FakeGlobalState } from "@bitwarden/common/spec/fake-state";
|
||||||
|
import { OrgKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
|
import { I18nService } from "../../core/i18n.service";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AcceptOrganizationInviteService,
|
||||||
|
ORGANIZATION_INVITE,
|
||||||
|
} from "./accept-organization.service";
|
||||||
|
import { OrganizationInvite } from "./organization-invite";
|
||||||
|
|
||||||
|
describe("AcceptOrganizationInviteService", () => {
|
||||||
|
let sut: AcceptOrganizationInviteService;
|
||||||
|
let apiService: MockProxy<ApiService>;
|
||||||
|
let authService: MockProxy<AuthService>;
|
||||||
|
let cryptoService: MockProxy<CryptoService>;
|
||||||
|
let encryptService: MockProxy<EncryptService>;
|
||||||
|
let policyApiService: MockProxy<PolicyApiServiceAbstraction>;
|
||||||
|
let policyService: MockProxy<PolicyService>;
|
||||||
|
let logService: MockProxy<LogService>;
|
||||||
|
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
|
||||||
|
let organizationUserService: MockProxy<OrganizationUserService>;
|
||||||
|
let i18nService: MockProxy<I18nService>;
|
||||||
|
let globalStateProvider: FakeGlobalStateProvider;
|
||||||
|
let globalState: FakeGlobalState<OrganizationInvite>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
apiService = mock();
|
||||||
|
authService = mock();
|
||||||
|
cryptoService = mock();
|
||||||
|
encryptService = mock();
|
||||||
|
policyApiService = mock();
|
||||||
|
policyService = mock();
|
||||||
|
logService = mock();
|
||||||
|
organizationApiService = mock();
|
||||||
|
organizationUserService = mock();
|
||||||
|
i18nService = mock();
|
||||||
|
globalStateProvider = new FakeGlobalStateProvider();
|
||||||
|
globalState = globalStateProvider.getFake(ORGANIZATION_INVITE);
|
||||||
|
|
||||||
|
sut = new AcceptOrganizationInviteService(
|
||||||
|
apiService,
|
||||||
|
authService,
|
||||||
|
cryptoService,
|
||||||
|
encryptService,
|
||||||
|
policyApiService,
|
||||||
|
policyService,
|
||||||
|
logService,
|
||||||
|
organizationApiService,
|
||||||
|
organizationUserService,
|
||||||
|
i18nService,
|
||||||
|
globalStateProvider,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("validateAndAcceptInvite", () => {
|
||||||
|
it("initializes an organization when given an invite where initOrganization is true", async () => {
|
||||||
|
cryptoService.makeOrgKey.mockResolvedValue([
|
||||||
|
{ encryptedString: "string" } as EncString,
|
||||||
|
"orgPrivateKey" as unknown as OrgKey,
|
||||||
|
]);
|
||||||
|
cryptoService.makeKeyPair.mockResolvedValue([
|
||||||
|
"orgPublicKey",
|
||||||
|
{ encryptedString: "string" } as EncString,
|
||||||
|
]);
|
||||||
|
encryptService.encrypt.mockResolvedValue({ encryptedString: "string" } as EncString);
|
||||||
|
const invite = createOrgInvite({ initOrganization: true });
|
||||||
|
|
||||||
|
const result = await sut.validateAndAcceptInvite(invite);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(organizationUserService.postOrganizationUserAcceptInit).toHaveBeenCalled();
|
||||||
|
expect(apiService.refreshIdentityToken).toHaveBeenCalled();
|
||||||
|
expect(globalState.nextMock).toHaveBeenCalledWith(null);
|
||||||
|
expect(organizationUserService.postOrganizationUserAccept).not.toHaveBeenCalled();
|
||||||
|
expect(authService.logOut).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("logs out the user and stores the invite when a master password policy check is required", async () => {
|
||||||
|
const invite = createOrgInvite();
|
||||||
|
policyApiService.getPoliciesByToken.mockResolvedValue([
|
||||||
|
{
|
||||||
|
type: PolicyType.MasterPassword,
|
||||||
|
enabled: true,
|
||||||
|
} as Policy,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await sut.validateAndAcceptInvite(invite);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(authService.logOut).toHaveBeenCalled();
|
||||||
|
expect(globalState.nextMock).toHaveBeenCalledWith(invite);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears the stored invite when a master password policy check is required but the stored invite doesn't match the provided one", async () => {
|
||||||
|
const storedInvite = createOrgInvite({ email: "wrongemail@example.com" });
|
||||||
|
const providedInvite = createOrgInvite();
|
||||||
|
await globalState.update(() => storedInvite);
|
||||||
|
policyApiService.getPoliciesByToken.mockResolvedValue([
|
||||||
|
{
|
||||||
|
type: PolicyType.MasterPassword,
|
||||||
|
enabled: true,
|
||||||
|
} as Policy,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await sut.validateAndAcceptInvite(providedInvite);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(authService.logOut).toHaveBeenCalled();
|
||||||
|
expect(globalState.nextMock).toHaveBeenCalledWith(providedInvite);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts the invitation request when the organization doesn't have a master password policy", async () => {
|
||||||
|
const invite = createOrgInvite();
|
||||||
|
policyApiService.getPoliciesByToken.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const result = await sut.validateAndAcceptInvite(invite);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(organizationUserService.postOrganizationUserAccept).toHaveBeenCalled();
|
||||||
|
expect(apiService.refreshIdentityToken).toHaveBeenCalled();
|
||||||
|
expect(globalState.nextMock).toHaveBeenCalledWith(null);
|
||||||
|
expect(organizationUserService.postOrganizationUserAcceptInit).not.toHaveBeenCalled();
|
||||||
|
expect(authService.logOut).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts the invitation request when the org has a master password policy, but the user has already passed it", async () => {
|
||||||
|
const invite = createOrgInvite();
|
||||||
|
policyApiService.getPoliciesByToken.mockResolvedValue([
|
||||||
|
{
|
||||||
|
type: PolicyType.MasterPassword,
|
||||||
|
enabled: true,
|
||||||
|
} as Policy,
|
||||||
|
]);
|
||||||
|
// an existing invite means the user has already passed the master password policy
|
||||||
|
await globalState.update(() => invite);
|
||||||
|
|
||||||
|
policyService.getResetPasswordPolicyOptions.mockReturnValue([
|
||||||
|
{
|
||||||
|
autoEnrollEnabled: false,
|
||||||
|
} as ResetPasswordPolicyOptions,
|
||||||
|
false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await sut.validateAndAcceptInvite(invite);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(organizationUserService.postOrganizationUserAccept).toHaveBeenCalled();
|
||||||
|
expect(organizationUserService.postOrganizationUserAcceptInit).not.toHaveBeenCalled();
|
||||||
|
expect(authService.logOut).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function createOrgInvite(custom: Partial<OrganizationInvite> = {}): OrganizationInvite {
|
||||||
|
return Object.assign(
|
||||||
|
{
|
||||||
|
email: "user@example.com",
|
||||||
|
initOrganization: false,
|
||||||
|
orgSsoIdentifier: null,
|
||||||
|
orgUserHasExistingUser: false,
|
||||||
|
organizationId: "organizationId",
|
||||||
|
organizationName: "organizationName",
|
||||||
|
organizationUserId: "organizationUserId",
|
||||||
|
token: "token",
|
||||||
|
},
|
||||||
|
custom,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { BehaviorSubject, firstValueFrom, map } from "rxjs";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
|
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||||
|
import {
|
||||||
|
OrganizationUserAcceptRequest,
|
||||||
|
OrganizationUserAcceptInitRequest,
|
||||||
|
} from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
|
||||||
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||||
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
|
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
|
||||||
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import {
|
||||||
|
GlobalState,
|
||||||
|
GlobalStateProvider,
|
||||||
|
KeyDefinition,
|
||||||
|
ORGANIZATION_INVITE_DISK,
|
||||||
|
} from "@bitwarden/common/platform/state";
|
||||||
|
import { OrgKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
|
import { OrganizationInvite } from "./organization-invite";
|
||||||
|
|
||||||
|
// We're storing the organization invite for 2 reasons:
|
||||||
|
// 1. If the org requires a MP policy check, we need to keep track that the user has already been redirected when they return.
|
||||||
|
// 2. The MP policy check happens on login/register flows, we need to store the token to retrieve the policies then.
|
||||||
|
export const ORGANIZATION_INVITE = new KeyDefinition<OrganizationInvite>(
|
||||||
|
ORGANIZATION_INVITE_DISK,
|
||||||
|
"organizationInvite",
|
||||||
|
{
|
||||||
|
deserializer: (invite) => OrganizationInvite.fromJSON(invite),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AcceptOrganizationInviteService {
|
||||||
|
private organizationInvitationState: GlobalState<OrganizationInvite>;
|
||||||
|
private orgNameSubject: BehaviorSubject<string> = new BehaviorSubject<string>(null);
|
||||||
|
private policyCache: Policy[];
|
||||||
|
|
||||||
|
// Fix URL encoding of space issue with Angular
|
||||||
|
orgName$ = this.orgNameSubject.pipe(map((orgName) => orgName.replace(/\+/g, " ")));
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly apiService: ApiService,
|
||||||
|
private readonly authService: AuthService,
|
||||||
|
private readonly cryptoService: CryptoService,
|
||||||
|
private readonly encryptService: EncryptService,
|
||||||
|
private readonly policyApiService: PolicyApiServiceAbstraction,
|
||||||
|
private readonly policyService: PolicyService,
|
||||||
|
private readonly logService: LogService,
|
||||||
|
private readonly organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
|
private readonly organizationUserService: OrganizationUserService,
|
||||||
|
private readonly i18nService: I18nService,
|
||||||
|
private readonly globalStateProvider: GlobalStateProvider,
|
||||||
|
) {
|
||||||
|
this.organizationInvitationState = this.globalStateProvider.get(ORGANIZATION_INVITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the currently stored organization invite */
|
||||||
|
async getOrganizationInvite(): Promise<OrganizationInvite> {
|
||||||
|
return await firstValueFrom(this.organizationInvitationState.state$);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores a new organization invite
|
||||||
|
* @param invite an organization invite
|
||||||
|
* @throws if the invite is nullish
|
||||||
|
*/
|
||||||
|
async setOrganizationInvitation(invite: OrganizationInvite): Promise<void> {
|
||||||
|
if (invite == null) {
|
||||||
|
throw new Error("Invite cannot be null. Use clearOrganizationInvitation instead.");
|
||||||
|
}
|
||||||
|
await this.organizationInvitationState.update(() => invite);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clears the currently stored organization invite */
|
||||||
|
async clearOrganizationInvitation(): Promise<void> {
|
||||||
|
await this.organizationInvitationState.update(() => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates and accepts the organization invitation if possible.
|
||||||
|
* Note: Users might need to pass a MP policy check before accepting an invite to an existing organization. If the user
|
||||||
|
* has not passed this check, they will be logged out and the invite will be stored for later use.
|
||||||
|
* @param invite an organization invite
|
||||||
|
* @returns a promise that resolves a boolean indicating if the invite was accepted.
|
||||||
|
*/
|
||||||
|
async validateAndAcceptInvite(invite: OrganizationInvite): Promise<boolean> {
|
||||||
|
if (invite == null) {
|
||||||
|
throw new Error("Invite cannot be null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creation of a new org
|
||||||
|
if (invite.initOrganization) {
|
||||||
|
await this.acceptAndInitOrganization(invite);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accepting an org invite from existing org
|
||||||
|
if (await this.masterPasswordPolicyCheckRequired(invite)) {
|
||||||
|
await this.setOrganizationInvitation(invite);
|
||||||
|
this.authService.logOut(() => {
|
||||||
|
/* Do nothing */
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We know the user has already logged in and passed a MP policy check
|
||||||
|
await this.accept(invite);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async acceptAndInitOrganization(invite: OrganizationInvite): Promise<void> {
|
||||||
|
await this.prepareAcceptAndInitRequest(invite).then((request) =>
|
||||||
|
this.organizationUserService.postOrganizationUserAcceptInit(
|
||||||
|
invite.organizationId,
|
||||||
|
invite.organizationUserId,
|
||||||
|
request,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await this.apiService.refreshIdentityToken();
|
||||||
|
await this.clearOrganizationInvitation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async prepareAcceptAndInitRequest(
|
||||||
|
invite: OrganizationInvite,
|
||||||
|
): Promise<OrganizationUserAcceptInitRequest> {
|
||||||
|
const request = new OrganizationUserAcceptInitRequest();
|
||||||
|
request.token = invite.token;
|
||||||
|
|
||||||
|
const [encryptedOrgKey, orgKey] = await this.cryptoService.makeOrgKey<OrgKey>();
|
||||||
|
const [orgPublicKey, encryptedOrgPrivateKey] = await this.cryptoService.makeKeyPair(orgKey);
|
||||||
|
const collection = await this.encryptService.encrypt(
|
||||||
|
this.i18nService.t("defaultCollection"),
|
||||||
|
orgKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
request.key = encryptedOrgKey.encryptedString;
|
||||||
|
request.keys = new OrganizationKeysRequest(
|
||||||
|
orgPublicKey,
|
||||||
|
encryptedOrgPrivateKey.encryptedString,
|
||||||
|
);
|
||||||
|
request.collectionName = collection.encryptedString;
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async accept(invite: OrganizationInvite): Promise<void> {
|
||||||
|
await this.prepareAcceptRequest(invite).then((request) =>
|
||||||
|
this.organizationUserService.postOrganizationUserAccept(
|
||||||
|
invite.organizationId,
|
||||||
|
invite.organizationUserId,
|
||||||
|
request,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.apiService.refreshIdentityToken();
|
||||||
|
await this.clearOrganizationInvitation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async prepareAcceptRequest(
|
||||||
|
invite: OrganizationInvite,
|
||||||
|
): Promise<OrganizationUserAcceptRequest> {
|
||||||
|
const request = new OrganizationUserAcceptRequest();
|
||||||
|
request.token = invite.token;
|
||||||
|
|
||||||
|
if (await this.resetPasswordEnrollRequired(invite)) {
|
||||||
|
const response = await this.organizationApiService.getKeys(invite.organizationId);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicKey = Utils.fromB64ToArray(response.publicKey);
|
||||||
|
|
||||||
|
// RSA Encrypt user's encKey.key with organization public key
|
||||||
|
const userKey = await this.cryptoService.getUserKey();
|
||||||
|
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey);
|
||||||
|
|
||||||
|
// Add reset password key to accept request
|
||||||
|
request.resetPasswordKey = encryptedKey.encryptedString;
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resetPasswordEnrollRequired(invite: OrganizationInvite): Promise<boolean> {
|
||||||
|
const policies = await this.getPolicies(invite);
|
||||||
|
|
||||||
|
if (policies == null || policies.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = this.policyService.getResetPasswordPolicyOptions(
|
||||||
|
policies,
|
||||||
|
invite.organizationId,
|
||||||
|
);
|
||||||
|
// Return true if policy enabled and auto-enroll enabled
|
||||||
|
return result[1] && result[0].autoEnrollEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async masterPasswordPolicyCheckRequired(invite: OrganizationInvite): Promise<boolean> {
|
||||||
|
const policies = await this.getPolicies(invite);
|
||||||
|
|
||||||
|
if (policies == null || policies.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasMasterPasswordPolicy = policies.some(
|
||||||
|
(p) => p.type === PolicyType.MasterPassword && p.enabled,
|
||||||
|
);
|
||||||
|
|
||||||
|
let storedInvite = await this.getOrganizationInvite();
|
||||||
|
if (storedInvite?.email !== invite.email) {
|
||||||
|
// clear stored invites if the email doesn't match
|
||||||
|
await this.clearOrganizationInvitation();
|
||||||
|
storedInvite = null;
|
||||||
|
}
|
||||||
|
// if we don't have an org invite stored, we know the user hasn't been redirected yet to check the MP policy
|
||||||
|
const hasNotCheckedMasterPasswordYet = storedInvite == null;
|
||||||
|
return hasMasterPasswordPolicy && hasNotCheckedMasterPasswordYet;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPolicies(invite: OrganizationInvite): Promise<Policy[] | null> {
|
||||||
|
// if policies are not cached, fetch them
|
||||||
|
if (this.policyCache == null) {
|
||||||
|
try {
|
||||||
|
this.policyCache = await this.policyApiService.getPoliciesByToken(
|
||||||
|
invite.organizationId,
|
||||||
|
invite.token,
|
||||||
|
invite.email,
|
||||||
|
invite.organizationUserId,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.policyCache;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Params } from "@angular/router";
|
||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
export class OrganizationInvite {
|
||||||
|
email: string;
|
||||||
|
initOrganization: boolean;
|
||||||
|
orgSsoIdentifier: string;
|
||||||
|
orgUserHasExistingUser: boolean;
|
||||||
|
organizationId: string;
|
||||||
|
organizationName: string;
|
||||||
|
organizationUserId: string;
|
||||||
|
token: string;
|
||||||
|
|
||||||
|
static fromJSON(json: Jsonify<OrganizationInvite>) {
|
||||||
|
return Object.assign(new OrganizationInvite(), json);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromParams(params: Params): OrganizationInvite {
|
||||||
|
return Object.assign(new OrganizationInvite(), {
|
||||||
|
email: params.email,
|
||||||
|
initOrganization: params.initOrganization?.toLocaleLowerCase() === "true",
|
||||||
|
orgSsoIdentifier: params.orgSsoIdentifier,
|
||||||
|
orgUserHasExistingUser: params.orgUserHasExistingUser?.toLocaleLowerCase() === "true",
|
||||||
|
organizationId: params.organizationId,
|
||||||
|
organizationName: params.organizationName,
|
||||||
|
organizationUserId: params.organizationUserId,
|
||||||
|
token: params.token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
|||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||||
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
|
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
|
||||||
|
import { RegisterRequest } from "@bitwarden/common/models/request/register.request";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@@ -19,6 +20,8 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
|
|||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-register-form",
|
selector: "app-register-form",
|
||||||
templateUrl: "./register-form.component.html",
|
templateUrl: "./register-form.component.html",
|
||||||
@@ -48,6 +51,7 @@ export class RegisterFormComponent extends BaseRegisterComponent {
|
|||||||
logService: LogService,
|
logService: LogService,
|
||||||
auditService: AuditService,
|
auditService: AuditService,
|
||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
|
acceptOrgInviteService: AcceptOrganizationInviteService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
formValidationErrorService,
|
formValidationErrorService,
|
||||||
@@ -65,6 +69,16 @@ export class RegisterFormComponent extends BaseRegisterComponent {
|
|||||||
auditService,
|
auditService,
|
||||||
dialogService,
|
dialogService,
|
||||||
);
|
);
|
||||||
|
super.modifyRegisterRequest = async (request: RegisterRequest) => {
|
||||||
|
// Org invites are deep linked. Non-existent accounts are redirected to the register page.
|
||||||
|
// Org user id and token are included here only for validation and two factor purposes.
|
||||||
|
const orgInvite = await acceptOrgInviteService.getOrganizationInvite();
|
||||||
|
if (orgInvite != null) {
|
||||||
|
request.organizationUserId = orgInvite.organizationUserId;
|
||||||
|
request.token = orgInvite.token;
|
||||||
|
}
|
||||||
|
// Invite is accepted after login (on deep link redirect).
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
|||||||
@@ -1,9 +1,30 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component, inject } from "@angular/core";
|
||||||
|
|
||||||
import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component";
|
import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component";
|
||||||
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
|
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
|
import { RouterService } from "../core";
|
||||||
|
|
||||||
|
import { AcceptOrganizationInviteService } from "./organization-invite/accept-organization.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-set-password",
|
selector: "app-set-password",
|
||||||
templateUrl: "set-password.component.html",
|
templateUrl: "set-password.component.html",
|
||||||
})
|
})
|
||||||
export class SetPasswordComponent extends BaseSetPasswordComponent {}
|
export class SetPasswordComponent extends BaseSetPasswordComponent {
|
||||||
|
routerService = inject(RouterService);
|
||||||
|
acceptOrganizationInviteService = inject(AcceptOrganizationInviteService);
|
||||||
|
|
||||||
|
protected override async onSetPasswordSuccess(
|
||||||
|
masterKey: MasterKey,
|
||||||
|
userKey: [UserKey, EncString],
|
||||||
|
keyPair: [string, EncString],
|
||||||
|
): Promise<void> {
|
||||||
|
await super.onSetPasswordSuccess(masterKey, userKey, keyPair);
|
||||||
|
// SSO JIT accepts org invites when setting their MP, meaning
|
||||||
|
// we can clear the deep linked url for accepting it.
|
||||||
|
await this.routerService.getAndClearLoginRedirectUrl();
|
||||||
|
await this.acceptOrganizationInviteService.clearOrganizationInvitation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,15 +12,16 @@ import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
|||||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||||
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
import { PlanType } from "@bitwarden/common/billing/enums";
|
import { PlanType } from "@bitwarden/common/billing/enums";
|
||||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
|
||||||
import { RouterService } from "../../core";
|
import { RouterService } from "../../core";
|
||||||
import { SharedModule } from "../../shared";
|
import { SharedModule } from "../../shared";
|
||||||
|
import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service";
|
||||||
|
import { OrganizationInvite } from "../organization-invite/organization-invite";
|
||||||
|
|
||||||
import { TrialInitiationComponent } from "./trial-initiation.component";
|
import { TrialInitiationComponent } from "./trial-initiation.component";
|
||||||
import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component";
|
import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component";
|
||||||
@@ -36,12 +37,16 @@ describe("TrialInitiationComponent", () => {
|
|||||||
let stateServiceMock: MockProxy<StateService>;
|
let stateServiceMock: MockProxy<StateService>;
|
||||||
let policyApiServiceMock: MockProxy<PolicyApiServiceAbstraction>;
|
let policyApiServiceMock: MockProxy<PolicyApiServiceAbstraction>;
|
||||||
let policyServiceMock: MockProxy<PolicyService>;
|
let policyServiceMock: MockProxy<PolicyService>;
|
||||||
|
let routerServiceMock: MockProxy<RouterService>;
|
||||||
|
let acceptOrgInviteServiceMock: MockProxy<AcceptOrganizationInviteService>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// only define services directly that we want to mock return values in this component
|
// only define services directly that we want to mock return values in this component
|
||||||
stateServiceMock = mock<StateService>();
|
stateServiceMock = mock<StateService>();
|
||||||
policyApiServiceMock = mock<PolicyApiServiceAbstraction>();
|
policyApiServiceMock = mock<PolicyApiServiceAbstraction>();
|
||||||
policyServiceMock = mock<PolicyService>();
|
policyServiceMock = mock<PolicyService>();
|
||||||
|
routerServiceMock = mock<RouterService>();
|
||||||
|
acceptOrgInviteServiceMock = mock<AcceptOrganizationInviteService>();
|
||||||
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
@@ -81,7 +86,11 @@ describe("TrialInitiationComponent", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: RouterService,
|
provide: RouterService,
|
||||||
useValue: mock<RouterService>(),
|
useValue: routerServiceMock,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AcceptOrganizationInviteService,
|
||||||
|
useValue: acceptOrgInviteServiceMock,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA], // Allows child components to be ignored (such as register component)
|
schemas: [NO_ERRORS_SCHEMA], // Allows child components to be ignored (such as register component)
|
||||||
@@ -100,8 +109,8 @@ describe("TrialInitiationComponent", () => {
|
|||||||
|
|
||||||
// These tests demonstrate mocking service calls
|
// These tests demonstrate mocking service calls
|
||||||
describe("onInit() enforcedPolicyOptions", () => {
|
describe("onInit() enforcedPolicyOptions", () => {
|
||||||
it("should not set enforcedPolicyOptions if state service returns no invite", async () => {
|
it("should not set enforcedPolicyOptions if there isn't an org invite in deep linked url", async () => {
|
||||||
stateServiceMock.getOrganizationInvitation.mockReturnValueOnce(null);
|
acceptOrgInviteServiceMock.getOrganizationInvite.mockResolvedValueOnce(null);
|
||||||
// Need to recreate component with new service mock
|
// Need to recreate component with new service mock
|
||||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
@@ -109,37 +118,31 @@ describe("TrialInitiationComponent", () => {
|
|||||||
|
|
||||||
expect(component.enforcedPolicyOptions).toBe(undefined);
|
expect(component.enforcedPolicyOptions).toBe(undefined);
|
||||||
});
|
});
|
||||||
it("should set enforcedPolicyOptions if state service returns an invite", async () => {
|
it("should set enforcedPolicyOptions if the deep linked url has an org invite", async () => {
|
||||||
// Set up service method mocks
|
// Set up service method mocks
|
||||||
stateServiceMock.getOrganizationInvitation.mockReturnValueOnce(
|
acceptOrgInviteServiceMock.getOrganizationInvite.mockResolvedValueOnce({
|
||||||
Promise.resolve({
|
organizationId: testOrgId,
|
||||||
organizationId: testOrgId,
|
token: "token",
|
||||||
token: "token",
|
email: "testEmail",
|
||||||
email: "testEmail",
|
organizationUserId: "123",
|
||||||
organizationUserId: "123",
|
} as OrganizationInvite);
|
||||||
}),
|
|
||||||
);
|
|
||||||
policyApiServiceMock.getPoliciesByToken.mockReturnValueOnce(
|
policyApiServiceMock.getPoliciesByToken.mockReturnValueOnce(
|
||||||
Promise.resolve({
|
Promise.resolve([
|
||||||
data: [
|
{
|
||||||
{
|
id: "345",
|
||||||
id: "345",
|
organizationId: testOrgId,
|
||||||
organizationId: testOrgId,
|
type: 1,
|
||||||
type: 1,
|
data: {
|
||||||
data: [
|
minComplexity: 4,
|
||||||
{
|
minLength: 10,
|
||||||
minComplexity: 4,
|
requireLower: null,
|
||||||
minLength: 10,
|
requireNumbers: null,
|
||||||
requireLower: null,
|
requireSpecial: null,
|
||||||
requireNumbers: null,
|
requireUpper: null,
|
||||||
requireSpecial: null,
|
|
||||||
requireUpper: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
enabled: true,
|
|
||||||
},
|
},
|
||||||
],
|
enabled: true,
|
||||||
} as ListResponse<PolicyResponse>),
|
},
|
||||||
|
] as Policy[]),
|
||||||
);
|
);
|
||||||
policyServiceMock.masterPasswordPolicyOptions$.mockReturnValue(
|
policyServiceMock.masterPasswordPolicyOptions$.mockReturnValue(
|
||||||
of({
|
of({
|
||||||
|
|||||||
@@ -14,13 +14,14 @@ import { ProductType } from "@bitwarden/common/enums";
|
|||||||
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
|
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
OrganizationCreatedEvent,
|
OrganizationCreatedEvent,
|
||||||
SubscriptionProduct,
|
SubscriptionProduct,
|
||||||
TrialOrganizationType,
|
TrialOrganizationType,
|
||||||
} from "../../billing/accounts/trial-initiation/trial-billing-step.component";
|
} from "../../billing/accounts/trial-initiation/trial-billing-step.component";
|
||||||
|
import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service";
|
||||||
|
import { OrganizationInvite } from "../organization-invite/organization-invite";
|
||||||
|
|
||||||
import { RouterService } from "./../../core/router.service";
|
import { RouterService } from "./../../core/router.service";
|
||||||
import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component";
|
import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component";
|
||||||
@@ -121,12 +122,12 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
|
|||||||
protected router: Router,
|
protected router: Router,
|
||||||
private formBuilder: UntypedFormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private titleCasePipe: TitleCasePipe,
|
private titleCasePipe: TitleCasePipe,
|
||||||
private stateService: StateService,
|
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private policyApiService: PolicyApiServiceAbstraction,
|
private policyApiService: PolicyApiServiceAbstraction,
|
||||||
private policyService: PolicyService,
|
private policyService: PolicyService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private routerService: RouterService,
|
private routerService: RouterService,
|
||||||
|
private acceptOrgInviteService: AcceptOrganizationInviteService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
@@ -180,30 +181,10 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
|
|||||||
: "Password Manager trial from marketing website";
|
: "Password Manager trial from marketing website";
|
||||||
});
|
});
|
||||||
|
|
||||||
const invite = await this.stateService.getOrganizationInvitation();
|
// If there's a deep linked org invite, use it to get the password policies
|
||||||
if (invite != null) {
|
const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite();
|
||||||
try {
|
if (orgInvite != null) {
|
||||||
const policies = await this.policyApiService.getPoliciesByToken(
|
await this.initPasswordPolicies(orgInvite);
|
||||||
invite.organizationId,
|
|
||||||
invite.token,
|
|
||||||
invite.email,
|
|
||||||
invite.organizationUserId,
|
|
||||||
);
|
|
||||||
if (policies.data != null) {
|
|
||||||
this.policies = Policy.fromListResponse(policies);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.policies != null) {
|
|
||||||
this.policyService
|
|
||||||
.masterPasswordPolicyOptions$(this.policies)
|
|
||||||
.pipe(takeUntil(this.destroy$))
|
|
||||||
.subscribe((enforcedPasswordPolicyOptions) => {
|
|
||||||
this.enforcedPolicyOptions = enforcedPasswordPolicyOptions;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.orgInfoFormGroup.controls.name.valueChanges
|
this.orgInfoFormGroup.controls.name.valueChanges
|
||||||
@@ -304,5 +285,31 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async initPasswordPolicies(invite: OrganizationInvite): Promise<void> {
|
||||||
|
if (invite == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.policies = await this.policyApiService.getPoliciesByToken(
|
||||||
|
invite.organizationId,
|
||||||
|
invite.token,
|
||||||
|
invite.email,
|
||||||
|
invite.organizationUserId,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.policies != null) {
|
||||||
|
this.policyService
|
||||||
|
.masterPasswordPolicyOptions$(this.policies)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe((enforcedPasswordPolicyOptions) => {
|
||||||
|
this.enforcedPolicyOptions = enforcedPasswordPolicyOptions;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected readonly SubscriptionProduct = SubscriptionProduct;
|
protected readonly SubscriptionProduct = SubscriptionProduct;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,24 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component, inject } from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
|
||||||
|
|
||||||
import { UpdatePasswordComponent as BaseUpdatePasswordComponent } from "@bitwarden/angular/auth/components/update-password.component";
|
import { UpdatePasswordComponent as BaseUpdatePasswordComponent } from "@bitwarden/angular/auth/components/update-password.component";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { RouterService } from "../core";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
|
||||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
import { AcceptOrganizationInviteService } from "./organization-invite/accept-organization.service";
|
||||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
|
||||||
import { DialogService } from "@bitwarden/components";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-update-password",
|
selector: "app-update-password",
|
||||||
templateUrl: "update-password.component.html",
|
templateUrl: "update-password.component.html",
|
||||||
})
|
})
|
||||||
export class UpdatePasswordComponent extends BaseUpdatePasswordComponent {
|
export class UpdatePasswordComponent extends BaseUpdatePasswordComponent {
|
||||||
constructor(
|
private routerService = inject(RouterService);
|
||||||
router: Router,
|
private acceptOrganizationInviteService = inject(AcceptOrganizationInviteService);
|
||||||
i18nService: I18nService,
|
|
||||||
platformUtilsService: PlatformUtilsService,
|
override async cancel() {
|
||||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
// clearing the login redirect url so that the user
|
||||||
policyService: PolicyService,
|
// does not join the organization if they cancel
|
||||||
cryptoService: CryptoService,
|
await this.routerService.getAndClearLoginRedirectUrl();
|
||||||
messagingService: MessagingService,
|
await this.acceptOrganizationInviteService.clearOrganizationInvitation();
|
||||||
apiService: ApiService,
|
await super.cancel();
|
||||||
logService: LogService,
|
|
||||||
stateService: StateService,
|
|
||||||
userVerificationService: UserVerificationService,
|
|
||||||
dialogService: DialogService,
|
|
||||||
kdfConfigService: KdfConfigService,
|
|
||||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
|
||||||
accountService: AccountService,
|
|
||||||
) {
|
|
||||||
super(
|
|
||||||
router,
|
|
||||||
i18nService,
|
|
||||||
platformUtilsService,
|
|
||||||
passwordGenerationService,
|
|
||||||
policyService,
|
|
||||||
cryptoService,
|
|
||||||
messagingService,
|
|
||||||
apiService,
|
|
||||||
stateService,
|
|
||||||
userVerificationService,
|
|
||||||
logService,
|
|
||||||
dialogService,
|
|
||||||
kdfConfigService,
|
|
||||||
masterPasswordService,
|
|
||||||
accountService,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Directive, OnInit } from "@angular/core";
|
import { Directive, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||||
import { Subject } from "rxjs";
|
import { Subject, firstValueFrom } from "rxjs";
|
||||||
import { first, switchMap, takeUntil } from "rxjs/operators";
|
import { first, switchMap, takeUntil } from "rxjs/operators";
|
||||||
|
|
||||||
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export abstract class BaseAcceptComponent implements OnInit {
|
export abstract class BaseAcceptComponent implements OnInit {
|
||||||
@@ -25,7 +26,7 @@ export abstract class BaseAcceptComponent implements OnInit {
|
|||||||
protected platformUtilService: PlatformUtilsService,
|
protected platformUtilService: PlatformUtilsService,
|
||||||
protected i18nService: I18nService,
|
protected i18nService: I18nService,
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected stateService: StateService,
|
protected authService: AuthService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
abstract authedHandler(qParams: Params): Promise<void>;
|
abstract authedHandler(qParams: Params): Promise<void>;
|
||||||
@@ -41,10 +42,10 @@ export abstract class BaseAcceptComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
let errorMessage: string = null;
|
let errorMessage: string = null;
|
||||||
if (!error) {
|
if (!error) {
|
||||||
this.authed = await this.stateService.getIsAuthenticated();
|
|
||||||
this.email = qParams.email;
|
this.email = qParams.email;
|
||||||
|
|
||||||
if (this.authed) {
|
const status = await firstValueFrom(this.authService.activeAccountStatus$);
|
||||||
|
if (status !== AuthenticationStatus.LoggedOut) {
|
||||||
try {
|
try {
|
||||||
await this.authedHandler(qParams);
|
await this.authedHandler(qParams);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export class RouterService {
|
|||||||
/**
|
/**
|
||||||
* Fetch and clear persisted LoginRedirectUrl if present in state
|
* Fetch and clear persisted LoginRedirectUrl if present in state
|
||||||
*/
|
*/
|
||||||
async getAndClearLoginRedirectUrl(): Promise<string> | undefined {
|
async getAndClearLoginRedirectUrl(): Promise<string | undefined> {
|
||||||
const persistedPreLoginUrl = await firstValueFrom(this.deepLinkRedirectUrlState.state$);
|
const persistedPreLoginUrl = await firstValueFrom(this.deepLinkRedirectUrlState.state$);
|
||||||
|
|
||||||
if (!Utils.isNullOrEmpty(persistedPreLoginUrl)) {
|
if (!Utils.isNullOrEmpty(persistedPreLoginUrl)) {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizatio
|
|||||||
import { VerifyRecoverDeleteProviderComponent } from "./admin-console/providers/verify-recover-delete-provider.component";
|
import { VerifyRecoverDeleteProviderComponent } from "./admin-console/providers/verify-recover-delete-provider.component";
|
||||||
import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component";
|
import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component";
|
||||||
import { SponsoredFamiliesComponent } from "./admin-console/settings/sponsored-families.component";
|
import { SponsoredFamiliesComponent } from "./admin-console/settings/sponsored-families.component";
|
||||||
import { AcceptOrganizationComponent } from "./auth/accept-organization.component";
|
|
||||||
import { deepLinkGuard } from "./auth/guards/deep-link.guard";
|
import { deepLinkGuard } from "./auth/guards/deep-link.guard";
|
||||||
import { HintComponent } from "./auth/hint.component";
|
import { HintComponent } from "./auth/hint.component";
|
||||||
import { LockComponent } from "./auth/lock.component";
|
import { LockComponent } from "./auth/lock.component";
|
||||||
@@ -25,6 +24,7 @@ import { LoginDecryptionOptionsComponent } from "./auth/login/login-decryption-o
|
|||||||
import { LoginViaAuthRequestComponent } from "./auth/login/login-via-auth-request.component";
|
import { LoginViaAuthRequestComponent } from "./auth/login/login-via-auth-request.component";
|
||||||
import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login-via-webauthn.component";
|
import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login-via-webauthn.component";
|
||||||
import { LoginComponent } from "./auth/login/login.component";
|
import { LoginComponent } from "./auth/login/login.component";
|
||||||
|
import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component";
|
||||||
import { RecoverDeleteComponent } from "./auth/recover-delete.component";
|
import { RecoverDeleteComponent } from "./auth/recover-delete.component";
|
||||||
import { RecoverTwoFactorComponent } from "./auth/recover-two-factor.component";
|
import { RecoverTwoFactorComponent } from "./auth/recover-two-factor.component";
|
||||||
import { RemovePasswordComponent } from "./auth/remove-password.component";
|
import { RemovePasswordComponent } from "./auth/remove-password.component";
|
||||||
@@ -120,8 +120,8 @@ const routes: Routes = [
|
|||||||
{ path: "verify-email", component: VerifyEmailTokenComponent },
|
{ path: "verify-email", component: VerifyEmailTokenComponent },
|
||||||
{
|
{
|
||||||
path: "accept-organization",
|
path: "accept-organization",
|
||||||
component: AcceptOrganizationComponent,
|
|
||||||
canActivate: [deepLinkGuard()],
|
canActivate: [deepLinkGuard()],
|
||||||
|
component: AcceptOrganizationComponent,
|
||||||
data: { titleId: "joinOrganization", doNotSaveUrl: false },
|
data: { titleId: "joinOrganization", doNotSaveUrl: false },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { ProvidersComponent } from "../admin-console/providers/providers.compone
|
|||||||
import { VerifyRecoverDeleteProviderComponent } from "../admin-console/providers/verify-recover-delete-provider.component";
|
import { VerifyRecoverDeleteProviderComponent } from "../admin-console/providers/verify-recover-delete-provider.component";
|
||||||
import { SponsoredFamiliesComponent } from "../admin-console/settings/sponsored-families.component";
|
import { SponsoredFamiliesComponent } from "../admin-console/settings/sponsored-families.component";
|
||||||
import { SponsoringOrgRowComponent } from "../admin-console/settings/sponsoring-org-row.component";
|
import { SponsoringOrgRowComponent } from "../admin-console/settings/sponsoring-org-row.component";
|
||||||
import { AcceptOrganizationComponent } from "../auth/accept-organization.component";
|
|
||||||
import { HintComponent } from "../auth/hint.component";
|
import { HintComponent } from "../auth/hint.component";
|
||||||
import { LockComponent } from "../auth/lock.component";
|
import { LockComponent } from "../auth/lock.component";
|
||||||
import { RecoverDeleteComponent } from "../auth/recover-delete.component";
|
import { RecoverDeleteComponent } from "../auth/recover-delete.component";
|
||||||
@@ -120,7 +119,6 @@ import { SharedModule } from "./shared.module";
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AcceptFamilySponsorshipComponent,
|
AcceptFamilySponsorshipComponent,
|
||||||
AcceptOrganizationComponent,
|
|
||||||
AccountComponent,
|
AccountComponent,
|
||||||
AddEditComponent,
|
AddEditComponent,
|
||||||
AddEditCustomFieldsComponent,
|
AddEditCustomFieldsComponent,
|
||||||
@@ -193,7 +191,6 @@ import { SharedModule } from "./shared.module";
|
|||||||
exports: [
|
exports: [
|
||||||
UserVerificationModule,
|
UserVerificationModule,
|
||||||
PremiumBadgeComponent,
|
PremiumBadgeComponent,
|
||||||
AcceptOrganizationComponent,
|
|
||||||
AccountComponent,
|
AccountComponent,
|
||||||
AddEditComponent,
|
AddEditComponent,
|
||||||
AddEditCustomFieldsComponent,
|
AddEditCustomFieldsComponent,
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { ActivatedRoute, Params, Router } from "@angular/router";
|
|||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { ProviderUserAcceptRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-accept.request";
|
import { ProviderUserAcceptRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-accept.request";
|
||||||
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept.component";
|
import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -23,11 +23,11 @@ export class AcceptProviderComponent extends BaseAcceptComponent {
|
|||||||
router: Router,
|
router: Router,
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
route: ActivatedRoute,
|
route: ActivatedRoute,
|
||||||
stateService: StateService,
|
authService: AuthService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
platformUtilService: PlatformUtilsService,
|
platformUtilService: PlatformUtilsService,
|
||||||
) {
|
) {
|
||||||
super(router, platformUtilService, i18nService, route, stateService);
|
super(router, platformUtilService, i18nService, route, authService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async authedHandler(qParams: Params) {
|
async authedHandler(qParams: Params) {
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
|||||||
|
|
||||||
if (this.handleCaptchaRequired(response)) {
|
if (this.handleCaptchaRequired(response)) {
|
||||||
return;
|
return;
|
||||||
} else if (this.handleMigrateEncryptionKey(response)) {
|
} else if (await this.handleMigrateEncryptionKey(response)) {
|
||||||
return;
|
return;
|
||||||
} else if (response.requiresTwoFactor) {
|
} else if (response.requiresTwoFactor) {
|
||||||
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
||||||
@@ -218,9 +218,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.setLoginEmailValues();
|
this.setLoginEmailValues();
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
await this.router.navigate(["/login-with-device"]);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.router.navigate(["/login-with-device"]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async launchSsoBrowser(clientId: string, ssoRedirectUri: string) {
|
async launchSsoBrowser(clientId: string, ssoRedirectUri: string) {
|
||||||
@@ -310,7 +308,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
|||||||
|
|
||||||
// Legacy accounts used the master key to encrypt data. Migration is required
|
// Legacy accounts used the master key to encrypt data. Migration is required
|
||||||
// but only performed on web
|
// but only performed on web
|
||||||
protected handleMigrateEncryptionKey(result: AuthResult): boolean {
|
protected async handleMigrateEncryptionKey(result: AuthResult): Promise<boolean> {
|
||||||
if (!result.requiresEncryptionKeyMigration) {
|
if (!result.requiresEncryptionKeyMigration) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,10 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
|
|||||||
|
|
||||||
protected captchaBypassToken: string = null;
|
protected captchaBypassToken: string = null;
|
||||||
|
|
||||||
|
// allows for extending classes to modify the register request before sending
|
||||||
|
// currently used by web to add organization invitation details
|
||||||
|
protected modifyRegisterRequest: (request: RegisterRequest) => Promise<void>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected formValidationErrorService: FormValidationErrorsService,
|
protected formValidationErrorService: FormValidationErrorsService,
|
||||||
protected formBuilder: UntypedFormBuilder,
|
protected formBuilder: UntypedFormBuilder,
|
||||||
@@ -290,10 +294,8 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
|
|||||||
kdfConfig.iterations,
|
kdfConfig.iterations,
|
||||||
);
|
);
|
||||||
request.keys = new KeysRequest(keys[0], keys[1].encryptedString);
|
request.keys = new KeysRequest(keys[0], keys[1].encryptedString);
|
||||||
const orgInvite = await this.stateService.getOrganizationInvitation();
|
if (this.modifyRegisterRequest) {
|
||||||
if (orgInvite != null && orgInvite.token != null && orgInvite.organizationUserId != null) {
|
await this.modifyRegisterRequest(request);
|
||||||
request.token = orgInvite.token;
|
|
||||||
request.organizationUserId = orgInvite.organizationUserId;
|
|
||||||
}
|
}
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,10 +72,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async cancel() {
|
async cancel() {
|
||||||
await this.stateService.setOrganizationInvitation(null);
|
await this.router.navigate(["/vault"]);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.router.navigate(["/vault"]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setupSubmitActions(): Promise<boolean> {
|
async setupSubmitActions(): Promise<boolean> {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ListResponse } from "../../../models/response/list.response";
|
import { ListResponse } from "../../../models/response/list.response";
|
||||||
import { PolicyType } from "../../enums";
|
import { PolicyType } from "../../enums";
|
||||||
import { MasterPasswordPolicyOptions } from "../../models/domain/master-password-policy-options";
|
import { MasterPasswordPolicyOptions } from "../../models/domain/master-password-policy-options";
|
||||||
|
import { Policy } from "../../models/domain/policy";
|
||||||
import { PolicyRequest } from "../../models/request/policy.request";
|
import { PolicyRequest } from "../../models/request/policy.request";
|
||||||
import { PolicyResponse } from "../../models/response/policy.response";
|
import { PolicyResponse } from "../../models/response/policy.response";
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ export class PolicyApiServiceAbstraction {
|
|||||||
token: string,
|
token: string,
|
||||||
email: string,
|
email: string,
|
||||||
organizationUserId: string,
|
organizationUserId: string,
|
||||||
) => Promise<ListResponse<PolicyResponse>>;
|
) => Promise<Policy[] | undefined>;
|
||||||
|
|
||||||
getMasterPasswordPolicyOptsForOrgUser: (orgId: string) => Promise<MasterPasswordPolicyOptions>;
|
getMasterPasswordPolicyOptsForOrgUser: (orgId: string) => Promise<MasterPasswordPolicyOptions>;
|
||||||
putPolicy: (organizationId: string, type: PolicyType, request: PolicyRequest) => Promise<any>;
|
putPolicy: (organizationId: string, type: PolicyType, request: PolicyRequest) => Promise<any>;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { PolicyId } from "../../../types/guid";
|
import { PolicyId } from "../../../types/guid";
|
||||||
import { PolicyType } from "../../enums";
|
import { PolicyType } from "../../enums";
|
||||||
|
import { Policy } from "../domain/policy";
|
||||||
import { PolicyResponse } from "../response/policy.response";
|
import { PolicyResponse } from "../response/policy.response";
|
||||||
|
|
||||||
export class PolicyData {
|
export class PolicyData {
|
||||||
@@ -20,4 +21,8 @@ export class PolicyData {
|
|||||||
this.data = response.data;
|
this.data = response.data;
|
||||||
this.enabled = response.enabled;
|
this.enabled = response.enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromPolicy(policy: Policy): PolicyData {
|
||||||
|
return Object.assign(new PolicyData(), policy);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export class PolicyApiService implements PolicyApiServiceAbstraction {
|
|||||||
token: string,
|
token: string,
|
||||||
email: string,
|
email: string,
|
||||||
organizationUserId: string,
|
organizationUserId: string,
|
||||||
): Promise<ListResponse<PolicyResponse>> {
|
): Promise<Policy[] | undefined> {
|
||||||
const r = await this.apiService.send(
|
const r = await this.apiService.send(
|
||||||
"GET",
|
"GET",
|
||||||
"/organizations/" +
|
"/organizations/" +
|
||||||
@@ -63,7 +63,7 @@ export class PolicyApiService implements PolicyApiServiceAbstraction {
|
|||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
return new ListResponse(r, PolicyResponse);
|
return Policy.fromListResponse(new ListResponse(r, PolicyResponse));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getMasterPasswordPolicyResponseForOrgUser(
|
private async getMasterPasswordPolicyResponseForOrgUser(
|
||||||
|
|||||||
@@ -3,11 +3,15 @@ import { UserKey } from "../../types/key";
|
|||||||
export abstract class PasswordResetEnrollmentServiceAbstraction {
|
export abstract class PasswordResetEnrollmentServiceAbstraction {
|
||||||
/*
|
/*
|
||||||
* Checks the user's enrollment status and enrolls them if required
|
* Checks the user's enrollment status and enrolls them if required
|
||||||
|
* NOTE: Will also enroll the user in the organization if in the
|
||||||
|
* invited status
|
||||||
*/
|
*/
|
||||||
abstract enrollIfRequired(organizationSsoIdentifier: string): Promise<void>;
|
abstract enrollIfRequired(organizationSsoIdentifier: string): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enroll current user in password reset
|
* Enroll current user in password reset
|
||||||
|
* NOTE: Will also enroll the user in the organization if in the
|
||||||
|
* invited status
|
||||||
* @param organizationId - Organization in which to enroll the user
|
* @param organizationId - Organization in which to enroll the user
|
||||||
* @returns Promise that resolves when the user is enrolled
|
* @returns Promise that resolves when the user is enrolled
|
||||||
* @throws Error if the action fails
|
* @throws Error if the action fails
|
||||||
@@ -16,6 +20,8 @@ export abstract class PasswordResetEnrollmentServiceAbstraction {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Enroll user in password reset
|
* Enroll user in password reset
|
||||||
|
* NOTE: Will also enroll the user in the organization if in the
|
||||||
|
* invited status
|
||||||
* @param organizationId - Organization in which to enroll the user
|
* @param organizationId - Organization in which to enroll the user
|
||||||
* @param userId - User to enroll
|
* @param userId - User to enroll
|
||||||
* @param userKey - User's symmetric key
|
* @param userKey - User's symmetric key
|
||||||
|
|||||||
@@ -101,8 +101,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
setLastSync: (value: string, options?: StorageOptions) => Promise<void>;
|
setLastSync: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>;
|
getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>;
|
||||||
setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>;
|
setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||||
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;
|
|
||||||
setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise<void>;
|
|
||||||
getPasswordGenerationOptions: (options?: StorageOptions) => Promise<PasswordGeneratorOptions>;
|
getPasswordGenerationOptions: (options?: StorageOptions) => Promise<PasswordGeneratorOptions>;
|
||||||
setPasswordGenerationOptions: (
|
setPasswordGenerationOptions: (
|
||||||
value: PasswordGeneratorOptions,
|
value: PasswordGeneratorOptions,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
export class GlobalState {
|
export class GlobalState {
|
||||||
organizationInvitation?: any;
|
|
||||||
enableBrowserIntegration?: boolean;
|
enableBrowserIntegration?: boolean;
|
||||||
enableBrowserIntegrationFingerprint?: boolean;
|
enableBrowserIntegrationFingerprint?: boolean;
|
||||||
enableDuckDuckGoBrowserIntegration?: boolean;
|
enableDuckDuckGoBrowserIntegration?: boolean;
|
||||||
|
|||||||
@@ -474,23 +474,6 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrganizationInvitation(options?: StorageOptions): Promise<any> {
|
|
||||||
return (
|
|
||||||
await this.getGlobals(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
|
||||||
)?.organizationInvitation;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setOrganizationInvitation(value: any, options?: StorageOptions): Promise<void> {
|
|
||||||
const globals = await this.getGlobals(
|
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
|
||||||
);
|
|
||||||
globals.organizationInvitation = value;
|
|
||||||
await this.saveGlobals(
|
|
||||||
globals,
|
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPasswordGenerationOptions(options?: StorageOptions): Promise<PasswordGeneratorOptions> {
|
async getPasswordGenerationOptions(options?: StorageOptions): Promise<PasswordGeneratorOptions> {
|
||||||
return (
|
return (
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", {
|
|||||||
export const TOKEN_MEMORY = new StateDefinition("token", "memory");
|
export const TOKEN_MEMORY = new StateDefinition("token", "memory");
|
||||||
export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory");
|
export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory");
|
||||||
export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk");
|
export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk");
|
||||||
|
export const ORGANIZATION_INVITE_DISK = new StateDefinition("organizationInvite", "disk");
|
||||||
export const VAULT_TIMEOUT_SETTINGS_DISK_LOCAL = new StateDefinition(
|
export const VAULT_TIMEOUT_SETTINGS_DISK_LOCAL = new StateDefinition(
|
||||||
"vaultTimeoutSettings",
|
"vaultTimeoutSettings",
|
||||||
"disk",
|
"disk",
|
||||||
|
|||||||
Reference in New Issue
Block a user