[Reset Password] Admin Actions (#935)

* [Reset Password] Admin Actions

* Updated components to pass orgUser.Id and use within password reset apis

* Removed password auto-generation, fixed loading visual bug by chaining promise actions

* Update jslib 97ece68 -> 73ec484

* Updated all classes to new reset password flows

* Update jslib (73ec484 -> 5f1ad85)

* Update jslib (5f1ad85 -> 395ded0)

* Update encryption steps for change-password flow

* Fixed merge conflicts

* Updated based on requested changes
This commit is contained in:
Vincent Salucci 2021-06-02 11:35:49 -05:00 committed by GitHub
parent 65b52617a8
commit 1bacc8b774
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 655 additions and 43 deletions

View File

@ -2,6 +2,7 @@ import {
Component,
OnInit,
} from '@angular/core';
import {
ActivatedRoute,
Router,
@ -13,11 +14,18 @@ import {
} from 'angular2-toaster';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PolicyService } from 'jslib/abstractions/policy.service';
import { StateService } from 'jslib/abstractions/state.service';
import { UserService } from 'jslib/abstractions/user.service';
import { Utils } from 'jslib/misc/utils';
import { Policy } from 'jslib/models/domain/policy';
import { OrganizationUserAcceptRequest } from 'jslib/models/request/organizationUserAcceptRequest';
import { OrganizationUserResetPasswordEnrollmentRequest } from 'jslib/models/request/organizationUserResetPasswordEnrollmentRequest';
@Component({
selector: 'app-accept-organization',
@ -33,7 +41,8 @@ export class AcceptOrganizationComponent implements OnInit {
constructor(private router: Router, private toasterService: ToasterService,
private i18nService: I18nService, private route: ActivatedRoute,
private apiService: ApiService, private userService: UserService,
private stateService: StateService) { }
private stateService: StateService, private cryptoService: CryptoService,
private policyService: PolicyService) { }
ngOnInit() {
let fired = false;
@ -51,8 +60,36 @@ export class AcceptOrganizationComponent implements OnInit {
const request = new OrganizationUserAcceptRequest();
request.token = qParams.token;
try {
this.actionPromise = this.apiService.postOrganizationUserAccept(qParams.organizationId,
qParams.organizationUserId, request);
if (await this.performResetPasswordAutoEnroll(qParams)) {
this.actionPromise = this.apiService.postOrganizationUserAccept(qParams.organizationId,
qParams.organizationUserId, request).then(() => {
// Retrieve Public Key
return this.apiService.getOrganizationKeys(qParams.organizationId);
}).then(async response => {
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 encKey = await this.cryptoService.getEncKey();
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
// Create request and execute enrollment
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
resetRequest.resetPasswordKey = encryptedKey.encryptedString;
// Get User Id
const userId = await this.userService.getUserId();
return this.apiService.putOrganizationUserResetPasswordEnrollment(qParams.organizationId, userId, resetRequest);
});
} else {
this.actionPromise = this.apiService.postOrganizationUserAccept(qParams.organizationId,
qParams.organizationUserId, request);
}
await this.actionPromise;
const toast: Toast = {
type: 'success',
@ -92,4 +129,21 @@ export class AcceptOrganizationComponent implements OnInit {
this.loading = false;
});
}
private async performResetPasswordAutoEnroll(qParams: any): Promise<boolean> {
let policyList: Policy[] = null;
try {
const policies = await this.apiService.getPoliciesByToken(qParams.organizationId, qParams.token,
qParams.email, qParams.organizationUserId);
policyList = this.policyService.mapPoliciesFromToken(policies);
} catch { }
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;
}
}

View File

@ -5,6 +5,10 @@
<p class="lead text-center mx-4 mb-4">{{'loginOrCreateNewAccount' | i18n}}</p>
<div class="card d-block">
<div class="card-body">
<app-callout type="warning" title="{{'resetPasswordPolicyAutoEnroll' | i18n}}"
*ngIf="showResetPasswordAutoEnrollWarning">
{{'resetPasswordAutoEnrollInviteWarning' | i18n}}
</app-callout>
<div class="form-group">
<label for="email">{{'emailAddress' | i18n}}</label>
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required

View File

@ -4,27 +4,35 @@ import {
Router,
} from '@angular/router';
import { ApiService } from 'jslib/abstractions/api.service';
import { AuthService } from 'jslib/abstractions/auth.service';
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { PolicyService } from 'jslib/abstractions/policy.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { LoginComponent as BaseLoginComponent } from 'jslib/angular/components/login.component';
import { Policy } from 'jslib/models/domain/policy';
@Component({
selector: 'app-login',
templateUrl: 'login.component.html',
})
export class LoginComponent extends BaseLoginComponent {
showResetPasswordAutoEnrollWarning = false;
constructor(authService: AuthService, router: Router,
i18nService: I18nService, private route: ActivatedRoute,
storageService: StorageService, stateService: StateService,
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationService, cryptoFunctionService: CryptoFunctionService) {
passwordGenerationService: PasswordGenerationService, cryptoFunctionService: CryptoFunctionService,
private apiService: ApiService, private policyService: PolicyService) {
super(authService, router,
platformUtilsService, i18nService,
stateService, environmentService,
@ -49,6 +57,22 @@ export class LoginComponent extends BaseLoginComponent {
queryParamsSub.unsubscribe();
}
});
const invite = await this.stateService.get<any>('orgInvitation');
if (invite != null) {
let policyList: Policy[] = null;
try {
const policies = await this.apiService.getPoliciesByToken(invite.organizationId, invite.token,
invite.email, invite.organizationUserId);
policyList = this.policyService.mapPoliciesFromToken(policies);
} catch { }
if (policyList != null) {
const result = this.policyService.getResetPasswordPolicyOptions(policyList, invite.organizationId);
// Set to true if policy enabled and auto-enroll enabled
this.showResetPasswordAutoEnrollWarning = result[1] && result[0].autoEnrollEnabled;
}
}
}
async goAfterLogIn() {

View File

@ -200,12 +200,12 @@ const routes: Routes = [
{
path: '',
component: EmergencyAccessComponent,
data: { titleId: 'emergencyAccess'},
data: { titleId: 'emergencyAccess' },
},
{
path: ':id',
component: EmergencyAccessViewComponent,
data: { titleId: 'emergencyAccess'},
data: { titleId: 'emergencyAccess' },
},
],
},
@ -390,7 +390,7 @@ const routes: Routes = [
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'people',
permissions: [Permissions.ManageUsers],
permissions: [Permissions.ManageUsers, Permissions.ManageUsersPassword],
},
},
{

View File

@ -52,6 +52,7 @@ import { ManageComponent as OrgManageComponent } from './organizations/manage/ma
import { PeopleComponent as OrgPeopleComponent } from './organizations/manage/people.component';
import { PoliciesComponent as OrgPoliciesComponent } from './organizations/manage/policies.component';
import { PolicyEditComponent as OrgPolicyEditComponent } from './organizations/manage/policy-edit.component';
import { ResetPasswordComponent as OrgResetPasswordComponent } from './organizations/manage/reset-password.component';
import { UserAddEditComponent as OrgUserAddEditComponent } from './organizations/manage/user-add-edit.component';
import { UserConfirmComponent as OrgUserConfirmComponent } from './organizations/manage/user-confirm.component';
import { UserGroupsComponent as OrgUserGroupsComponent } from './organizations/manage/user-groups.component';
@ -368,6 +369,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
OrgPeopleComponent,
OrgPolicyEditComponent,
OrgPoliciesComponent,
OrgResetPasswordComponent,
OrgReusedPasswordsReportComponent,
OrgSettingComponent,
OrgToolsComponent,
@ -456,6 +458,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
OrgEntityUsersComponent,
OrgGroupAddEditComponent,
OrgPolicyEditComponent,
OrgResetPasswordComponent,
OrgUserAddEditComponent,
OrgUserConfirmComponent,
OrgUserGroupsComponent,

View File

@ -35,7 +35,8 @@
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
{{'reinviteSelected' | i18n}}
</button>
<button class="dropdown-item text-success" appStopClick (click)="bulkConfirm()" *ngIf="showConfirmUsers">
<button class="dropdown-item text-success" appStopClick (click)="bulkConfirm()"
*ngIf="showConfirmUsers">
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
{{'confirmSelected' | i18n}}
</button>
@ -53,7 +54,7 @@
{{'unselectAll' | i18n}}
</button>
</div>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="invite()">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{'inviteUser' | i18n}}
@ -95,6 +96,10 @@
<i class="fa fa-lock" title="{{'userUsingTwoStep' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'userUsingTwoStep' | i18n}}</span>
</ng-container>
<ng-container *ngIf="showEnrolledStatus(u)">
<i class="fa fa-key" title="{{'enrolledPasswordReset' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'enrolledPasswordReset' | i18n}}</span>
</ng-container>
</td>
<td>
<span *ngIf="u.type === organizationUserType.Owner">{{'owner' | i18n}}</span>
@ -130,6 +135,11 @@
<i class="fa fa-fw fa-file-text-o" aria-hidden="true"></i>
{{'eventLogs' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick (click)="resetPassword(u)"
*ngIf="allowResetPassword(u)">
<i class="fa fa-fw fa-key" aria-hidden="true"></i>
{{'resetPassword' | i18n}}
</a>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
{{'remove' | i18n}}
@ -146,4 +156,5 @@
<ng-template #groupsTemplate></ng-template>
<ng-template #eventsTemplate></ng-template>
<ng-template #confirmTemplate></ng-template>
<ng-template #resetPasswordTemplate></ng-template>
<ng-template #bulkStatusTemplate></ng-template>

View File

@ -5,6 +5,7 @@ import {
ViewChild,
ViewContainerRef,
} from '@angular/core';
import {
ActivatedRoute,
Router,
@ -13,32 +14,38 @@ import {
import { ToasterService } from 'angular2-toaster';
import { ValidationService } from 'jslib/angular/services/validation.service';
import { ConstantsService } from 'jslib/services/constants.service';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { PolicyService } from 'jslib/abstractions/policy.service';
import { SearchService } from 'jslib/abstractions/search.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { UserService } from 'jslib/abstractions/user.service';
import { OrganizationKeysRequest } from 'jslib/models/request/organizationKeysRequest';
import { OrganizationUserBulkRequest } from 'jslib/models/request/organizationUserBulkRequest';
import { OrganizationUserBulkConfirmRequest } from 'jslib/models/request/organizationUserBulkConfirmRequest';
import { OrganizationUserConfirmRequest } from 'jslib/models/request/organizationUserConfirmRequest';
import { OrganizationUserBulkRequest } from 'jslib/models/request/organizationUserBulkRequest';
import { OrganizationUserUserDetailsResponse } from 'jslib/models/response/organizationUserResponse';
import { OrganizationUserStatusType } from 'jslib/enums/organizationUserStatusType';
import { OrganizationUserType } from 'jslib/enums/organizationUserType';
import { PolicyType } from 'jslib/enums/policyType';
import { Utils } from 'jslib/misc/utils';
import { ListResponse } from 'jslib/models/response';
import { OrganizationUserBulkResponse } from 'jslib/models/response/organizationUserBulkResponse';
import { ModalComponent } from '../../modal.component';
import { BulkStatusComponent } from './bulk-status.component';
import { EntityEventsComponent } from './entity-events.component';
import { ResetPasswordComponent } from './reset-password.component';
import { UserAddEditComponent } from './user-add-edit.component';
import { UserConfirmComponent } from './user-confirm.component';
import { UserGroupsComponent } from './user-groups.component';
@ -54,6 +61,7 @@ export class PeopleComponent implements OnInit {
@ViewChild('groupsTemplate', { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef;
@ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef;
@ViewChild('confirmTemplate', { read: ViewContainerRef, static: true }) confirmModalRef: ViewContainerRef;
@ViewChild('resetPasswordTemplate', { read: ViewContainerRef, static: true }) resetPasswordModalRef: ViewContainerRef;
@ViewChild('bulkStatusTemplate', { read: ViewContainerRef, static: true }) bulkStatusModalRef: ViewContainerRef;
loading = true;
@ -68,6 +76,11 @@ export class PeopleComponent implements OnInit {
actionPromise: Promise<any>;
accessEvents = false;
accessGroups = false;
canResetPassword = false; // User permission (admin/custom)
orgUseResetPassword = false; // Org plan ability
orgHasKeys = false; // Org public/private keys
orgResetPasswordPolicyEnabled = false;
callingUserType: OrganizationUserType = null;
protected didScroll = false;
protected pageSize = 100;
@ -81,7 +94,7 @@ export class PeopleComponent implements OnInit {
private platformUtilsService: PlatformUtilsService, private toasterService: ToasterService,
private cryptoService: CryptoService, private userService: UserService, private router: Router,
private storageService: StorageService, private searchService: SearchService,
private validationService: ValidationService) { }
private validationService: ValidationService, private policyService: PolicyService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
@ -93,6 +106,25 @@ export class PeopleComponent implements OnInit {
}
this.accessEvents = organization.useEvents;
this.accessGroups = organization.useGroups;
this.canResetPassword = organization.canManageUsersPassword;
this.orgUseResetPassword = organization.useResetPassword;
this.callingUserType = organization.type;
// Backfill pub/priv key if necessary
if (!organization.hasPublicAndPrivateKeys) {
const orgShareKey = await this.cryptoService.getOrgKey(this.organizationId);
const orgKeys = await this.cryptoService.makeKeyPair(orgShareKey);
const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
const response = await this.apiService.postOrganizationKeys(this.organizationId, request);
if (response != null) {
this.orgHasKeys = response.publicKey != null && response.privateKey != null;
} else {
throw new Error(this.i18nService.t('resetPasswordOrgKeysError'));
}
} else {
this.orgHasKeys = true;
}
await this.load();
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
@ -123,9 +155,38 @@ export class PeopleComponent implements OnInit {
}
});
this.filter(this.status);
const policies = await this.policyService.getAll(PolicyType.ResetPassword);
this.orgResetPasswordPolicyEnabled = policies.some(p => p.organizationId === this.organizationId && p.enabled);
this.loading = false;
}
allowResetPassword(orgUser: OrganizationUserUserDetailsResponse): boolean {
// Hierarchy check
let callingUserHasPermission = false;
switch (this.callingUserType) {
case OrganizationUserType.Owner:
callingUserHasPermission = true;
break;
case OrganizationUserType.Admin:
callingUserHasPermission = orgUser.type !== OrganizationUserType.Owner;
break;
case OrganizationUserType.Custom:
callingUserHasPermission = orgUser.type !== OrganizationUserType.Owner
&& orgUser.type !== OrganizationUserType.Admin;
break;
}
// Final
return this.canResetPassword && callingUserHasPermission && this.orgUseResetPassword && this.orgHasKeys
&& orgUser.resetPasswordEnrolled && this.orgResetPasswordPolicyEnabled
&& orgUser.status === OrganizationUserStatusType.Confirmed;
}
showEnrolledStatus(orgUser: OrganizationUserUserDetailsResponse): boolean {
return this.orgUseResetPassword && orgUser.resetPasswordEnrolled && this.orgResetPasswordPolicyEnabled;
}
filter(status: OrganizationUserStatusType) {
this.status = status;
if (this.status != null) {
@ -454,6 +515,31 @@ export class PeopleComponent implements OnInit {
return !searching && this.users && this.users.length > this.pageSize;
}
async resetPassword(user: OrganizationUserUserDetailsResponse) {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.resetPasswordModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<ResetPasswordComponent>(
ResetPasswordComponent, this.resetPasswordModalRef);
childComponent.name = user != null ? user.name || user.email : null;
childComponent.email = user != null ? user.email : null;
childComponent.organizationId = this.organizationId;
childComponent.id = user != null ? user.id : null;
childComponent.onPasswordReset.subscribe(() => {
this.modal.close();
this.load();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
checkUser(user: OrganizationUserUserDetailsResponse, select?: boolean) {
(user as any).checked = select == null ? !(user as any).checked : select;
}
@ -496,8 +582,8 @@ export class PeopleComponent implements OnInit {
const response = await request;
if (this.modal) {
const keyedErrors: any = response.data.filter(r => r.error !== '').reduce((a, x) => ({...a, [x.id]: x.error}), {});
const keyedFilteredUsers: any = filteredUsers.reduce((a, x) => ({...a, [x.id]: x}), {});
const keyedErrors: any = response.data.filter(r => r.error !== '').reduce((a, x) => ({ ...a, [x.id]: x.error }), {});
const keyedFilteredUsers: any = filteredUsers.reduce((a, x) => ({ ...a, [x.id]: x }), {});
childComponent.users = users.map(user => {
let message = keyedErrors[user.id] ?? successfullMessage;

View File

@ -115,6 +115,12 @@ export class PoliciesComponent implements OnInit {
type: PolicyType.SendOptions,
enabled: false,
display: true,
}, {
name: this.i18nService.t('resetPasswordPolicy'),
description: this.i18nService.t('resetPasswordPolicyDescription'),
type: PolicyType.ResetPassword,
enabled: false,
display: organization.useResetPassword,
},
];
await this.load();

View File

@ -38,6 +38,9 @@
<app-callout type="warning" *ngIf="type === policyType.SendOptions">
{{'sendOptionsExemption' | i18n}}
</app-callout>
<app-callout type="warning" *ngIf="type === policyType.ResetPassword">
{{'resetPasswordPolicyWarning' | i18n}}
</app-callout>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enabled" [(ngModel)]="enabled"
@ -153,19 +156,33 @@
<ng-container *ngIf="type === policyType.SendOptions">
<h3 class="mt-4">{{'options' | i18n}}</h3>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="sendDisableHideEmail" [(ngModel)]="sendDisableHideEmail"
name="SendDisableHideEmail">
<input class="form-check-input" type="checkbox" id="sendDisableHideEmail"
[(ngModel)]="sendDisableHideEmail" name="SendDisableHideEmail">
<label class="form-check-label" for="sendDisableHideEmail">{{'disableHideEmail' | i18n}}</label>
</div>
</ng-container>
<ng-container *ngIf="type === policyType.ResetPassword">
<h3 class="mt-4">{{'resetPasswordPolicyAutoEnroll' | i18n}}</h3>
<p>{{'resetPasswordPolicyAutoEnrollDescription' | i18n}}</p>
<app-callout type="warning">
{{'resetPasswordPolicyAutoEnrollWarning' | i18n}}
</app-callout>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="autoEnrollEnabled"
[(ngModel)]="resetPasswordAutoEnroll" name="AutoEnrollEnabled">
<label class="form-check-label"
for="autoEnrollEnabled">{{'resetPasswordPolicyAutoEnrollCheckbox' | i18n }}</label>
</div>
</ng-container>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'save' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'cancel' | i18n}}</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{'cancel' | i18n}}
</button>
</div>
</form>
</div>

View File

@ -60,6 +60,9 @@ export class PolicyEditComponent implements OnInit {
// Send options
sendDisableHideEmail?: boolean;
// Reset Password
resetPasswordAutoEnroll?: boolean;
private policy: PolicyResponse;
constructor(private apiService: ApiService, private i18nService: I18nService,
@ -116,6 +119,9 @@ export class PolicyEditComponent implements OnInit {
case PolicyType.SendOptions:
this.sendDisableHideEmail = this.policy.data.disableHideEmail;
break;
case PolicyType.ResetPassword:
this.resetPasswordAutoEnroll = this.policy.data.autoEnrollEnabled;
break;
default:
break;
}
@ -167,6 +173,11 @@ export class PolicyEditComponent implements OnInit {
disableHideEmail: this.sendDisableHideEmail,
};
break;
case PolicyType.ResetPassword:
request.data = {
autoEnrollEnabled: this.resetPasswordAutoEnroll,
};
break;
default:
break;
}

View File

@ -0,0 +1,78 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="resetPasswordTitle">
<div class="modal-dialog" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h2 class="modal-title" id="resetPasswordTitle">
{{'resetPassword' | i18n}}
<small class="text-muted" *ngIf="name">{{name}}</small>
</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<app-callout type="warning">{{'resetPasswordLoggedOutWarning' | i18n: loggedOutWarningName}}
</app-callout>
<app-callout type="info" *ngIf="enforcedPolicyOptions">
{{'resetPasswordMasterPasswordPolicyInEffect' | i18n}}
<ul class="mb-0">
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
{{'policyInEffectMinComplexity' | i18n : getPasswordScoreAlertDisplay()}}
</li>
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
{{'policyInEffectMinLength' | i18n : enforcedPolicyOptions?.minLength.toString()}}
</li>
<li *ngIf="enforcedPolicyOptions?.requireUpper">
{{'policyInEffectUppercase' | i18n}}</li>
<li *ngIf="enforcedPolicyOptions?.requireLower">
{{'policyInEffectLowercase' | i18n}}</li>
<li *ngIf="enforcedPolicyOptions?.requireNumbers">
{{'policyInEffectNumbers' | i18n}}</li>
<li *ngIf="enforcedPolicyOptions?.requireSpecial">
{{'policyInEffectSpecial' | i18n : '!@#$%^&*'}}</li>
</ul>
</app-callout>
<div class="row">
<div class="col form-group">
<div class="d-flex">
<label for="newPassword">{{'newPassword' | i18n}}</label>
<div class="ml-auto d-flex">
<a href="#" class="d-block mr-2 fa-icon-above-input" appStopClick
appA11yTitle="{{'generatePassword' | i18n}}" (click)="generatePassword()">
<i class="fa fa-lg fa-fw fa-refresh" aria-hidden="true"></i>
</a>
</div>
</div>
<div class="input-group mb-1">
<input id="newPassword" class="form-control text-monospace" appAutofocus
type="{{showPassword ? 'text' : 'password'}}" name="NewPassword"
[(ngModel)]="newPassword" required appInputVerbatim autocomplete="new-password"
(input)="updatePasswordStrength()">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword()">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'copyPassword' | i18n}}" (click)="copy(newPassword)">
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
<app-password-strength [score]="masterPasswordScore" [showText]="true">
</app-password-strength>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'save' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'cancel' |
i18n}}</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,191 @@
import {
AfterViewInit,
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { PolicyService } from 'jslib/abstractions/policy.service';
import { EncString } from 'jslib/models/domain/encString';
import { MasterPasswordPolicyOptions } from 'jslib/models/domain/masterPasswordPolicyOptions';
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
import { OrganizationUserResetPasswordRequest } from 'jslib/models/request/organizationUserResetPasswordRequest';
@Component({
selector: 'app-reset-password',
templateUrl: 'reset-password.component.html',
})
export class ResetPasswordComponent implements OnInit {
@Input() name: string;
@Input() email: string;
@Input() id: string;
@Input() organizationId: string;
@Output() onPasswordReset = new EventEmitter();
enforcedPolicyOptions: MasterPasswordPolicyOptions;
newPassword: string = null;
showPassword: boolean = false;
masterPasswordScore: number;
formPromise: Promise<any>;
private newPasswordStrengthTimeout: any;
constructor(private apiService: ApiService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private passwordGenerationService: PasswordGenerationService,
private policyService: PolicyService, private cryptoService: CryptoService) { }
async ngOnInit() {
// Get Enforced Policy Options
this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions();
}
get loggedOutWarningName() {
return this.name != null ? this.name : this.i18nService.t('thisUser');
}
getPasswordScoreAlertDisplay() {
if (this.enforcedPolicyOptions == null) {
return '';
}
let str: string;
switch (this.enforcedPolicyOptions.minComplexity) {
case 4:
str = this.i18nService.t('strong');
break;
case 3:
str = this.i18nService.t('good');
break;
default:
str = this.i18nService.t('weak');
break;
}
return str + ' (' + this.enforcedPolicyOptions.minComplexity + ')';
}
async generatePassword() {
const options = (await this.passwordGenerationService.getOptions())[0];
this.newPassword = await this.passwordGenerationService.generatePassword(options);
this.updatePasswordStrength();
}
togglePassword() {
this.showPassword = !this.showPassword;
document.getElementById('newPassword').focus();
}
copy(value: string) {
if (value == null) {
return;
}
this.platformUtilsService.copyToClipboard(value, { window: window });
this.platformUtilsService.showToast('info', null,
this.i18nService.t('valueCopied', this.i18nService.t('password')));
}
async submit() {
// Validation
if (this.newPassword == null || this.newPassword === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassRequired'));
return false;
}
if (this.newPassword.length < 8) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassLength'));
return false;
}
if (this.enforcedPolicyOptions != null &&
!this.policyService.evaluateMasterPassword(this.masterPasswordScore, this.newPassword,
this.enforcedPolicyOptions)) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPasswordPolicyRequirementsNotMet'));
return;
}
if (this.masterPasswordScore < 3) {
const result = await this.platformUtilsService.showDialog(this.i18nService.t('weakMasterPasswordDesc'),
this.i18nService.t('weakMasterPassword'), this.i18nService.t('yes'), this.i18nService.t('no'),
'warning');
if (!result) {
return false;
}
}
// Get user Information (kdf type, kdf iterations, resetPasswordKey, private key) and change password
try {
this.formPromise = this.apiService.getOrganizationUserResetPasswordDetails(this.organizationId, this.id)
.then(async response => {
if (response == null) {
throw new Error(this.i18nService.t('resetPasswordDetailsError'));
}
const kdfType = response.kdf;
const kdfIterations = response.kdfIterations;
const resetPasswordKey = response.resetPasswordKey;
const encryptedPrivateKey = response.encryptedPrivateKey;
// Decrypt Organization's encrypted Private Key with org key
const orgSymKey = await this.cryptoService.getOrgKey(this.organizationId);
const decPrivateKey = await this.cryptoService.decryptToBytes(new EncString(encryptedPrivateKey), orgSymKey);
// Decrypt User's Reset Password Key to get EncKey
const decValue = await this.cryptoService.rsaDecrypt(resetPasswordKey, decPrivateKey);
const userEncKey = new SymmetricCryptoKey(decValue);
// Create new key and hash new password
const newKey = await this.cryptoService.makeKey(this.newPassword, this.email.trim().toLowerCase(),
kdfType, kdfIterations);
const newPasswordHash = await this.cryptoService.hashPassword(this.newPassword, newKey);
// Create new encKey for the User
const newEncKey = await this.cryptoService.remakeEncKey(newKey, userEncKey);
// Create request
const request = new OrganizationUserResetPasswordRequest();
request.key = newEncKey[1].encryptedString;
request.newMasterPasswordHash = newPasswordHash;
// Change user's password
return this.apiService.putOrganizationUserResetPassword(this.organizationId, this.id, request);
});
await this.formPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('resetPasswordSuccess'));
this.onPasswordReset.emit();
} catch { }
}
updatePasswordStrength() {
if (this.newPasswordStrengthTimeout != null) {
clearTimeout(this.newPasswordStrengthTimeout);
}
this.newPasswordStrengthTimeout = setTimeout(() => {
const strengthResult = this.passwordGenerationService.passwordStrength(this.newPassword,
this.getPasswordStrengthUserInput());
this.masterPasswordScore = strengthResult == null ? null : strengthResult.score;
}, 300);
}
private getPasswordStrengthUserInput() {
let userInput: string[] = [];
const atPosition = this.email.indexOf('@');
if (atPosition > -1) {
userInput = userInput.concat(this.email.substr(0, atPosition).trim().toLowerCase().split(/[^A-Za-z0-9]/));
}
if (this.name != null && this.name !== '') {
userInput = userInput.concat(this.name.trim().toLowerCase().split(' '));
}
return userInput;
}
}

View File

@ -4,22 +4,28 @@ import {
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { OrganizationKeysRequest } from 'jslib/models/request/organizationKeysRequest';
import { OrganizationUpdateRequest } from 'jslib/models/request/organizationUpdateRequest';
import { OrganizationResponse } from 'jslib/models/response/organizationResponse';
import { ModalComponent } from '../../modal.component';
import { ApiKeyComponent } from '../../settings/api-key.component';
import { PurgeVaultComponent } from '../../settings/purge-vault.component';
import { TaxInfoComponent } from '../../settings/tax-info.component';
import { DeleteOrganizationComponent } from './delete-organization.component';
@Component({
@ -46,7 +52,8 @@ export class AccountComponent {
constructor(private componentFactoryResolver: ComponentFactoryResolver,
private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService, private route: ActivatedRoute,
private syncService: SyncService, private platformUtilsService: PlatformUtilsService) { }
private syncService: SyncService, private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService) { }
async ngOnInit() {
this.selfHosted = this.platformUtilsService.isSelfHost();
@ -67,6 +74,14 @@ export class AccountComponent {
request.businessName = this.org.businessName;
request.billingEmail = this.org.billingEmail;
request.identifier = this.org.identifier;
// Backfill pub/priv key if necessary
if (!this.org.hasPublicAndPrivateKeys) {
const orgShareKey = await this.cryptoService.getOrgKey(this.organizationId);
const orgKeys = await this.cryptoService.makeKeyPair(orgShareKey);
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
}
this.formPromise = this.apiService.putOrganization(this.organizationId, request).then(() => {
return this.syncService.fullSync(true);
});

View File

@ -27,7 +27,8 @@ export class OrganizationTypeGuardService implements CanActivate {
(permissions.indexOf(Permissions.ManageGroups) !== -1 && org.canManageGroups) ||
(permissions.indexOf(Permissions.ManageOrganization) !== -1 && org.isOwner) ||
(permissions.indexOf(Permissions.ManagePolicies) !== -1 && org.canManagePolicies) ||
(permissions.indexOf(Permissions.ManageUsers) !== -1 && org.canManageUsers)
(permissions.indexOf(Permissions.ManageUsers) !== -1 && org.canManageUsers) ||
(permissions.indexOf(Permissions.ManageUsersPassword) !== -1 && org.canManageUsersPassword)
) {
return true;
}

View File

@ -205,9 +205,12 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
continue;
}
// Re-enroll - encrpyt user's encKey.key with organization key
const orgSymKey = await this.cryptoService.getOrgKey(org.id);
const encryptedKey = await this.cryptoService.encrypt(encKey.key, orgSymKey);
// Retrieve public key
const response = await this.apiService.getOrganizationKeys(org.id);
const publicKey = Utils.fromB64ToArray(response?.publicKey);
// Re-enroll - encrpyt user's encKey.key with organization public key
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
// Create/Execute request
const request = new OrganizationUserResetPasswordEnrollmentRequest();

View File

@ -30,7 +30,9 @@ import { PolicyType } from 'jslib/enums/policyType';
import { ProductType } from 'jslib/enums/productType';
import { OrganizationCreateRequest } from 'jslib/models/request/organizationCreateRequest';
import { OrganizationKeysRequest } from 'jslib/models/request/organizationKeysRequest';
import { OrganizationUpgradeRequest } from 'jslib/models/request/organizationUpgradeRequest';
import { PlanResponse } from 'jslib/models/response/planResponse';
@Component({
@ -259,6 +261,7 @@ export class OrganizationPlansComponent implements OnInit {
const collection = await this.cryptoService.encrypt(
this.i18nService.t('defaultCollection'), shareKey[1]);
const collectionCt = collection.encryptedString;
const orgKeys = await this.cryptoService.makeKeyPair(shareKey[1]);
if (this.selfHosted) {
const fd = new FormData();
@ -267,12 +270,17 @@ export class OrganizationPlansComponent implements OnInit {
fd.append('collectionName', collectionCt);
const response = await this.apiService.postOrganizationLicense(fd);
orgId = response.id;
// Org Keys live outside of the OrganizationLicense - add the keys to the org here
const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
await this.apiService.postOrganizationKeys(orgId, request);
} else {
const request = new OrganizationCreateRequest();
request.key = key;
request.collectionName = collectionCt;
request.name = this.name;
request.billingEmail = this.billingEmail;
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
if (this.selectedPlan.type === PlanType.Free) {
request.planType = PlanType.Free;
@ -309,6 +317,14 @@ export class OrganizationPlansComponent implements OnInit {
request.billingAddressCountry = this.taxComponent.taxInfo.country;
request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode;
// Retrieve org info to backfill pub/priv key if necessary
const org = await this.userService.getOrganization(this.organizationId);
if (!org.hasPublicAndPrivateKeys) {
const orgShareKey = await this.cryptoService.getOrgKey(this.organizationId);
const orgKeys = await this.cryptoService.makeKeyPair(orgShareKey);
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
}
const result = await this.apiService.postOrganizationUpgrade(this.organizationId, request);
if (!result.success && result.paymentIntentClientSecret != null) {
await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null);

View File

@ -65,7 +65,7 @@
title="{{'organizationIsDisabled' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'organizationIsDisabled' | i18n}}</span>
</ng-container>
<ng-container *ngIf="o.isResetPasswordEnrolled">
<ng-container *ngIf="showEnrolledStatus(o)">
<i class="fa fa-key" appStopProp title="{{'enrolledPasswordReset' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'enrolledPasswordReset' | i18n}}</span>
@ -79,13 +79,13 @@
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a *ngIf="!o.isResetPasswordEnrolled && resetPasswordFeatureFlag" class="dropdown-item"
<a *ngIf="allowEnrollmentChanges(o) && !o.resetPasswordEnrolled" class="dropdown-item"
href="#" appStopClick (click)="toggleResetPasswordEnrollment(o)">
<i class="fa fa-fw fa-key" aria-hidden="true"></i>
{{'enrollPasswordReset' | i18n}}
</a>
<a *ngIf="o.isResetPasswordEnrolled" class="dropdown-item" href="#" appStopClick
(click)="toggleResetPasswordEnrollment(o)">
<a *ngIf="allowEnrollmentChanges(o) && o.resetPasswordEnrolled" class="dropdown-item"
href="#" appStopClick (click)="toggleResetPasswordEnrollment(o)">
<i class="fa fa-fw fa-undo" aria-hidden="true"></i>
{{'withdrawPasswordReset' | i18n}}
</a>

View File

@ -10,15 +10,19 @@ import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { PolicyService } from 'jslib/abstractions/policy.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { UserService } from 'jslib/abstractions/user.service';
import { Organization } from 'jslib/models/domain/organization';
import { Policy } from 'jslib/models/domain/policy';
import { Utils } from 'jslib/misc/utils';
import { OrganizationUserResetPasswordEnrollmentRequest } from 'jslib/models/request/organizationUserResetPasswordEnrollmentRequest';
import { PolicyType } from 'jslib/enums/policyType';
@Component({
selector: 'app-organizations',
templateUrl: 'organizations.component.html',
@ -27,15 +31,14 @@ export class OrganizationsComponent implements OnInit {
@Input() vault = false;
organizations: Organization[];
policies: Policy[];
loaded: boolean = false;
actionPromise: Promise<any>;
// TODO Remove feature flag once ready for general release
resetPasswordFeatureFlag = false;
constructor(private userService: UserService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private apiService: ApiService,
private toasterService: ToasterService, private syncService: SyncService,
private cryptoService: CryptoService) { }
private cryptoService: CryptoService, private policyService: PolicyService) { }
async ngOnInit() {
if (!this.vault) {
@ -48,9 +51,22 @@ export class OrganizationsComponent implements OnInit {
const orgs = await this.userService.getAllOrganizations();
orgs.sort(Utils.getSortFunction(this.i18nService, 'name'));
this.organizations = orgs;
this.policies = await this.policyService.getAll(PolicyType.ResetPassword);
this.loaded = true;
}
allowEnrollmentChanges(org: Organization): boolean {
if (org.usePolicies && org.useResetPassword && org.hasPublicAndPrivateKeys) {
return this.policies.some(p => p.organizationId === org.id && p.enabled);
}
return false;
}
showEnrolledStatus(org: Organization): boolean {
return org.useResetPassword && org.resetPasswordEnrolled && this.policies.some(p => p.organizationId === org.id && p.enabled);
}
async unlinkSso(org: Organization) {
const confirmed = await this.platformUtilsService.showDialog(
'Are you sure you want to unlink SSO for this organization?', org.name,
@ -88,32 +104,54 @@ export class OrganizationsComponent implements OnInit {
}
async toggleResetPasswordEnrollment(org: Organization) {
// Feature Flag
if (!this.resetPasswordFeatureFlag) {
return;
}
// Set variables
let keyString: string = null;
let toastStringRef = 'withdrawPasswordResetSuccess';
// Enroll - encrpyt user's encKey.key with organization key
// Enrolling
if (!org.resetPasswordEnrolled) {
const encKey = await this.cryptoService.getEncKey();
const orgSymKey = await this.cryptoService.getOrgKey(org.id);
const encryptedKey = await this.cryptoService.encrypt(encKey.key, orgSymKey);
keyString = encryptedKey.encryptedString;
toastStringRef = 'enrollPasswordResetSuccess';
}
// Alert user about enrollment
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('resetPasswordEnrollmentWarning'), null,
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return;
}
// Create/Execute request
try {
// Retrieve Public Key
this.actionPromise = this.apiService.getOrganizationKeys(org.id)
.then(async response => {
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 encKey = await this.cryptoService.getEncKey();
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
keyString = encryptedKey.encryptedString;
toastStringRef = 'enrollPasswordResetSuccess';
// Create request and execute enrollment
const request = new OrganizationUserResetPasswordEnrollmentRequest();
request.resetPasswordKey = keyString;
return this.apiService.putOrganizationUserResetPasswordEnrollment(org.id, org.userId, request);
})
.then(() => {
return this.syncService.fullSync(true);
});
} else {
// Withdrawal
const request = new OrganizationUserResetPasswordEnrollmentRequest();
request.resetPasswordKey = keyString;
this.actionPromise = this.apiService.putOrganizationUserResetPasswordEnrollment(org.id, org.userId, request)
.then(() => {
return this.syncService.fullSync(true);
});
}
try {
await this.actionPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t(toastStringRef));
await this.load();

View File

@ -3891,6 +3891,60 @@
}
}
},
"resetPassword": {
"message": "Reset Password"
},
"resetPasswordLoggedOutWarning": {
"message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.",
"placeholders": {
"name": {
"content": "$1",
"example": "John Smith"
}
}
},
"thisUser": {
"message": "this user"
},
"resetPasswordMasterPasswordPolicyInEffect": {
"message": "One or more organization policies require the master password to meet the following requirements:"
},
"resetPasswordSuccess": {
"message": "Password reset success!"
},
"resetPasswordEnrollmentWarning": {
"message": "Enrollment will allow organization administrators to change your master password. Are you sure you want to enroll?"
},
"resetPasswordPolicy": {
"message": "Master Password Reset"
},
"resetPasswordPolicyDescription": {
"message": "Allow administrators in the organization to reset organization users' master password."
},
"resetPasswordPolicyWarning": {
"message": "Users in the organization will need to self-enroll or be auto-enrolled before administrators can reset their master password."
},
"resetPasswordPolicyAutoEnroll": {
"message": "Automatic Enrollment"
},
"resetPasswordPolicyAutoEnrollDescription": {
"message": "All users will be automatically enrolled in password reset once their invite is accepted."
},
"resetPasswordPolicyAutoEnrollWarning": {
"message": "Users already in the organization will not be retroactively enrolled in password reset. They will need to self-enroll before administrators can reset their master password."
},
"resetPasswordPolicyAutoEnrollCheckbox": {
"message": "Automatically enroll new users"
},
"resetPasswordAutoEnrollInviteWarning": {
"message": "This organization has an enterprise policy that will automatically enroll you in password reset. Enrollment will allow organization administrators to change your master password."
},
"resetPasswordOrgKeysError": {
"message": "Organization Keys response is null"
},
"resetPasswordDetailsError": {
"message": "Reset Password Details response is null"
},
"trashCleanupWarning": {
"message": "Items that have been in Trash more than 30 days will be automatically deleted."
},