[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 jslib97ece68
->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:
parent
65b52617a8
commit
1bacc8b774
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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">×</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>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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."
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue