mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 08:43:33 +00:00
[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:
@@ -2,6 +2,7 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
OnInit,
|
OnInit,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ActivatedRoute,
|
ActivatedRoute,
|
||||||
Router,
|
Router,
|
||||||
@@ -13,11 +14,18 @@ import {
|
|||||||
} from 'angular2-toaster';
|
} from 'angular2-toaster';
|
||||||
|
|
||||||
import { ApiService } from 'jslib/abstractions/api.service';
|
import { ApiService } from 'jslib/abstractions/api.service';
|
||||||
|
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||||
|
import { PolicyService } from 'jslib/abstractions/policy.service';
|
||||||
import { StateService } from 'jslib/abstractions/state.service';
|
import { StateService } from 'jslib/abstractions/state.service';
|
||||||
import { UserService } from 'jslib/abstractions/user.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 { OrganizationUserAcceptRequest } from 'jslib/models/request/organizationUserAcceptRequest';
|
||||||
|
import { OrganizationUserResetPasswordEnrollmentRequest } from 'jslib/models/request/organizationUserResetPasswordEnrollmentRequest';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-accept-organization',
|
selector: 'app-accept-organization',
|
||||||
@@ -33,7 +41,8 @@ export class AcceptOrganizationComponent implements OnInit {
|
|||||||
constructor(private router: Router, private toasterService: ToasterService,
|
constructor(private router: Router, private toasterService: ToasterService,
|
||||||
private i18nService: I18nService, private route: ActivatedRoute,
|
private i18nService: I18nService, private route: ActivatedRoute,
|
||||||
private apiService: ApiService, private userService: UserService,
|
private apiService: ApiService, private userService: UserService,
|
||||||
private stateService: StateService) { }
|
private stateService: StateService, private cryptoService: CryptoService,
|
||||||
|
private policyService: PolicyService) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
let fired = false;
|
let fired = false;
|
||||||
@@ -51,8 +60,36 @@ export class AcceptOrganizationComponent implements OnInit {
|
|||||||
const request = new OrganizationUserAcceptRequest();
|
const request = new OrganizationUserAcceptRequest();
|
||||||
request.token = qParams.token;
|
request.token = qParams.token;
|
||||||
try {
|
try {
|
||||||
this.actionPromise = this.apiService.postOrganizationUserAccept(qParams.organizationId,
|
if (await this.performResetPasswordAutoEnroll(qParams)) {
|
||||||
qParams.organizationUserId, request);
|
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;
|
await this.actionPromise;
|
||||||
const toast: Toast = {
|
const toast: Toast = {
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@@ -92,4 +129,21 @@ export class AcceptOrganizationComponent implements OnInit {
|
|||||||
this.loading = false;
|
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>
|
<p class="lead text-center mx-4 mb-4">{{'loginOrCreateNewAccount' | i18n}}</p>
|
||||||
<div class="card d-block">
|
<div class="card d-block">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<app-callout type="warning" title="{{'resetPasswordPolicyAutoEnroll' | i18n}}"
|
||||||
|
*ngIf="showResetPasswordAutoEnrollWarning">
|
||||||
|
{{'resetPasswordAutoEnrollInviteWarning' | i18n}}
|
||||||
|
</app-callout>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email">{{'emailAddress' | i18n}}</label>
|
<label for="email">{{'emailAddress' | i18n}}</label>
|
||||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
|
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
|
||||||
|
|||||||
@@ -4,27 +4,35 @@ import {
|
|||||||
Router,
|
Router,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
|
|
||||||
|
import { ApiService } from 'jslib/abstractions/api.service';
|
||||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||||
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
|
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
|
||||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||||
|
import { PolicyService } from 'jslib/abstractions/policy.service';
|
||||||
import { StateService } from 'jslib/abstractions/state.service';
|
import { StateService } from 'jslib/abstractions/state.service';
|
||||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||||
|
|
||||||
import { LoginComponent as BaseLoginComponent } from 'jslib/angular/components/login.component';
|
import { LoginComponent as BaseLoginComponent } from 'jslib/angular/components/login.component';
|
||||||
|
|
||||||
|
import { Policy } from 'jslib/models/domain/policy';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-login',
|
selector: 'app-login',
|
||||||
templateUrl: 'login.component.html',
|
templateUrl: 'login.component.html',
|
||||||
})
|
})
|
||||||
export class LoginComponent extends BaseLoginComponent {
|
export class LoginComponent extends BaseLoginComponent {
|
||||||
|
|
||||||
|
showResetPasswordAutoEnrollWarning = false;
|
||||||
|
|
||||||
constructor(authService: AuthService, router: Router,
|
constructor(authService: AuthService, router: Router,
|
||||||
i18nService: I18nService, private route: ActivatedRoute,
|
i18nService: I18nService, private route: ActivatedRoute,
|
||||||
storageService: StorageService, stateService: StateService,
|
storageService: StorageService, stateService: StateService,
|
||||||
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
|
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
|
||||||
passwordGenerationService: PasswordGenerationService, cryptoFunctionService: CryptoFunctionService) {
|
passwordGenerationService: PasswordGenerationService, cryptoFunctionService: CryptoFunctionService,
|
||||||
|
private apiService: ApiService, private policyService: PolicyService) {
|
||||||
super(authService, router,
|
super(authService, router,
|
||||||
platformUtilsService, i18nService,
|
platformUtilsService, i18nService,
|
||||||
stateService, environmentService,
|
stateService, environmentService,
|
||||||
@@ -49,6 +57,22 @@ export class LoginComponent extends BaseLoginComponent {
|
|||||||
queryParamsSub.unsubscribe();
|
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() {
|
async goAfterLogIn() {
|
||||||
|
|||||||
@@ -200,12 +200,12 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: EmergencyAccessComponent,
|
component: EmergencyAccessComponent,
|
||||||
data: { titleId: 'emergencyAccess'},
|
data: { titleId: 'emergencyAccess' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
component: EmergencyAccessViewComponent,
|
component: EmergencyAccessViewComponent,
|
||||||
data: { titleId: 'emergencyAccess'},
|
data: { titleId: 'emergencyAccess' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -390,7 +390,7 @@ const routes: Routes = [
|
|||||||
canActivate: [OrganizationTypeGuardService],
|
canActivate: [OrganizationTypeGuardService],
|
||||||
data: {
|
data: {
|
||||||
titleId: 'people',
|
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 { PeopleComponent as OrgPeopleComponent } from './organizations/manage/people.component';
|
||||||
import { PoliciesComponent as OrgPoliciesComponent } from './organizations/manage/policies.component';
|
import { PoliciesComponent as OrgPoliciesComponent } from './organizations/manage/policies.component';
|
||||||
import { PolicyEditComponent as OrgPolicyEditComponent } from './organizations/manage/policy-edit.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 { UserAddEditComponent as OrgUserAddEditComponent } from './organizations/manage/user-add-edit.component';
|
||||||
import { UserConfirmComponent as OrgUserConfirmComponent } from './organizations/manage/user-confirm.component';
|
import { UserConfirmComponent as OrgUserConfirmComponent } from './organizations/manage/user-confirm.component';
|
||||||
import { UserGroupsComponent as OrgUserGroupsComponent } from './organizations/manage/user-groups.component';
|
import { UserGroupsComponent as OrgUserGroupsComponent } from './organizations/manage/user-groups.component';
|
||||||
@@ -368,6 +369,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
|||||||
OrgPeopleComponent,
|
OrgPeopleComponent,
|
||||||
OrgPolicyEditComponent,
|
OrgPolicyEditComponent,
|
||||||
OrgPoliciesComponent,
|
OrgPoliciesComponent,
|
||||||
|
OrgResetPasswordComponent,
|
||||||
OrgReusedPasswordsReportComponent,
|
OrgReusedPasswordsReportComponent,
|
||||||
OrgSettingComponent,
|
OrgSettingComponent,
|
||||||
OrgToolsComponent,
|
OrgToolsComponent,
|
||||||
@@ -456,6 +458,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
|||||||
OrgEntityUsersComponent,
|
OrgEntityUsersComponent,
|
||||||
OrgGroupAddEditComponent,
|
OrgGroupAddEditComponent,
|
||||||
OrgPolicyEditComponent,
|
OrgPolicyEditComponent,
|
||||||
|
OrgResetPasswordComponent,
|
||||||
OrgUserAddEditComponent,
|
OrgUserAddEditComponent,
|
||||||
OrgUserConfirmComponent,
|
OrgUserConfirmComponent,
|
||||||
OrgUserGroupsComponent,
|
OrgUserGroupsComponent,
|
||||||
|
|||||||
@@ -35,7 +35,8 @@
|
|||||||
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
|
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
|
||||||
{{'reinviteSelected' | i18n}}
|
{{'reinviteSelected' | i18n}}
|
||||||
</button>
|
</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>
|
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
|
||||||
{{'confirmSelected' | i18n}}
|
{{'confirmSelected' | i18n}}
|
||||||
</button>
|
</button>
|
||||||
@@ -53,7 +54,7 @@
|
|||||||
{{'unselectAll' | i18n}}
|
{{'unselectAll' | i18n}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="invite()">
|
<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>
|
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
|
||||||
{{'inviteUser' | i18n}}
|
{{'inviteUser' | i18n}}
|
||||||
@@ -95,6 +96,10 @@
|
|||||||
<i class="fa fa-lock" title="{{'userUsingTwoStep' | i18n}}" aria-hidden="true"></i>
|
<i class="fa fa-lock" title="{{'userUsingTwoStep' | i18n}}" aria-hidden="true"></i>
|
||||||
<span class="sr-only">{{'userUsingTwoStep' | i18n}}</span>
|
<span class="sr-only">{{'userUsingTwoStep' | i18n}}</span>
|
||||||
</ng-container>
|
</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>
|
||||||
<td>
|
<td>
|
||||||
<span *ngIf="u.type === organizationUserType.Owner">{{'owner' | i18n}}</span>
|
<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>
|
<i class="fa fa-fw fa-file-text-o" aria-hidden="true"></i>
|
||||||
{{'eventLogs' | i18n}}
|
{{'eventLogs' | i18n}}
|
||||||
</a>
|
</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)">
|
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
|
||||||
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
|
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
|
||||||
{{'remove' | i18n}}
|
{{'remove' | i18n}}
|
||||||
@@ -146,4 +156,5 @@
|
|||||||
<ng-template #groupsTemplate></ng-template>
|
<ng-template #groupsTemplate></ng-template>
|
||||||
<ng-template #eventsTemplate></ng-template>
|
<ng-template #eventsTemplate></ng-template>
|
||||||
<ng-template #confirmTemplate></ng-template>
|
<ng-template #confirmTemplate></ng-template>
|
||||||
|
<ng-template #resetPasswordTemplate></ng-template>
|
||||||
<ng-template #bulkStatusTemplate></ng-template>
|
<ng-template #bulkStatusTemplate></ng-template>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
ViewChild,
|
ViewChild,
|
||||||
ViewContainerRef,
|
ViewContainerRef,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ActivatedRoute,
|
ActivatedRoute,
|
||||||
Router,
|
Router,
|
||||||
@@ -13,32 +14,38 @@ import {
|
|||||||
import { ToasterService } from 'angular2-toaster';
|
import { ToasterService } from 'angular2-toaster';
|
||||||
|
|
||||||
import { ValidationService } from 'jslib/angular/services/validation.service';
|
import { ValidationService } from 'jslib/angular/services/validation.service';
|
||||||
|
|
||||||
import { ConstantsService } from 'jslib/services/constants.service';
|
import { ConstantsService } from 'jslib/services/constants.service';
|
||||||
|
|
||||||
import { ApiService } from 'jslib/abstractions/api.service';
|
import { ApiService } from 'jslib/abstractions/api.service';
|
||||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||||
|
import { PolicyService } from 'jslib/abstractions/policy.service';
|
||||||
import { SearchService } from 'jslib/abstractions/search.service';
|
import { SearchService } from 'jslib/abstractions/search.service';
|
||||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||||
import { UserService } from 'jslib/abstractions/user.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 { OrganizationUserBulkConfirmRequest } from 'jslib/models/request/organizationUserBulkConfirmRequest';
|
||||||
import { OrganizationUserConfirmRequest } from 'jslib/models/request/organizationUserConfirmRequest';
|
import { OrganizationUserConfirmRequest } from 'jslib/models/request/organizationUserConfirmRequest';
|
||||||
|
|
||||||
import { OrganizationUserBulkRequest } from 'jslib/models/request/organizationUserBulkRequest';
|
|
||||||
import { OrganizationUserUserDetailsResponse } from 'jslib/models/response/organizationUserResponse';
|
import { OrganizationUserUserDetailsResponse } from 'jslib/models/response/organizationUserResponse';
|
||||||
|
|
||||||
import { OrganizationUserStatusType } from 'jslib/enums/organizationUserStatusType';
|
import { OrganizationUserStatusType } from 'jslib/enums/organizationUserStatusType';
|
||||||
import { OrganizationUserType } from 'jslib/enums/organizationUserType';
|
import { OrganizationUserType } from 'jslib/enums/organizationUserType';
|
||||||
|
import { PolicyType } from 'jslib/enums/policyType';
|
||||||
|
|
||||||
import { Utils } from 'jslib/misc/utils';
|
import { Utils } from 'jslib/misc/utils';
|
||||||
|
|
||||||
import { ListResponse } from 'jslib/models/response';
|
import { ListResponse } from 'jslib/models/response';
|
||||||
import { OrganizationUserBulkResponse } from 'jslib/models/response/organizationUserBulkResponse';
|
import { OrganizationUserBulkResponse } from 'jslib/models/response/organizationUserBulkResponse';
|
||||||
|
|
||||||
import { ModalComponent } from '../../modal.component';
|
import { ModalComponent } from '../../modal.component';
|
||||||
import { BulkStatusComponent } from './bulk-status.component';
|
import { BulkStatusComponent } from './bulk-status.component';
|
||||||
import { EntityEventsComponent } from './entity-events.component';
|
import { EntityEventsComponent } from './entity-events.component';
|
||||||
|
import { ResetPasswordComponent } from './reset-password.component';
|
||||||
import { UserAddEditComponent } from './user-add-edit.component';
|
import { UserAddEditComponent } from './user-add-edit.component';
|
||||||
import { UserConfirmComponent } from './user-confirm.component';
|
import { UserConfirmComponent } from './user-confirm.component';
|
||||||
import { UserGroupsComponent } from './user-groups.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('groupsTemplate', { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef;
|
||||||
@ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef;
|
@ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef;
|
||||||
@ViewChild('confirmTemplate', { read: ViewContainerRef, static: true }) confirmModalRef: 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;
|
@ViewChild('bulkStatusTemplate', { read: ViewContainerRef, static: true }) bulkStatusModalRef: ViewContainerRef;
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
@@ -68,6 +76,11 @@ export class PeopleComponent implements OnInit {
|
|||||||
actionPromise: Promise<any>;
|
actionPromise: Promise<any>;
|
||||||
accessEvents = false;
|
accessEvents = false;
|
||||||
accessGroups = 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 didScroll = false;
|
||||||
protected pageSize = 100;
|
protected pageSize = 100;
|
||||||
@@ -81,7 +94,7 @@ export class PeopleComponent implements OnInit {
|
|||||||
private platformUtilsService: PlatformUtilsService, private toasterService: ToasterService,
|
private platformUtilsService: PlatformUtilsService, private toasterService: ToasterService,
|
||||||
private cryptoService: CryptoService, private userService: UserService, private router: Router,
|
private cryptoService: CryptoService, private userService: UserService, private router: Router,
|
||||||
private storageService: StorageService, private searchService: SearchService,
|
private storageService: StorageService, private searchService: SearchService,
|
||||||
private validationService: ValidationService) { }
|
private validationService: ValidationService, private policyService: PolicyService) { }
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.route.parent.parent.params.subscribe(async params => {
|
this.route.parent.parent.params.subscribe(async params => {
|
||||||
@@ -93,6 +106,25 @@ export class PeopleComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
this.accessEvents = organization.useEvents;
|
this.accessEvents = organization.useEvents;
|
||||||
this.accessGroups = organization.useGroups;
|
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();
|
await this.load();
|
||||||
|
|
||||||
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
|
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
|
||||||
@@ -123,9 +155,38 @@ export class PeopleComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.filter(this.status);
|
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;
|
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) {
|
filter(status: OrganizationUserStatusType) {
|
||||||
this.status = status;
|
this.status = status;
|
||||||
if (this.status != null) {
|
if (this.status != null) {
|
||||||
@@ -454,6 +515,31 @@ export class PeopleComponent implements OnInit {
|
|||||||
return !searching && this.users && this.users.length > this.pageSize;
|
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) {
|
checkUser(user: OrganizationUserUserDetailsResponse, select?: boolean) {
|
||||||
(user as any).checked = select == null ? !(user as any).checked : select;
|
(user as any).checked = select == null ? !(user as any).checked : select;
|
||||||
}
|
}
|
||||||
@@ -496,8 +582,8 @@ export class PeopleComponent implements OnInit {
|
|||||||
const response = await request;
|
const response = await request;
|
||||||
|
|
||||||
if (this.modal) {
|
if (this.modal) {
|
||||||
const keyedErrors: any = response.data.filter(r => r.error !== '').reduce((a, x) => ({...a, [x.id]: x.error}), {});
|
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 keyedFilteredUsers: any = filteredUsers.reduce((a, x) => ({ ...a, [x.id]: x }), {});
|
||||||
|
|
||||||
childComponent.users = users.map(user => {
|
childComponent.users = users.map(user => {
|
||||||
let message = keyedErrors[user.id] ?? successfullMessage;
|
let message = keyedErrors[user.id] ?? successfullMessage;
|
||||||
|
|||||||
@@ -115,6 +115,12 @@ export class PoliciesComponent implements OnInit {
|
|||||||
type: PolicyType.SendOptions,
|
type: PolicyType.SendOptions,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
display: true,
|
display: true,
|
||||||
|
}, {
|
||||||
|
name: this.i18nService.t('resetPasswordPolicy'),
|
||||||
|
description: this.i18nService.t('resetPasswordPolicyDescription'),
|
||||||
|
type: PolicyType.ResetPassword,
|
||||||
|
enabled: false,
|
||||||
|
display: organization.useResetPassword,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
await this.load();
|
await this.load();
|
||||||
|
|||||||
@@ -38,6 +38,9 @@
|
|||||||
<app-callout type="warning" *ngIf="type === policyType.SendOptions">
|
<app-callout type="warning" *ngIf="type === policyType.SendOptions">
|
||||||
{{'sendOptionsExemption' | i18n}}
|
{{'sendOptionsExemption' | i18n}}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
|
<app-callout type="warning" *ngIf="type === policyType.ResetPassword">
|
||||||
|
{{'resetPasswordPolicyWarning' | i18n}}
|
||||||
|
</app-callout>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" id="enabled" [(ngModel)]="enabled"
|
<input class="form-check-input" type="checkbox" id="enabled" [(ngModel)]="enabled"
|
||||||
@@ -153,19 +156,33 @@
|
|||||||
<ng-container *ngIf="type === policyType.SendOptions">
|
<ng-container *ngIf="type === policyType.SendOptions">
|
||||||
<h3 class="mt-4">{{'options' | i18n}}</h3>
|
<h3 class="mt-4">{{'options' | i18n}}</h3>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" id="sendDisableHideEmail" [(ngModel)]="sendDisableHideEmail"
|
<input class="form-check-input" type="checkbox" id="sendDisableHideEmail"
|
||||||
name="SendDisableHideEmail">
|
[(ngModel)]="sendDisableHideEmail" name="SendDisableHideEmail">
|
||||||
<label class="form-check-label" for="sendDisableHideEmail">{{'disableHideEmail' | i18n}}</label>
|
<label class="form-check-label" for="sendDisableHideEmail">{{'disableHideEmail' | i18n}}</label>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</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>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
<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>
|
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
<span>{{'save' | i18n}}</span>
|
<span>{{'save' | i18n}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-secondary"
|
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||||
data-dismiss="modal">{{'cancel' | i18n}}</button>
|
{{'cancel' | i18n}}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ export class PolicyEditComponent implements OnInit {
|
|||||||
// Send options
|
// Send options
|
||||||
sendDisableHideEmail?: boolean;
|
sendDisableHideEmail?: boolean;
|
||||||
|
|
||||||
|
// Reset Password
|
||||||
|
resetPasswordAutoEnroll?: boolean;
|
||||||
|
|
||||||
private policy: PolicyResponse;
|
private policy: PolicyResponse;
|
||||||
|
|
||||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||||
@@ -116,6 +119,9 @@ export class PolicyEditComponent implements OnInit {
|
|||||||
case PolicyType.SendOptions:
|
case PolicyType.SendOptions:
|
||||||
this.sendDisableHideEmail = this.policy.data.disableHideEmail;
|
this.sendDisableHideEmail = this.policy.data.disableHideEmail;
|
||||||
break;
|
break;
|
||||||
|
case PolicyType.ResetPassword:
|
||||||
|
this.resetPasswordAutoEnroll = this.policy.data.autoEnrollEnabled;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -167,6 +173,11 @@ export class PolicyEditComponent implements OnInit {
|
|||||||
disableHideEmail: this.sendDisableHideEmail,
|
disableHideEmail: this.sendDisableHideEmail,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
case PolicyType.ResetPassword:
|
||||||
|
request.data = {
|
||||||
|
autoEnrollEnabled: this.resetPasswordAutoEnroll,
|
||||||
|
};
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
78
src/app/organizations/manage/reset-password.component.html
Normal file
78
src/app/organizations/manage/reset-password.component.html
Normal 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">×</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>
|
||||||
191
src/app/organizations/manage/reset-password.component.ts
Normal file
191
src/app/organizations/manage/reset-password.component.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,22 +4,28 @@ import {
|
|||||||
ViewChild,
|
ViewChild,
|
||||||
ViewContainerRef,
|
ViewContainerRef,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
import { ToasterService } from 'angular2-toaster';
|
import { ToasterService } from 'angular2-toaster';
|
||||||
|
|
||||||
import { ApiService } from 'jslib/abstractions/api.service';
|
import { ApiService } from 'jslib/abstractions/api.service';
|
||||||
|
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||||
|
|
||||||
|
import { OrganizationKeysRequest } from 'jslib/models/request/organizationKeysRequest';
|
||||||
import { OrganizationUpdateRequest } from 'jslib/models/request/organizationUpdateRequest';
|
import { OrganizationUpdateRequest } from 'jslib/models/request/organizationUpdateRequest';
|
||||||
|
|
||||||
import { OrganizationResponse } from 'jslib/models/response/organizationResponse';
|
import { OrganizationResponse } from 'jslib/models/response/organizationResponse';
|
||||||
|
|
||||||
import { ModalComponent } from '../../modal.component';
|
import { ModalComponent } from '../../modal.component';
|
||||||
|
|
||||||
import { ApiKeyComponent } from '../../settings/api-key.component';
|
import { ApiKeyComponent } from '../../settings/api-key.component';
|
||||||
import { PurgeVaultComponent } from '../../settings/purge-vault.component';
|
import { PurgeVaultComponent } from '../../settings/purge-vault.component';
|
||||||
import { TaxInfoComponent } from '../../settings/tax-info.component';
|
import { TaxInfoComponent } from '../../settings/tax-info.component';
|
||||||
|
|
||||||
import { DeleteOrganizationComponent } from './delete-organization.component';
|
import { DeleteOrganizationComponent } from './delete-organization.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -46,7 +52,8 @@ export class AccountComponent {
|
|||||||
constructor(private componentFactoryResolver: ComponentFactoryResolver,
|
constructor(private componentFactoryResolver: ComponentFactoryResolver,
|
||||||
private apiService: ApiService, private i18nService: I18nService,
|
private apiService: ApiService, private i18nService: I18nService,
|
||||||
private toasterService: ToasterService, private route: ActivatedRoute,
|
private toasterService: ToasterService, private route: ActivatedRoute,
|
||||||
private syncService: SyncService, private platformUtilsService: PlatformUtilsService) { }
|
private syncService: SyncService, private platformUtilsService: PlatformUtilsService,
|
||||||
|
private cryptoService: CryptoService) { }
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.selfHosted = this.platformUtilsService.isSelfHost();
|
this.selfHosted = this.platformUtilsService.isSelfHost();
|
||||||
@@ -67,6 +74,14 @@ export class AccountComponent {
|
|||||||
request.businessName = this.org.businessName;
|
request.businessName = this.org.businessName;
|
||||||
request.billingEmail = this.org.billingEmail;
|
request.billingEmail = this.org.billingEmail;
|
||||||
request.identifier = this.org.identifier;
|
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(() => {
|
this.formPromise = this.apiService.putOrganization(this.organizationId, request).then(() => {
|
||||||
return this.syncService.fullSync(true);
|
return this.syncService.fullSync(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ export class OrganizationTypeGuardService implements CanActivate {
|
|||||||
(permissions.indexOf(Permissions.ManageGroups) !== -1 && org.canManageGroups) ||
|
(permissions.indexOf(Permissions.ManageGroups) !== -1 && org.canManageGroups) ||
|
||||||
(permissions.indexOf(Permissions.ManageOrganization) !== -1 && org.isOwner) ||
|
(permissions.indexOf(Permissions.ManageOrganization) !== -1 && org.isOwner) ||
|
||||||
(permissions.indexOf(Permissions.ManagePolicies) !== -1 && org.canManagePolicies) ||
|
(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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,9 +205,12 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-enroll - encrpyt user's encKey.key with organization key
|
// Retrieve public key
|
||||||
const orgSymKey = await this.cryptoService.getOrgKey(org.id);
|
const response = await this.apiService.getOrganizationKeys(org.id);
|
||||||
const encryptedKey = await this.cryptoService.encrypt(encKey.key, orgSymKey);
|
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
|
// Create/Execute request
|
||||||
const request = new OrganizationUserResetPasswordEnrollmentRequest();
|
const request = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ import { PolicyType } from 'jslib/enums/policyType';
|
|||||||
import { ProductType } from 'jslib/enums/productType';
|
import { ProductType } from 'jslib/enums/productType';
|
||||||
|
|
||||||
import { OrganizationCreateRequest } from 'jslib/models/request/organizationCreateRequest';
|
import { OrganizationCreateRequest } from 'jslib/models/request/organizationCreateRequest';
|
||||||
|
import { OrganizationKeysRequest } from 'jslib/models/request/organizationKeysRequest';
|
||||||
import { OrganizationUpgradeRequest } from 'jslib/models/request/organizationUpgradeRequest';
|
import { OrganizationUpgradeRequest } from 'jslib/models/request/organizationUpgradeRequest';
|
||||||
|
|
||||||
import { PlanResponse } from 'jslib/models/response/planResponse';
|
import { PlanResponse } from 'jslib/models/response/planResponse';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -259,6 +261,7 @@ export class OrganizationPlansComponent implements OnInit {
|
|||||||
const collection = await this.cryptoService.encrypt(
|
const collection = await this.cryptoService.encrypt(
|
||||||
this.i18nService.t('defaultCollection'), shareKey[1]);
|
this.i18nService.t('defaultCollection'), shareKey[1]);
|
||||||
const collectionCt = collection.encryptedString;
|
const collectionCt = collection.encryptedString;
|
||||||
|
const orgKeys = await this.cryptoService.makeKeyPair(shareKey[1]);
|
||||||
|
|
||||||
if (this.selfHosted) {
|
if (this.selfHosted) {
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
@@ -267,12 +270,17 @@ export class OrganizationPlansComponent implements OnInit {
|
|||||||
fd.append('collectionName', collectionCt);
|
fd.append('collectionName', collectionCt);
|
||||||
const response = await this.apiService.postOrganizationLicense(fd);
|
const response = await this.apiService.postOrganizationLicense(fd);
|
||||||
orgId = response.id;
|
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 {
|
} else {
|
||||||
const request = new OrganizationCreateRequest();
|
const request = new OrganizationCreateRequest();
|
||||||
request.key = key;
|
request.key = key;
|
||||||
request.collectionName = collectionCt;
|
request.collectionName = collectionCt;
|
||||||
request.name = this.name;
|
request.name = this.name;
|
||||||
request.billingEmail = this.billingEmail;
|
request.billingEmail = this.billingEmail;
|
||||||
|
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
|
||||||
|
|
||||||
if (this.selectedPlan.type === PlanType.Free) {
|
if (this.selectedPlan.type === PlanType.Free) {
|
||||||
request.planType = PlanType.Free;
|
request.planType = PlanType.Free;
|
||||||
@@ -309,6 +317,14 @@ export class OrganizationPlansComponent implements OnInit {
|
|||||||
request.billingAddressCountry = this.taxComponent.taxInfo.country;
|
request.billingAddressCountry = this.taxComponent.taxInfo.country;
|
||||||
request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode;
|
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);
|
const result = await this.apiService.postOrganizationUpgrade(this.organizationId, request);
|
||||||
if (!result.success && result.paymentIntentClientSecret != null) {
|
if (!result.success && result.paymentIntentClientSecret != null) {
|
||||||
await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null);
|
await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null);
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
title="{{'organizationIsDisabled' | i18n}}" aria-hidden="true"></i>
|
title="{{'organizationIsDisabled' | i18n}}" aria-hidden="true"></i>
|
||||||
<span class="sr-only">{{'organizationIsDisabled' | i18n}}</span>
|
<span class="sr-only">{{'organizationIsDisabled' | i18n}}</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="o.isResetPasswordEnrolled">
|
<ng-container *ngIf="showEnrolledStatus(o)">
|
||||||
<i class="fa fa-key" appStopProp title="{{'enrolledPasswordReset' | i18n}}"
|
<i class="fa fa-key" appStopProp title="{{'enrolledPasswordReset' | i18n}}"
|
||||||
aria-hidden="true"></i>
|
aria-hidden="true"></i>
|
||||||
<span class="sr-only">{{'enrolledPasswordReset' | i18n}}</span>
|
<span class="sr-only">{{'enrolledPasswordReset' | i18n}}</span>
|
||||||
@@ -79,13 +79,13 @@
|
|||||||
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
|
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu dropdown-menu-right">
|
<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)">
|
href="#" appStopClick (click)="toggleResetPasswordEnrollment(o)">
|
||||||
<i class="fa fa-fw fa-key" aria-hidden="true"></i>
|
<i class="fa fa-fw fa-key" aria-hidden="true"></i>
|
||||||
{{'enrollPasswordReset' | i18n}}
|
{{'enrollPasswordReset' | i18n}}
|
||||||
</a>
|
</a>
|
||||||
<a *ngIf="o.isResetPasswordEnrolled" class="dropdown-item" href="#" appStopClick
|
<a *ngIf="allowEnrollmentChanges(o) && o.resetPasswordEnrolled" class="dropdown-item"
|
||||||
(click)="toggleResetPasswordEnrollment(o)">
|
href="#" appStopClick (click)="toggleResetPasswordEnrollment(o)">
|
||||||
<i class="fa fa-fw fa-undo" aria-hidden="true"></i>
|
<i class="fa fa-fw fa-undo" aria-hidden="true"></i>
|
||||||
{{'withdrawPasswordReset' | i18n}}
|
{{'withdrawPasswordReset' | i18n}}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -10,15 +10,19 @@ import { ApiService } from 'jslib/abstractions/api.service';
|
|||||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||||
|
import { PolicyService } from 'jslib/abstractions/policy.service';
|
||||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||||
import { UserService } from 'jslib/abstractions/user.service';
|
import { UserService } from 'jslib/abstractions/user.service';
|
||||||
|
|
||||||
import { Organization } from 'jslib/models/domain/organization';
|
import { Organization } from 'jslib/models/domain/organization';
|
||||||
|
import { Policy } from 'jslib/models/domain/policy';
|
||||||
|
|
||||||
import { Utils } from 'jslib/misc/utils';
|
import { Utils } from 'jslib/misc/utils';
|
||||||
|
|
||||||
import { OrganizationUserResetPasswordEnrollmentRequest } from 'jslib/models/request/organizationUserResetPasswordEnrollmentRequest';
|
import { OrganizationUserResetPasswordEnrollmentRequest } from 'jslib/models/request/organizationUserResetPasswordEnrollmentRequest';
|
||||||
|
|
||||||
|
import { PolicyType } from 'jslib/enums/policyType';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-organizations',
|
selector: 'app-organizations',
|
||||||
templateUrl: 'organizations.component.html',
|
templateUrl: 'organizations.component.html',
|
||||||
@@ -27,15 +31,14 @@ export class OrganizationsComponent implements OnInit {
|
|||||||
@Input() vault = false;
|
@Input() vault = false;
|
||||||
|
|
||||||
organizations: Organization[];
|
organizations: Organization[];
|
||||||
|
policies: Policy[];
|
||||||
loaded: boolean = false;
|
loaded: boolean = false;
|
||||||
actionPromise: Promise<any>;
|
actionPromise: Promise<any>;
|
||||||
// TODO Remove feature flag once ready for general release
|
|
||||||
resetPasswordFeatureFlag = false;
|
|
||||||
|
|
||||||
constructor(private userService: UserService, private platformUtilsService: PlatformUtilsService,
|
constructor(private userService: UserService, private platformUtilsService: PlatformUtilsService,
|
||||||
private i18nService: I18nService, private apiService: ApiService,
|
private i18nService: I18nService, private apiService: ApiService,
|
||||||
private toasterService: ToasterService, private syncService: SyncService,
|
private toasterService: ToasterService, private syncService: SyncService,
|
||||||
private cryptoService: CryptoService) { }
|
private cryptoService: CryptoService, private policyService: PolicyService) { }
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
if (!this.vault) {
|
if (!this.vault) {
|
||||||
@@ -48,9 +51,22 @@ export class OrganizationsComponent implements OnInit {
|
|||||||
const orgs = await this.userService.getAllOrganizations();
|
const orgs = await this.userService.getAllOrganizations();
|
||||||
orgs.sort(Utils.getSortFunction(this.i18nService, 'name'));
|
orgs.sort(Utils.getSortFunction(this.i18nService, 'name'));
|
||||||
this.organizations = orgs;
|
this.organizations = orgs;
|
||||||
|
this.policies = await this.policyService.getAll(PolicyType.ResetPassword);
|
||||||
this.loaded = true;
|
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) {
|
async unlinkSso(org: Organization) {
|
||||||
const confirmed = await this.platformUtilsService.showDialog(
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
'Are you sure you want to unlink SSO for this organization?', org.name,
|
'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) {
|
async toggleResetPasswordEnrollment(org: Organization) {
|
||||||
// Feature Flag
|
|
||||||
if (!this.resetPasswordFeatureFlag) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set variables
|
// Set variables
|
||||||
let keyString: string = null;
|
let keyString: string = null;
|
||||||
let toastStringRef = 'withdrawPasswordResetSuccess';
|
let toastStringRef = 'withdrawPasswordResetSuccess';
|
||||||
|
|
||||||
// Enroll - encrpyt user's encKey.key with organization key
|
// Enrolling
|
||||||
if (!org.resetPasswordEnrolled) {
|
if (!org.resetPasswordEnrolled) {
|
||||||
const encKey = await this.cryptoService.getEncKey();
|
// Alert user about enrollment
|
||||||
const orgSymKey = await this.cryptoService.getOrgKey(org.id);
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
const encryptedKey = await this.cryptoService.encrypt(encKey.key, orgSymKey);
|
this.i18nService.t('resetPasswordEnrollmentWarning'), null,
|
||||||
keyString = encryptedKey.encryptedString;
|
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||||
toastStringRef = 'enrollPasswordResetSuccess';
|
if (!confirmed) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Create/Execute request
|
// Retrieve Public Key
|
||||||
try {
|
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();
|
const request = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||||
request.resetPasswordKey = keyString;
|
request.resetPasswordKey = keyString;
|
||||||
this.actionPromise = this.apiService.putOrganizationUserResetPasswordEnrollment(org.id, org.userId, request)
|
this.actionPromise = this.apiService.putOrganizationUserResetPasswordEnrollment(org.id, org.userId, request)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return this.syncService.fullSync(true);
|
return this.syncService.fullSync(true);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
await this.actionPromise;
|
await this.actionPromise;
|
||||||
this.platformUtilsService.showToast('success', null, this.i18nService.t(toastStringRef));
|
this.platformUtilsService.showToast('success', null, this.i18nService.t(toastStringRef));
|
||||||
await this.load();
|
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": {
|
"trashCleanupWarning": {
|
||||||
"message": "Items that have been in Trash more than 30 days will be automatically deleted."
|
"message": "Items that have been in Trash more than 30 days will be automatically deleted."
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user