1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-30 13:03:53 +01:00

Implemented Custom role and permissions (#750)

* Implemented Custom role and permissions

* converted Permissions interface into a class

* fixed a merge issue

* updated jslib

* code review cleanup for Permissions

* trailing commas
This commit is contained in:
Addison Beck 2021-01-12 15:31:22 -05:00 committed by GitHub
parent c3f4c6c03b
commit dc87510a7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 555 additions and 127 deletions

2
jslib

@ -1 +1 @@
Subproject commit cea09a22e533ef3598bb497ba0503c2fcd5b2dc1 Subproject commit 6ac6df75d7a9bd5ea58f5d8310f1b3e34abd2bde

View File

@ -91,9 +91,10 @@ import { UnauthGuardService } from './services/unauth-guard.service';
import { AuthGuardService } from 'jslib/angular/services/auth-guard.service'; import { AuthGuardService } from 'jslib/angular/services/auth-guard.service';
import { OrganizationUserType } from 'jslib/enums/organizationUserType'; import { Permissions } from 'jslib/enums/permissions';
import { EmergencyAccessComponent } from './settings/emergency-access.component';
import { EmergencyAccessViewComponent } from './settings/emergency-access-view.component'; import { EmergencyAccessViewComponent } from './settings/emergency-access-view.component';
import { EmergencyAccessComponent } from './settings/emergency-access.component';
const routes: Routes = [ const routes: Routes = [
{ {
@ -259,35 +260,75 @@ const routes: Routes = [
path: 'tools', path: 'tools',
component: OrgToolsComponent, component: OrgToolsComponent,
canActivate: [OrganizationTypeGuardService], canActivate: [OrganizationTypeGuardService],
data: { allowedTypes: [OrganizationUserType.Owner, OrganizationUserType.Admin] }, data: { permissions: [Permissions.AccessImportExport, Permissions.AccessReports] },
children: [ children: [
{ path: '', pathMatch: 'full', redirectTo: 'import' }, {
{ path: 'import', component: OrgImportComponent, data: { titleId: 'importData' } }, path: '',
{ path: 'export', component: OrgExportComponent, data: { titleId: 'exportVault' } }, pathMatch: 'full',
redirectTo: 'import',
},
{
path: 'import',
component: OrgImportComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'importData',
permissions: [Permissions.AccessImportExport],
},
},
{
path: 'export',
component: OrgExportComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'exportVault',
permissions: [Permissions.AccessImportExport],
},
},
{ {
path: 'exposed-passwords-report', path: 'exposed-passwords-report',
component: OrgExposedPasswordsReportComponent, component: OrgExposedPasswordsReportComponent,
data: { titleId: 'exposedPasswordsReport' }, canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'exposedPasswordsReport',
permissions: [Permissions.AccessReports],
},
}, },
{ {
path: 'inactive-two-factor-report', path: 'inactive-two-factor-report',
component: OrgInactiveTwoFactorReportComponent, component: OrgInactiveTwoFactorReportComponent,
data: { titleId: 'inactive2faReport' }, canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'inactive2faReport',
permissions: [Permissions.AccessReports],
},
}, },
{ {
path: 'reused-passwords-report', path: 'reused-passwords-report',
component: OrgReusedPasswordsReportComponent, component: OrgReusedPasswordsReportComponent,
data: { titleId: 'reusedPasswordsReport' }, canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'reusedPasswordsReport',
permissions: [Permissions.AccessReports],
},
}, },
{ {
path: 'unsecured-websites-report', path: 'unsecured-websites-report',
component: OrgUnsecuredWebsitesReportComponent, component: OrgUnsecuredWebsitesReportComponent,
data: { titleId: 'unsecuredWebsitesReport' }, canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'unsecuredWebsitesReport',
permissions: [Permissions.AccessReports],
},
}, },
{ {
path: 'weak-passwords-report', path: 'weak-passwords-report',
component: OrgWeakPasswordsReportComponent, component: OrgWeakPasswordsReportComponent,
data: { titleId: 'weakPasswordsReport' }, canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'weakPasswordsReport',
permissions: [Permissions.AccessReports],
},
}, },
], ],
}, },
@ -296,26 +337,73 @@ const routes: Routes = [
component: OrgManageComponent, component: OrgManageComponent,
canActivate: [OrganizationTypeGuardService], canActivate: [OrganizationTypeGuardService],
data: { data: {
allowedTypes: [ permissions: [
OrganizationUserType.Owner, Permissions.ManageAssignedCollections,
OrganizationUserType.Admin, Permissions.ManageAllCollections,
OrganizationUserType.Manager, Permissions.AccessEventLogs,
Permissions.ManageGroups,
Permissions.ManageUsers,
Permissions.ManagePolicies,
], ],
}, },
children: [ children: [
{ path: '', pathMatch: 'full', redirectTo: 'people' }, {
{ path: 'collections', component: OrgManageCollectionsComponent, data: { titleId: 'collections' } }, path: '',
{ path: 'events', component: OrgEventsComponent, data: { titleId: 'eventLogs' } }, pathMatch: 'full',
{ path: 'groups', component: OrgGroupsComponent, data: { titleId: 'groups' } }, redirectTo: 'people',
{ path: 'people', component: OrgPeopleComponent, data: { titleId: 'people' } }, },
{ path: 'policies', component: OrgPoliciesComponent, data: { titleId: 'policies' } }, {
path: 'collections',
component: OrgManageCollectionsComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'collections',
permissions: [Permissions.ManageAssignedCollections, Permissions.ManageAllCollections],
},
},
{
path: 'events',
component: OrgEventsComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'eventLogs',
permissions: [Permissions.AccessEventLogs],
},
},
{
path: 'groups',
component: OrgGroupsComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'groups',
permissions: [Permissions.ManageGroups],
},
},
{
path: 'people',
component: OrgPeopleComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'people',
permissions: [Permissions.ManageUsers],
},
},
{
path: 'policies',
component: OrgPoliciesComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'policies',
permissions: [Permissions.ManagePolicies],
},
},
], ],
}, },
{ {
path: 'settings', path: 'settings',
component: OrgSettingsComponent, component: OrgSettingsComponent,
canActivate: [OrganizationTypeGuardService], canActivate: [OrganizationTypeGuardService],
data: { allowedTypes: [OrganizationUserType.Owner] }, data: { permissions: [Permissions.ManageOrganization] },
children: [ children: [
{ path: '', pathMatch: 'full', redirectTo: 'account' }, { path: '', pathMatch: 'full', redirectTo: 'account' },
{ path: 'account', component: OrgAccountComponent, data: { titleId: 'myOrganization' } }, { path: 'account', component: OrgAccountComponent, data: { titleId: 'myOrganization' } },
@ -340,6 +428,7 @@ const routes: Routes = [
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(routes, { imports: [RouterModule.forRoot(routes, {
useHash: true, useHash: true,
paramsInheritanceStrategy: 'always',
/*enableTracing: true,*/ /*enableTracing: true,*/
})], })],
exports: [RouterModule], exports: [RouterModule],

View File

@ -15,21 +15,21 @@
</div> </div>
</div> </div>
</div> </div>
<ul class="nav nav-tabs" *ngIf="organization.isManager"> <ul class="nav nav-tabs" *ngIf="showMenuBar">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="vault" routerLinkActive="active"> <a class="nav-link" routerLink="vault" routerLinkActive="active">
<i class="fa fa-lock" aria-hidden="true"></i> <i class="fa fa-lock" aria-hidden="true"></i>
{{'vault' | i18n}} {{'vault' | i18n}}
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item" *ngIf="showManageTab">
<a class="nav-link" routerLink="manage" routerLinkActive="active"> <a class="nav-link" [routerLink]="manageRoute" routerLinkActive="active">
<i class="fa fa-sliders" aria-hidden="true"></i> <i class="fa fa-sliders" aria-hidden="true"></i>
{{'manage' | i18n}} {{'manage' | i18n}}
</a> </a>
</li> </li>
<li class="nav-item" *ngIf="organization.isAdmin"> <li class="nav-item" *ngIf="showToolsTab">
<a class="nav-link" routerLink="tools" routerLinkActive="active"> <a class="nav-link" [routerLink]="toolsRoute" routerLinkActive="active">
<i class="fa fa-wrench" aria-hidden="true"></i> <i class="fa fa-wrench" aria-hidden="true"></i>
{{'tools' | i18n}} {{'tools' | i18n}}
</a> </a>
@ -43,10 +43,10 @@
</ul> </ul>
</div> </div>
<div class="ml-auto d-flex align-items-center"> <div class="ml-auto d-flex align-items-center">
<button class="btn btn-primary" (click)="goToEnterprisePortal()" #enterpriseBtn <button class="btn btn-primary" (click)="goToBusinessPortal()" #businessBtn
[appApiAction]="enterpriseTokenPromise" *ngIf="organization.useBusinessPortal"> [appApiAction]="businessTokenPromise" *ngIf="showBusinessPortalButton">
<i class="fa fa-bank fa-fw" [hidden]="enterpriseBtn.loading" aria-hidden="true"></i> <i class="fa fa-bank fa-fw" [hidden]="businessBtn.loading" aria-hidden="true"></i>
<i class="fa fa-spinner fa-spin fa-fw" [hidden]="!enterpriseBtn.loading" title="{{'loading' | i18n}}" <i class="fa fa-spinner fa-spin fa-fw" [hidden]="!businessBtn.loading" title="{{'loading' | i18n}}"
aria-hidden="true"></i> aria-hidden="true"></i>
{{'businessPortal' | i18n}} → {{'businessPortal' | i18n}} →
</button> </button>

View File

@ -24,9 +24,9 @@ const BroadcasterSubscriptionId = 'OrganizationLayoutComponent';
}) })
export class OrganizationLayoutComponent implements OnInit, OnDestroy { export class OrganizationLayoutComponent implements OnInit, OnDestroy {
organization: Organization; organization: Organization;
enterpriseTokenPromise: Promise<any>; businessTokenPromise: Promise<any>;
private organizationId: string; private organizationId: string;
private enterpriseUrl: string; private businessUrl: string;
constructor(private route: ActivatedRoute, private userService: UserService, constructor(private route: ActivatedRoute, private userService: UserService,
private broadcasterService: BroadcasterService, private ngZone: NgZone, private broadcasterService: BroadcasterService, private ngZone: NgZone,
@ -34,11 +34,11 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
private environmentService: EnvironmentService) { } private environmentService: EnvironmentService) { }
ngOnInit() { ngOnInit() {
this.enterpriseUrl = 'https://portal.bitwarden.com'; this.businessUrl = 'https://portal.bitwarden.com';
if (this.environmentService.enterpriseUrl != null) { if (this.environmentService.enterpriseUrl != null) {
this.enterpriseUrl = this.environmentService.enterpriseUrl; this.businessUrl = this.environmentService.enterpriseUrl;
} else if (this.environmentService.baseUrl != null) { } else if (this.environmentService.baseUrl != null) {
this.enterpriseUrl = this.environmentService.baseUrl + '/portal'; this.businessUrl = this.environmentService.baseUrl + '/portal';
} }
document.body.classList.remove('layout_frontend'); document.body.classList.remove('layout_frontend');
@ -65,19 +65,68 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
this.organization = await this.userService.getOrganization(this.organizationId); this.organization = await this.userService.getOrganization(this.organizationId);
} }
async goToEnterprisePortal() { async goToBusinessPortal() {
if (this.enterpriseTokenPromise != null) { if (this.businessTokenPromise != null) {
return; return;
} }
try { try {
this.enterpriseTokenPromise = this.apiService.getEnterprisePortalSignInToken(); this.businessTokenPromise = this.apiService.getEnterprisePortalSignInToken();
const token = await this.enterpriseTokenPromise; const token = await this.businessTokenPromise;
if (token != null) { if (token != null) {
const userId = await this.userService.getUserId(); const userId = await this.userService.getUserId();
this.platformUtilsService.launchUri(this.enterpriseUrl + '/login?userId=' + userId + this.platformUtilsService.launchUri(this.businessUrl + '/login?userId=' + userId +
'&token=' + (window as any).encodeURIComponent(token) + '&organizationId=' + this.organization.id); '&token=' + (window as any).encodeURIComponent(token) + '&organizationId=' + this.organization.id);
} }
} catch { } } catch { }
this.enterpriseTokenPromise = null; this.businessTokenPromise = null;
}
get showMenuBar() {
return this.showManageTab || this.showToolsTab || this.organization.isOwner;
}
get showManageTab(): boolean {
return this.organization.canManageUsers ||
this.organization.canManageAssignedCollections ||
this.organization.canManageAllCollections ||
this.organization.canManageGroups ||
this.organization.canManagePolicies ||
this.organization.canAccessEventLogs;
}
get showToolsTab(): boolean {
return this.organization.canAccessImportExport || this.organization.canAccessReports;
}
get showBusinessPortalButton(): boolean {
return this.organization.useBusinessPortal && this.organization.canAccessBusinessPortal;
}
get toolsRoute(): string {
return this.organization.canAccessImportExport ?
'tools/import' :
'tools/exposed-passwords-report';
}
get manageRoute(): string {
let route: string;
switch (true) {
case this.organization.canManageUsers:
route = 'manage/people';
break;
case this.organization.canManageAssignedCollections || this.organization.canManageAllCollections:
route = 'manage/collections';
break;
case this.organization.canManageGroups:
route = 'manage/groups';
break;
case this.organization.canManagePolicies:
route = 'manage/policies';
break;
case this.organization.canAccessEventLogs:
route = 'manage/events';
break;
}
return route;
} }
} }

View File

@ -72,7 +72,7 @@ export class CollectionsComponent implements OnInit {
async load() { async load() {
const organization = await this.userService.getOrganization(this.organizationId); const organization = await this.userService.getOrganization(this.organizationId);
let response: ListResponse<CollectionResponse>; let response: ListResponse<CollectionResponse>;
if (organization.isAdmin) { if (organization.canManageAllCollections) {
response = await this.apiService.getCollections(this.organizationId); response = await this.apiService.getCollections(this.organizationId);
} else { } else {
response = await this.apiService.getUserCollections(); response = await this.apiService.getUserCollections();

View File

@ -5,22 +5,23 @@
<div class="card-header">{{'manage' | i18n}}</div> <div class="card-header">{{'manage' | i18n}}</div>
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
<a routerLink="people" class="list-group-item" routerLinkActive="active" <a routerLink="people" class="list-group-item" routerLinkActive="active"
*ngIf="organization.isAdmin"> *ngIf="organization.canManageUsers">
{{'people' | i18n}} {{'people' | i18n}}
</a> </a>
<a routerLink="collections" class="list-group-item" routerLinkActive="active"> <a routerLink="collections" class="list-group-item" routerLinkActive="active"
*ngIf="organization.canManageAssignedCollections || organization.canManageAllCollections">
{{'collections' | i18n}} {{'collections' | i18n}}
</a> </a>
<a routerLink="groups" class="list-group-item" routerLinkActive="active" <a routerLink="groups" class="list-group-item" routerLinkActive="active"
*ngIf="organization.isAdmin && accessGroups"> *ngIf="organization.canManageGroups && accessGroups">
{{'groups' | i18n}} {{'groups' | i18n}}
</a> </a>
<a routerLink="policies" class="list-group-item" routerLinkActive="active" <a routerLink="policies" class="list-group-item" routerLinkActive="active"
*ngIf="organization.isAdmin && accessPolicies"> *ngIf="organization.canManagePolicies && accessPolicies">
{{'policies' | i18n}} {{'policies' | i18n}}
</a> </a>
<a routerLink="events" class="list-group-item" routerLinkActive="active" <a routerLink="events" class="list-group-item" routerLinkActive="active"
*ngIf="organization.isAdmin && accessEvents"> *ngIf="organization.canAccessEventLogs && accessEvents">
{{'eventLogs' | i18n}} {{'eventLogs' | i18n}}
</a> </a>
</div> </div>

View File

@ -69,6 +69,7 @@
<span *ngIf="u.type === organizationUserType.Admin">{{'admin' | i18n}}</span> <span *ngIf="u.type === organizationUserType.Admin">{{'admin' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Manager">{{'manager' | i18n}}</span> <span *ngIf="u.type === organizationUserType.Manager">{{'manager' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.User">{{'user' | i18n}}</span> <span *ngIf="u.type === organizationUserType.User">{{'user' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Custom">{{'custom' | i18n}}</span>
</td> </td>
<td class="table-list-options"> <td class="table-list-options">
<div class="dropdown" appListDropdown> <div class="dropdown" appListDropdown>

View File

@ -79,7 +79,7 @@ export class PeopleComponent implements OnInit {
this.route.parent.parent.params.subscribe(async (params) => { this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId; this.organizationId = params.organizationId;
const organization = await this.userService.getOrganization(this.organizationId); const organization = await this.userService.getOrganization(this.organizationId);
if (!organization.isAdmin) { if (!organization.canManageUsers) {
this.router.navigate(['../collections'], { relativeTo: this.route }); this.router.navigate(['../collections'], { relativeTo: this.route });
return; return;
} }

View File

@ -63,8 +63,127 @@
<small>{{'ownerDesc' | i18n}}</small> <small>{{'ownerDesc' | i18n}}</small>
</label> </label>
</div> </div>
<div class="form-check mt-2 form-check-block">
<input class="form-check-input" type="radio" name="userType" id="userTypeCustom"
[value]="organizationUserType.Custom" [(ngModel)]="type">
<label class="form-check-label" for="userTypeCustom">
{{'custom' | i18n}}
<small>{{'customDesc' | i18n}}</small>
</label>
</div>
<ng-container *ngIf="customUserTypeSelected">
<h3 class="mt-4 d-flex">
{{'permissions' | i18n}}
</h3>
<div class="row">
<div class="col-6">
<div class="mb-3">
<label class="font-weight-bold mb-0">Manager Permissions</label>
<hr class="my-0 mr-2" />
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="manageAssignedCollections"
id="manageAssignedCollections"
[(ngModel)]="permissions.manageAssignedCollections">
<label class="form-check-label font-weight-normal"
for="manageAssignedCollections">
{{'manageAssignedCollections' | i18n}}
</label>
</div>
</div>
</div>
</div>
<div class="col-6">
<div class="mb-3">
<label class="font-weight-bold mb-0">Admin Permissions</label>
<hr class="my-0 mr-2" />
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="accessBusinessPortal"
id="accessBusinessPortal" [(ngModel)]="permissions.accessBusinessPortal">
<label class="form-check-label font-weight-normal" for="accessBusinessPortal">
{{'accessBusinessPortal' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="accessEventLogs"
id="accessEventLogs" [(ngModel)]="permissions.accessEventLogs">
<label class="form-check-label font-weight-normal" for="accessEventLogs">
{{'accessEventLogs' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="accessImportExport"
id="accessImportExport" [(ngModel)]="permissions.accessImportExport">
<label class="form-check-label font-weight-normal" for="accessImportExport">
{{'accessImportExport' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="accessReports"
id="accessReports" [(ngModel)]="permissions.accessReports">
<label class="form-check-label font-weight-normal" for="accessReports">
{{'accessReports' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="manageAllCollections"
id="manageAllCollections" [(ngModel)]="permissions.manageAllCollections">
<label class="form-check-label font-weight-normal" for="manageAllCollections">
{{'manageAllCollections' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="manageGroups"
id="manageGroups" [(ngModel)]="permissions.manageGroups">
<label class="form-check-label font-weight-normal" for="manageGroups">
{{'manageGroups' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="manageSso"
id="managePolicies" [(ngModel)]="permissions.manageSso">
<label class="form-check-label font-weight-normal" for="manageSso">
{{'manageSso' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="managePolicies"
id="managePolicies" [(ngModel)]="permissions.managePolicies">
<label class="form-check-label font-weight-normal" for="managePolicies">
{{'managePolicies' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="manageUsers"
id="manageUsers" [(ngModel)]="permissions.manageUsers">
<label class="form-check-label font-weight-normal" for="manageUsers">
{{'manageUsers' | i18n}}
</label>
</div>
</div>
</div>
</div>
</div>
</ng-container>
<h3 class="mt-4 d-flex"> <h3 class="mt-4 d-flex">
<div class="mb-2"> <div class="mb-3">
{{'accessControl' | i18n}} {{'accessControl' | i18n}}
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}" <a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
href="https://bitwarden.com/help/article/user-types-access-control/#access-control"> href="https://bitwarden.com/help/article/user-types-access-control/#access-control">

View File

@ -23,6 +23,7 @@ import { CollectionDetailsResponse } from 'jslib/models/response/collectionRespo
import { CollectionView } from 'jslib/models/view/collectionView'; import { CollectionView } from 'jslib/models/view/collectionView';
import { OrganizationUserType } from 'jslib/enums/organizationUserType'; import { OrganizationUserType } from 'jslib/enums/organizationUserType';
import { PermissionsApi } from 'jslib/models/api/permissionsApi';
@Component({ @Component({
selector: 'app-user-add-edit', selector: 'app-user-add-edit',
@ -40,12 +41,18 @@ export class UserAddEditComponent implements OnInit {
title: string; title: string;
emails: string; emails: string;
type: OrganizationUserType = OrganizationUserType.User; type: OrganizationUserType = OrganizationUserType.User;
permissions = new PermissionsApi();
showCustom = false;
access: 'all' | 'selected' = 'selected'; access: 'all' | 'selected' = 'selected';
collections: CollectionView[] = []; collections: CollectionView[] = [];
formPromise: Promise<any>; formPromise: Promise<any>;
deletePromise: Promise<any>; deletePromise: Promise<any>;
organizationUserType = OrganizationUserType; organizationUserType = OrganizationUserType;
get customUserTypeSelected(): boolean {
return this.type === OrganizationUserType.Custom;
}
constructor(private apiService: ApiService, private i18nService: I18nService, constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService, private analytics: Angulartics2, private toasterService: ToasterService,
private collectionService: CollectionService, private platformUtilsService: PlatformUtilsService) { } private collectionService: CollectionService, private platformUtilsService: PlatformUtilsService) { }
@ -61,6 +68,9 @@ export class UserAddEditComponent implements OnInit {
const user = await this.apiService.getOrganizationUser(this.organizationId, this.organizationUserId); const user = await this.apiService.getOrganizationUser(this.organizationId, this.organizationUserId);
this.access = user.accessAll ? 'all' : 'selected'; this.access = user.accessAll ? 'all' : 'selected';
this.type = user.type; this.type = user.type;
if (user.type === OrganizationUserType.Custom) {
this.permissions = user.permissions;
}
if (user.collections != null && this.collections != null) { if (user.collections != null && this.collections != null) {
user.collections.forEach((s) => { user.collections.forEach((s) => {
const collection = this.collections.filter((c) => c.id === s.id); const collection = this.collections.filter((c) => c.id === s.id);
@ -97,6 +107,40 @@ export class UserAddEditComponent implements OnInit {
this.collections.forEach((c) => this.check(c, select)); this.collections.forEach((c) => this.check(c, select));
} }
setRequestPermissions(p: PermissionsApi, clearPermissions: boolean) {
p.accessBusinessPortal = clearPermissions ?
false :
this.permissions.accessBusinessPortal;
p.accessEventLogs = this.permissions.accessEventLogs = clearPermissions ?
false :
this.permissions.accessEventLogs;
p.accessImportExport = clearPermissions ?
false :
this.permissions.accessImportExport;
p.accessReports = clearPermissions ?
false :
this.permissions.accessReports;
p.manageAllCollections = clearPermissions ?
false :
this.permissions.manageAllCollections;
p.manageAssignedCollections = clearPermissions ?
false :
this.permissions.manageAssignedCollections;
p.manageGroups = clearPermissions ?
false :
this.permissions.manageGroups;
p.manageSso = clearPermissions ?
false :
this.permissions.manageSso;
p.managePolicies = clearPermissions ?
false :
this.permissions.managePolicies;
p.manageUsers = clearPermissions ?
false :
this.permissions.manageUsers;
return p;
}
async submit() { async submit() {
let collections: SelectionReadOnlyRequest[] = null; let collections: SelectionReadOnlyRequest[] = null;
if (this.access !== 'all') { if (this.access !== 'all') {
@ -110,6 +154,7 @@ export class UserAddEditComponent implements OnInit {
request.accessAll = this.access === 'all'; request.accessAll = this.access === 'all';
request.type = this.type; request.type = this.type;
request.collections = collections; request.collections = collections;
request.permissions = this.setRequestPermissions(request.permissions ?? new PermissionsApi(), request.type !== OrganizationUserType.Custom);
this.formPromise = this.apiService.putOrganizationUser(this.organizationId, this.organizationUserId, this.formPromise = this.apiService.putOrganizationUser(this.organizationId, this.organizationUserId,
request); request);
} else { } else {
@ -117,6 +162,7 @@ export class UserAddEditComponent implements OnInit {
request.emails = this.emails.trim().split(/\s*,\s*/); request.emails = this.emails.trim().split(/\s*,\s*/);
request.accessAll = this.access === 'all'; request.accessAll = this.access === 'all';
request.type = this.type; request.type = this.type;
request.permissions = this.setRequestPermissions(request.permissions ?? new PermissionsApi(), request.type !== OrganizationUserType.Custom);
request.collections = collections; request.collections = collections;
this.formPromise = this.apiService.postOrganizationUserInvite(this.organizationId, request); this.formPromise = this.apiService.postOrganizationUserInvite(this.organizationId, request);
} }
@ -148,4 +194,5 @@ export class UserAddEditComponent implements OnInit {
this.onDeletedUser.emit(); this.onDeletedUser.emit();
} catch { } } catch { }
} }
} }

View File

@ -14,12 +14,15 @@ import {
} from '../../tools/exposed-passwords-report.component'; } from '../../tools/exposed-passwords-report.component';
import { CipherView } from 'jslib/models/view/cipherView'; import { CipherView } from 'jslib/models/view/cipherView';
import { Cipher } from 'jslib/models/domain/cipher';
@Component({ @Component({
selector: 'app-exposed-passwords-report', selector: 'app-exposed-passwords-report',
templateUrl: '../../tools/exposed-passwords-report.component.html', templateUrl: '../../tools/exposed-passwords-report.component.html',
}) })
export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportComponent { export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportComponent {
manageableCiphers: Cipher[];
constructor(cipherService: CipherService, auditService: AuditService, constructor(cipherService: CipherService, auditService: AuditService,
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService, componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
userService: UserService, private route: ActivatedRoute) { userService: UserService, private route: ActivatedRoute) {
@ -29,6 +32,7 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC
ngOnInit() { ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => { this.route.parent.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId); this.organization = await this.userService.getOrganization(params.organizationId);
this.manageableCiphers = await this.cipherService.getAll();
super.ngOnInit(); super.ngOnInit();
}); });
} }
@ -36,4 +40,8 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC
getAllCiphers(): Promise<CipherView[]> { getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllFromApiForOrganization(this.organization.id); return this.cipherService.getAllFromApiForOrganization(this.organization.id);
} }
canManageCipher(c: CipherView): boolean {
return this.manageableCiphers.some(x => x.id === c.id);
}
} }

View File

@ -8,6 +8,8 @@ import { CipherService } from 'jslib/abstractions/cipher.service';
import { MessagingService } from 'jslib/abstractions/messaging.service'; import { MessagingService } from 'jslib/abstractions/messaging.service';
import { UserService } from 'jslib/abstractions/user.service'; import { UserService } from 'jslib/abstractions/user.service';
import { Cipher } from 'jslib/models/domain/cipher';
import { CipherView } from 'jslib/models/view/cipherView'; import { CipherView } from 'jslib/models/view/cipherView';
import { import {
@ -19,6 +21,8 @@ import {
templateUrl: '../../tools/reused-passwords-report.component.html', templateUrl: '../../tools/reused-passwords-report.component.html',
}) })
export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportComponent { export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportComponent {
manageableCiphers: Cipher[];
constructor(cipherService: CipherService, componentFactoryResolver: ComponentFactoryResolver, constructor(cipherService: CipherService, componentFactoryResolver: ComponentFactoryResolver,
messagingService: MessagingService, userService: UserService, messagingService: MessagingService, userService: UserService,
private route: ActivatedRoute) { private route: ActivatedRoute) {
@ -28,6 +32,7 @@ export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportCom
async ngOnInit() { async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => { this.route.parent.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId); this.organization = await this.userService.getOrganization(params.organizationId);
this.manageableCiphers = await this.cipherService.getAll();
await super.ngOnInit(); await super.ngOnInit();
}); });
} }
@ -35,4 +40,8 @@ export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportCom
getAllCiphers(): Promise<CipherView[]> { getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllFromApiForOrganization(this.organization.id); return this.cipherService.getAllFromApiForOrganization(this.organization.id);
} }
canManageCipher(c: CipherView): boolean {
return this.manageableCiphers.some(x => x.id === c.id);
}
} }

View File

@ -1,48 +1,54 @@
<div class="container page-content"> <div class="container page-content">
<div class="row"> <ng-container *ngIf="loading">
<div class="col-3"> <i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<div class="card mb-4"> <span class="sr-only">{{'loading' | i18n}}</span>
<div class="card-header">{{'tools' | i18n}}</div> </ng-container>
<div class="list-group list-group-flush"> <ng-container *ngIf="!loading">
<a routerLink="import" class="list-group-item" routerLinkActive="active"> <div class="row">
{{'importData' | i18n}} <div class="col-3">
</a> <div class="card mb-4" *ngIf="organization.canAccessImportExport">
<a routerLink="export" class="list-group-item" routerLinkActive="active"> <div class="card-header">{{'tools' | i18n}}</div>
{{'exportVault' | i18n}} <div class="list-group list-group-flush">
</a> <a routerLink="import" class="list-group-item" routerLinkActive="active">
</div> {{'importData' | i18n}}
</div> </a>
<div class="card"> <a routerLink="export" class="list-group-item" routerLinkActive="active">
<div class="card-header d-flex"> {{'exportVault' | i18n}}
{{'reports' | i18n}}
<div class="ml-auto">
<a href="#" appStopClick class="badge badge-primary" *ngIf="!accessReports"
(click)="upgradeOrganization()">
{{'upgrade' | i18n}}
</a> </a>
</div> </div>
</div> </div>
<div class="list-group list-group-flush"> <div class="card" *ngIf="organization.canAccessReports">
<a routerLink="exposed-passwords-report" class="list-group-item" routerLinkActive="active"> <div class="card-header d-flex">
{{'exposedPasswordsReport' | i18n}} {{'reports' | i18n}}
</a> <div class="ml-auto">
<a routerLink="reused-passwords-report" class="list-group-item" routerLinkActive="active"> <a href="#" appStopClick class="badge badge-primary" *ngIf="!accessReports"
{{'reusedPasswordsReport' | i18n}} (click)="upgradeOrganization()">
</a> {{'upgrade' | i18n}}
<a routerLink="weak-passwords-report" class="list-group-item" routerLinkActive="active"> </a>
{{'weakPasswordsReport' | i18n}} </div>
</a> </div>
<a routerLink="unsecured-websites-report" class="list-group-item" routerLinkActive="active"> <div class="list-group list-group-flush">
{{'unsecuredWebsitesReport' | i18n}} <a routerLink="exposed-passwords-report" class="list-group-item" routerLinkActive="active">
</a> {{'exposedPasswordsReport' | i18n}}
<a routerLink="inactive-two-factor-report" class="list-group-item" routerLinkActive="active"> </a>
{{'inactive2faReport' | i18n}} <a routerLink="reused-passwords-report" class="list-group-item" routerLinkActive="active">
</a> {{'reusedPasswordsReport' | i18n}}
</a>
<a routerLink="weak-passwords-report" class="list-group-item" routerLinkActive="active">
{{'weakPasswordsReport' | i18n}}
</a>
<a routerLink="unsecured-websites-report" class="list-group-item" routerLinkActive="active">
{{'unsecuredWebsitesReport' | i18n}}
</a>
<a routerLink="inactive-two-factor-report" class="list-group-item" routerLinkActive="active">
{{'inactive2faReport' | i18n}}
</a>
</div>
</div> </div>
</div> </div>
<div class="col-9">
<router-outlet></router-outlet>
</div>
</div> </div>
<div class="col-9"> </ng-container>
<router-outlet></router-outlet>
</div>
</div>
</div> </div>

View File

@ -13,6 +13,7 @@ import { UserService } from 'jslib/abstractions/user.service';
export class ToolsComponent { export class ToolsComponent {
organization: Organization; organization: Organization;
accessReports = false; accessReports = false;
loading = true;
constructor(private route: ActivatedRoute, private userService: UserService, constructor(private route: ActivatedRoute, private userService: UserService,
private messagingService: MessagingService) { } private messagingService: MessagingService) { }
@ -23,6 +24,7 @@ export class ToolsComponent {
// TODO: Maybe we want to just make sure they are not on a free plan? Just compare useTotp for now // TODO: Maybe we want to just make sure they are not on a free plan? Just compare useTotp for now
// since all paid plans include useTotp // since all paid plans include useTotp
this.accessReports = this.organization.useTotp; this.accessReports = this.organization.useTotp;
this.loading = false;
}); });
} }

View File

@ -9,6 +9,8 @@ import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { UserService } from 'jslib/abstractions/user.service'; import { UserService } from 'jslib/abstractions/user.service';
import { Cipher } from 'jslib/models/domain/cipher';
import { CipherView } from 'jslib/models/view/cipherView'; import { CipherView } from 'jslib/models/view/cipherView';
import { import {
@ -20,6 +22,8 @@ import {
templateUrl: '../../tools/weak-passwords-report.component.html', templateUrl: '../../tools/weak-passwords-report.component.html',
}) })
export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportComponent { export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportComponent {
manageableCiphers: Cipher[];
constructor(cipherService: CipherService, passwordGenerationService: PasswordGenerationService, constructor(cipherService: CipherService, passwordGenerationService: PasswordGenerationService,
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService, componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
userService: UserService, private route: ActivatedRoute) { userService: UserService, private route: ActivatedRoute) {
@ -29,6 +33,7 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone
async ngOnInit() { async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => { this.route.parent.parent.params.subscribe(async (params) => {
this.organization = await this.userService.getOrganization(params.organizationId); this.organization = await this.userService.getOrganization(params.organizationId);
this.manageableCiphers = await this.cipherService.getAll();
await super.ngOnInit(); await super.ngOnInit();
}); });
} }
@ -36,4 +41,8 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone
getAllCiphers(): Promise<CipherView[]> { getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllFromApiForOrganization(this.organization.id); return this.cipherService.getAllFromApiForOrganization(this.organization.id);
} }
canManageCipher(c: CipherView): boolean {
return this.manageableCiphers.some(x => x.id === c.id);
}
} }

View File

@ -46,7 +46,7 @@ export class AddEditComponent extends BaseAddEditComponent {
protected allowOwnershipAssignment() { protected allowOwnershipAssignment() {
if (this.ownershipOptions != null && (this.ownershipOptions.length > 1 || !this.allowPersonal)) { if (this.ownershipOptions != null && (this.ownershipOptions.length > 1 || !this.allowPersonal)) {
if (this.organization != null) { if (this.organization != null) {
return this.cloneMode && this.organization.isAdmin; return this.cloneMode && this.organization.canManageAllCollections;
} else { } else {
return !this.editMode || this.cloneMode; return !this.editMode || this.cloneMode;
} }
@ -55,14 +55,14 @@ export class AddEditComponent extends BaseAddEditComponent {
} }
protected loadCollections() { protected loadCollections() {
if (!this.organization.isAdmin) { if (!this.organization.canManageAllCollections) {
return super.loadCollections(); return super.loadCollections();
} }
return Promise.resolve(this.collections); return Promise.resolve(this.collections);
} }
protected async loadCipher() { protected async loadCipher() {
if (!this.organization.isAdmin) { if (!this.organization.canManageAllCollections) {
return await super.loadCipher(); return await super.loadCipher();
} }
const response = await this.apiService.getCipherAdmin(this.cipherId); const response = await this.apiService.getCipherAdmin(this.cipherId);
@ -72,14 +72,14 @@ export class AddEditComponent extends BaseAddEditComponent {
} }
protected encryptCipher() { protected encryptCipher() {
if (!this.organization.isAdmin) { if (!this.organization.canManageAllCollections) {
return super.encryptCipher(); return super.encryptCipher();
} }
return this.cipherService.encrypt(this.cipher, null, this.originalCipher); return this.cipherService.encrypt(this.cipher, null, this.originalCipher);
} }
protected async saveCipher(cipher: Cipher) { protected async saveCipher(cipher: Cipher) {
if (!this.organization.isAdmin || cipher.organizationId == null) { if (!this.organization.canManageAllCollections || cipher.organizationId == null) {
return super.saveCipher(cipher); return super.saveCipher(cipher);
} }
if (this.editMode && !this.cloneMode) { if (this.editMode && !this.cloneMode) {
@ -92,7 +92,7 @@ export class AddEditComponent extends BaseAddEditComponent {
} }
protected async deleteCipher() { protected async deleteCipher() {
if (!this.organization.isAdmin) { if (!this.organization.canManageAllCollections) {
return super.deleteCipher(); return super.deleteCipher();
} }
return this.cipher.isDeleted ? this.apiService.deleteCipherAdmin(this.cipherId) return this.cipher.isDeleted ? this.apiService.deleteCipherAdmin(this.cipherId)

View File

@ -29,13 +29,13 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
} }
protected async reupload(attachment: AttachmentView) { protected async reupload(attachment: AttachmentView) {
if (this.organization.isAdmin && this.showFixOldAttachments(attachment)) { if (this.organization.canManageAllCollections && this.showFixOldAttachments(attachment)) {
await super.reuploadCipherAttachment(attachment, true); await super.reuploadCipherAttachment(attachment, true);
} }
} }
protected async loadCipher() { protected async loadCipher() {
if (!this.organization.isAdmin) { if (!this.organization.canManageAllCollections) {
return await super.loadCipher(); return await super.loadCipher();
} }
const response = await this.apiService.getCipherAdmin(this.cipherId); const response = await this.apiService.getCipherAdmin(this.cipherId);
@ -43,17 +43,17 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
} }
protected saveCipherAttachment(file: File) { protected saveCipherAttachment(file: File) {
return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file, this.organization.isAdmin); return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file, this.organization.canManageAllCollections);
} }
protected deleteCipherAttachment(attachmentId: string) { protected deleteCipherAttachment(attachmentId: string) {
if (!this.organization.isAdmin) { if (!this.organization.canManageAllCollections) {
return super.deleteCipherAttachment(attachmentId); return super.deleteCipherAttachment(attachmentId);
} }
return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId); return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId);
} }
protected showFixOldAttachments(attachment: AttachmentView) { protected showFixOldAttachments(attachment: AttachmentView) {
return attachment.key == null && this.organization.isAdmin; return attachment.key == null && this.organization.canManageAllCollections;
} }
} }

View File

@ -42,7 +42,7 @@ export class CiphersComponent extends BaseCiphersComponent {
} }
async load(filter: (cipher: CipherView) => boolean = null) { async load(filter: (cipher: CipherView) => boolean = null) {
if (!this.organization.isAdmin) { if (!this.organization.canManageAllCollections) {
await super.load(filter, this.deleted); await super.load(filter, this.deleted);
return; return;
} }
@ -53,7 +53,7 @@ export class CiphersComponent extends BaseCiphersComponent {
} }
async applyFilter(filter: (cipher: CipherView) => boolean = null) { async applyFilter(filter: (cipher: CipherView) => boolean = null) {
if (this.organization.isAdmin) { if (this.organization.canManageAllCollections) {
await super.applyFilter(filter); await super.applyFilter(filter);
} else { } else {
const f = (c: CipherView) => c.organizationId === this.organization.id && (filter == null || filter(c)); const f = (c: CipherView) => c.organizationId === this.organization.id && (filter == null || filter(c));
@ -62,7 +62,7 @@ export class CiphersComponent extends BaseCiphersComponent {
} }
async search(timeout: number = null) { async search(timeout: number = null) {
if (!this.organization.isAdmin) { if (!this.organization.canManageAllCollections) {
return super.search(timeout); return super.search(timeout);
} }
this.searchPending = false; this.searchPending = false;
@ -89,13 +89,13 @@ export class CiphersComponent extends BaseCiphersComponent {
} }
protected deleteCipher(id: string) { protected deleteCipher(id: string) {
if (!this.organization.isAdmin) { if (!this.organization.canManageAllCollections) {
return super.deleteCipher(id, this.deleted); return super.deleteCipher(id, this.deleted);
} }
return this.deleted ? this.apiService.deleteCipherAdmin(id) : this.apiService.putDeleteCipherAdmin(id); return this.deleted ? this.apiService.deleteCipherAdmin(id) : this.apiService.putDeleteCipherAdmin(id);
} }
protected showFixOldAttachments(c: CipherView) { protected showFixOldAttachments(c: CipherView) {
return this.organization.isAdmin && c.hasOldAttachments; return this.organization.canManageAllCollections && c.hasOldAttachments;
} }
} }

View File

@ -28,7 +28,7 @@ export class CollectionsComponent extends BaseCollectionsComponent {
} }
protected async loadCipher() { protected async loadCipher() {
if (!this.organization.isAdmin) { if (!this.organization.canManageAllCollections) {
return await super.loadCipher(); return await super.loadCipher();
} }
const response = await this.apiService.getCipherAdmin(this.cipherId); const response = await this.apiService.getCipherAdmin(this.cipherId);
@ -36,21 +36,21 @@ export class CollectionsComponent extends BaseCollectionsComponent {
} }
protected loadCipherCollections() { protected loadCipherCollections() {
if (!this.organization.isAdmin) { if (!this.organization.canManageAllCollections) {
return super.loadCipherCollections(); return super.loadCipherCollections();
} }
return this.collectionIds; return this.collectionIds;
} }
protected loadCollections() { protected loadCollections() {
if (!this.organization.isAdmin) { if (!this.organization.canManageAllCollections) {
return super.loadCollections(); return super.loadCollections();
} }
return Promise.resolve(this.collections); return Promise.resolve(this.collections);
} }
protected saveCollections() { protected saveCollections() {
if (this.organization.isAdmin) { if (this.organization.canManageAllCollections) {
const request = new CipherCollectionsRequest(this.cipherDomain.collectionIds); const request = new CipherCollectionsRequest(this.cipherDomain.collectionIds);
return this.apiService.putCipherCollectionsAdmin(this.cipherId, request); return this.apiService.putCipherCollectionsAdmin(this.cipherId, request);
} else { } else {

View File

@ -29,7 +29,7 @@ export class GroupingsComponent extends BaseGroupingsComponent {
} }
async loadCollections() { async loadCollections() {
if (!this.organization.isAdmin) { if (!this.organization.canManageAllCollections) {
await super.loadCollections(this.organization.id); await super.loadCollections(this.organization.id);
return; return;
} }

View File

@ -69,7 +69,7 @@ export class VaultComponent implements OnInit, OnDestroy {
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => { const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
this.ciphersComponent.searchText = this.groupingsComponent.searchText = qParams.search; this.ciphersComponent.searchText = this.groupingsComponent.searchText = qParams.search;
if (!this.organization.isAdmin) { if (!this.organization.canManageAllCollections) {
await this.syncService.fullSync(false); await this.syncService.fullSync(false);
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
this.ngZone.run(async () => { this.ngZone.run(async () => {
@ -233,7 +233,7 @@ export class VaultComponent implements OnInit, OnDestroy {
this.modal = this.collectionsModalRef.createComponent(factory).instance; this.modal = this.collectionsModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<CollectionsComponent>(CollectionsComponent, this.collectionsModalRef); const childComponent = this.modal.show<CollectionsComponent>(CollectionsComponent, this.collectionsModalRef);
if (this.organization.isAdmin) { if (this.organization.canManageAllCollections) {
childComponent.collectionIds = cipher.collectionIds; childComponent.collectionIds = cipher.collectionIds;
childComponent.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly); childComponent.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly);
} }
@ -253,7 +253,7 @@ export class VaultComponent implements OnInit, OnDestroy {
const component = this.editCipher(null); const component = this.editCipher(null);
component.organizationId = this.organization.id; component.organizationId = this.organization.id;
component.type = this.type; component.type = this.type;
if (this.organization.isAdmin) { if (this.organization.canManageAllCollections) {
component.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly); component.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly);
} }
if (this.collectionId != null) { if (this.collectionId != null) {
@ -296,7 +296,7 @@ export class VaultComponent implements OnInit, OnDestroy {
const component = this.editCipher(cipher); const component = this.editCipher(cipher);
component.cloneMode = true; component.cloneMode = true;
component.organizationId = this.organization.id; component.organizationId = this.organization.id;
if (this.organization.isAdmin) { if (this.organization.canManageAllCollections) {
component.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly); component.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly);
} }
// Regardless of Admin state, the collection Ids need to passed manually as they are not assigned value // Regardless of Admin state, the collection Ids need to passed manually as they are not assigned value

View File

@ -7,20 +7,32 @@ import {
import { UserService } from 'jslib/abstractions/user.service'; import { UserService } from 'jslib/abstractions/user.service';
import { OrganizationUserType } from 'jslib/enums/organizationUserType'; import { Permissions } from 'jslib/enums/permissions';
@Injectable() @Injectable()
export class OrganizationTypeGuardService implements CanActivate { export class OrganizationTypeGuardService implements CanActivate {
constructor(private userService: UserService, private router: Router) { } constructor(private userService: UserService, private router: Router) { }
async canActivate(route: ActivatedRouteSnapshot) { async canActivate(route: ActivatedRouteSnapshot) {
const org = await this.userService.getOrganization(route.parent.params.organizationId); const org = await this.userService.getOrganization(route.params.organizationId);
const allowedTypes = route.data == null ? null : route.data.allowedTypes as OrganizationUserType[]; const permissions = route.data == null ? null : route.data.permissions as Permissions[];
if (allowedTypes == null || allowedTypes.indexOf(org.type) === -1) {
this.router.navigate(['/organizations', org.id]); if (
return false; (permissions.indexOf(Permissions.AccessBusinessPortal) !== -1 && org.canAccessBusinessPortal) ||
(permissions.indexOf(Permissions.AccessEventLogs) !== -1 && org.canAccessEventLogs) ||
(permissions.indexOf(Permissions.AccessImportExport) !== -1 && org.canAccessImportExport) ||
(permissions.indexOf(Permissions.AccessReports) !== -1 && org.canAccessReports) ||
(permissions.indexOf(Permissions.ManageAllCollections) !== -1 && org.canManageAllCollections) ||
(permissions.indexOf(Permissions.ManageAssignedCollections) !== -1 && org.canManageAssignedCollections) ||
(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)
) {
return true;
} }
return true; this.router.navigate(['/organizations', org.id]);
return false;
} }
} }

View File

@ -21,7 +21,12 @@
<app-vault-icon [cipher]="c"></app-vault-icon> <app-vault-icon [cipher]="c"></app-vault-icon>
</td> </td>
<td class="reduced-lh wrap"> <td class="reduced-lh wrap">
<a href="#" appStopClick (click)="selectCipher(c)" title="{{'editItem' | i18n}}">{{c.name}}</a> <ng-container *ngIf="!organization || canManageCipher(c) ; else cantManage">
<a href="#" appStopClick (click)="selectCipher(c)" title="{{'editItem' | i18n}}">{{c.name}}</a>
</ng-container>
<ng-template #cantManage>
<span>{{c.name}}</span>
</ng-template>
<ng-container *ngIf="!organization && c.organizationId"> <ng-container *ngIf="!organization && c.organizationId">
<i class="fa fa-share-alt" appStopProp title="{{'shared' | i18n}}" aria-hidden="true"></i> <i class="fa fa-share-alt" appStopProp title="{{'shared' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'shared' | i18n}}</span> <span class="sr-only">{{'shared' | i18n}}</span>

View File

@ -61,4 +61,9 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
protected getAllCiphers(): Promise<CipherView[]> { protected getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllDecrypted(); return this.cipherService.getAllDecrypted();
} }
protected canManageCipher(c: CipherView): boolean {
// this will only ever be false from the org view;
return true;
}
} }

View File

@ -27,7 +27,12 @@
<app-vault-icon [cipher]="c"></app-vault-icon> <app-vault-icon [cipher]="c"></app-vault-icon>
</td> </td>
<td class="reduced-lh wrap"> <td class="reduced-lh wrap">
<a href="#" appStopClick (click)="selectCipher(c)" title="{{'editItem' | i18n}}">{{c.name}}</a> <ng-container *ngIf="!organization || canManageCipher(c) ; else cantManage">
<a href="#" appStopClick (click)="selectCipher(c)" title="{{'editItem' | i18n}}">{{c.name}}</a>
</ng-container>
<ng-template #cantManage>
<span>{{c.name}}</span>
</ng-template>
<ng-container *ngIf="!organization && c.organizationId"> <ng-container *ngIf="!organization && c.organizationId">
<i class="fa fa-share-alt" appStopProp title="{{'shared' | i18n}}" aria-hidden="true"></i> <i class="fa fa-share-alt" appStopProp title="{{'shared' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'shared' | i18n}}</span> <span class="sr-only">{{'shared' | i18n}}</span>

View File

@ -55,4 +55,9 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
protected getAllCiphers(): Promise<CipherView[]> { protected getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllDecrypted(); return this.cipherService.getAllDecrypted();
} }
protected canManageCipher(c: CipherView): boolean {
// this will only ever be false from an organization view
return true;
}
} }

View File

@ -27,7 +27,12 @@
<app-vault-icon [cipher]="c"></app-vault-icon> <app-vault-icon [cipher]="c"></app-vault-icon>
</td> </td>
<td class="reduced-lh wrap"> <td class="reduced-lh wrap">
<a href="#" appStopClick (click)="selectCipher(c)" title="{{'editItem' | i18n}}">{{c.name}}</a> <ng-container *ngIf="!organization || canManageCipher(c) ; else cantManage">
<a href="#" appStopClick (click)="selectCipher(c)" title="{{'editItem' | i18n}}">{{c.name}}</a>
</ng-container>
<ng-template #cantManage>
<span>{{c.name}}</span>
</ng-template>
<ng-container *ngIf="!organization && c.organizationId"> <ng-container *ngIf="!organization && c.organizationId">
<i class="fa fa-share-alt" appStopProp title="{{'shared' | i18n}}" aria-hidden="true"></i> <i class="fa fa-share-alt" appStopProp title="{{'shared' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'shared' | i18n}}</span> <span class="sr-only">{{'shared' | i18n}}</span>

View File

@ -75,6 +75,11 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
return this.cipherService.getAllDecrypted(); return this.cipherService.getAllDecrypted();
} }
protected canManageCipher(c: CipherView): boolean {
// this will only ever be false from the org view;
return true;
}
private scoreKey(score: number): [string, string] { private scoreKey(score: number): [string, string] {
switch (score) { switch (score) {
case 4: case 4:

View File

@ -31,7 +31,7 @@ export class BulkDeleteComponent {
private apiService: ApiService) { } private apiService: ApiService) { }
async submit() { async submit() {
if (!this.organization || !this.organization.isAdmin) { if (!this.organization || !this.organization.canManageAllCollections) {
await this.deleteCiphers(); await this.deleteCiphers();
} else { } else {
await this.deleteCiphersAdmin(); await this.deleteCiphersAdmin();

View File

@ -3577,6 +3577,45 @@
"estimatedTax": { "estimatedTax": {
"message": "Estimated tax" "message": "Estimated tax"
}, },
"custom": {
"message": "Custom"
},
"customDesc": {
"message": "Allows more granular control of user permissions for advanced configurations."
},
"permissions": {
"message": "Permissions"
},
"accessBusinessPortal": {
"message": "Access Business Portal"
},
"accessEventLogs": {
"message": "Access Event Logs"
},
"accessImportExport": {
"message": "Access Import/Export"
},
"accessReports": {
"message": "Access Reports"
},
"manageAllCollections": {
"message": "Manage All Collections"
},
"manageAssignedCollections": {
"message": "Manage Assigned Collections"
},
"manageGroups": {
"message": "Manage Groups"
},
"managePolicies": {
"message": "Manage Policies"
},
"manageSso": {
"message": "Manage Sso"
},
"manageUsers": {
"message": "Manage Users"
},
"disableRequireSsoError": { "disableRequireSsoError": {
"message": "You must manually disable the Single Sign-On Authentication policy before this policy can be disabled." "message": "You must manually disable the Single Sign-On Authentication policy before this policy can be disabled."
}, },

View File

@ -53,6 +53,13 @@
"semicolon": [ "semicolon": [
true, true,
"always" "always"
],
"trailing-comma": [
true,
{
"multiline": "always",
"singleline": "never"
}
] ]
} }
} }