diff --git a/bitwarden_license/src/app/app.component.ts b/bitwarden_license/src/app/app.component.ts new file mode 100644 index 0000000000..33c6e828c8 --- /dev/null +++ b/bitwarden_license/src/app/app.component.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +import { AppComponent as BaseAppComponent } from 'src/app/app.component'; +import { MaximumVaultTimeoutPolicy } from './policies/maximum-vault-timeout.component'; + +@Component({ + selector: 'app-root', + templateUrl: '../../../src/app/app.component.html', +}) +export class AppComponent extends BaseAppComponent { + + ngOnInit() { + super.ngOnInit(); + + this.policyListService.addPolicies([ + new MaximumVaultTimeoutPolicy(), + ]); + } + +} diff --git a/bitwarden_license/src/app/app.module.ts b/bitwarden_license/src/app/app.module.ts index cdf705c1c3..c69bc1b356 100644 --- a/bitwarden_license/src/app/app.module.ts +++ b/bitwarden_license/src/app/app.module.ts @@ -3,27 +3,36 @@ import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { DragDropModule } from '@angular/cdk/drag-drop'; import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterModule } from '@angular/router'; import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; +import { MaximumVaultTimeoutPolicyComponent } from './policies/maximum-vault-timeout.component'; -import { AppComponent } from 'src/app/app.component'; import { OssRoutingModule } from 'src/app/oss-routing.module'; import { OssModule } from 'src/app/oss.module'; import { ServicesModule } from 'src/app/services/services.module'; + @NgModule({ imports: [ OssModule, BrowserAnimationsModule, FormsModule, + ReactiveFormsModule, ServicesModule, ToasterModule.forRoot(), InfiniteScrollModule, DragDropModule, AppRoutingModule, OssRoutingModule, + RouterModule, + ], + declarations: [ + AppComponent, + MaximumVaultTimeoutPolicyComponent, ], bootstrap: [AppComponent], }) diff --git a/bitwarden_license/src/app/policies/maximum-vault-timeout.component.html b/bitwarden_license/src/app/policies/maximum-vault-timeout.component.html new file mode 100644 index 0000000000..ac43f2d120 --- /dev/null +++ b/bitwarden_license/src/app/policies/maximum-vault-timeout.component.html @@ -0,0 +1,27 @@ + + {{'requireSsoPolicyReq' | i18n}} + + +
+
+ + +
+
+ +
+
+ +
+
+ + {{'hours' | i18n }} +
+
+ + {{'minutes' | i18n }} +
+
+
+
diff --git a/bitwarden_license/src/app/policies/maximum-vault-timeout.component.ts b/bitwarden_license/src/app/policies/maximum-vault-timeout.component.ts new file mode 100644 index 0000000000..496cf5a3e2 --- /dev/null +++ b/bitwarden_license/src/app/policies/maximum-vault-timeout.component.ts @@ -0,0 +1,65 @@ +import { Component } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; + +import { I18nService } from 'jslib-common/abstractions/i18n.service'; + +import { PolicyType } from 'jslib-common/enums/policyType'; + +import { PolicyRequest } from 'jslib-common/models/request/policyRequest'; + +import { BasePolicy, BasePolicyComponent } from 'src/app/organizations/policies/base-policy.component'; + +export class MaximumVaultTimeoutPolicy extends BasePolicy { + name = 'maximumVaultTimeout'; + description = 'maximumVaultTimeoutDesc'; + type = PolicyType.MaximumVaultTimeout; + component = MaximumVaultTimeoutPolicyComponent; +} + +@Component({ + selector: 'policy-maximum-timeout', + templateUrl: 'maximum-vault-timeout.component.html', +}) +export class MaximumVaultTimeoutPolicyComponent extends BasePolicyComponent { + + data = this.fb.group({ + hours: [null], + minutes: [null], + }); + + constructor(private fb: FormBuilder, private i18nService: I18nService) { + super(); + } + + loadData() { + const minutes = this.policyResponse.data?.minutes; + + if (minutes == null) { + return; + } + + this.data.patchValue({ + hours: Math.floor(minutes / 60), + minutes: minutes % 60, + }); + } + + buildRequestData() { + if (this.data.value.hours == null && this.data.value.minutes == null) { + return null; + } + + return { + minutes: this.data.value.hours * 60 + this.data.value.minutes, + }; + } + + buildRequest(policiesEnabledMap: Map): Promise { + const singleOrgEnabled = policiesEnabledMap.get(PolicyType.SingleOrg) ?? false; + if (this.enabled.value && !singleOrgEnabled) { + throw new Error(this.i18nService.t('requireSsoPolicyReqError')); + } + + return super.buildRequest(policiesEnabledMap); + } +} diff --git a/jslib b/jslib index 5f64d95652..32774561f3 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit 5f64d956520612a681611a27c5f4f2e5f27b640e +Subproject commit 32774561f37bdcf9abb80276c5d1958b7ec192de diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 1066149beb..a8e2f88a92 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -22,6 +22,9 @@ import { ServicesModule } from './services/services.module'; DragDropModule, OssRoutingModule, ], + declarations: [ + AppComponent, + ], bootstrap: [AppComponent], }) export class AppModule { } diff --git a/src/app/organizations/policies/base-policy.component.ts b/src/app/organizations/policies/base-policy.component.ts index bc4b3e3d46..c42647a760 100644 --- a/src/app/organizations/policies/base-policy.component.ts +++ b/src/app/organizations/policies/base-policy.component.ts @@ -35,19 +35,28 @@ export abstract class BasePolicyComponent implements OnInit { ngOnInit(): void { this.enabled.setValue(this.policyResponse.enabled); - if (this.data != null) { - this.data.patchValue(this.policyResponse.data ?? {}); + if (this.policyResponse.data != null) { + this.loadData(); } } + loadData() { + this.data.patchValue(this.policyResponse.data ?? {}); + } + + buildRequestData() { + if (this.data != null) { + return this.data.value; + } + + return null; + } + buildRequest(policiesEnabledMap: Map) { const request = new PolicyRequest(); request.enabled = this.enabled.value; request.type = this.policy.type; - - if (this.data != null) { - request.data = this.data.value; - } + request.data = this.buildRequestData(); return Promise.resolve(request); } diff --git a/src/app/oss.module.ts b/src/app/oss.module.ts index 06b7301952..3ea40c4e4a 100644 --- a/src/app/oss.module.ts +++ b/src/app/oss.module.ts @@ -11,8 +11,6 @@ import { RouterModule } from '@angular/router'; import { ToasterModule } from 'angular2-toaster'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; -import { AppComponent } from './app.component'; - import { AvatarComponent } from './components/avatar.component'; import { PasswordRepromptComponent } from './components/password-reprompt.component'; import { PasswordStrengthComponent } from './components/password-strength.component'; @@ -144,6 +142,7 @@ import { UpdateKeyComponent } from './settings/update-key.component'; import { UpdateLicenseComponent } from './settings/update-license.component'; import { UserBillingComponent } from './settings/user-billing.component'; import { UserSubscriptionComponent } from './settings/user-subscription.component'; +import { VaultTimeoutInputComponent } from './settings/vault-timeout-input.component'; import { VerifyEmailComponent } from './settings/verify-email.component'; import { BreachReportComponent } from './tools/breach-report.component'; @@ -307,7 +306,6 @@ registerLocaleData(localeZhTw, 'zh-TW'); AdjustStorageComponent, ApiActionDirective, ApiKeyComponent, - AppComponent, AttachmentsComponent, AutofocusDirective, AvatarComponent, @@ -457,6 +455,7 @@ registerLocaleData(localeZhTw, 'zh-TW'); DisableSendPolicyComponent, SendOptionsPolicyComponent, ResetPasswordPolicyComponent, + VaultTimeoutInputComponent, ], exports: [ A11yTitleDirective, diff --git a/src/app/services/services.module.ts b/src/app/services/services.module.ts index 6639268f13..144fe7db6c 100644 --- a/src/app/services/services.module.ts +++ b/src/app/services/services.module.ts @@ -124,7 +124,7 @@ const sendService = new SendService(cryptoService, userService, apiService, file i18nService, cryptoFunctionService); const vaultTimeoutService = new VaultTimeoutService(cipherService, folderService, collectionService, cryptoService, platformUtilsService, storageService, messagingService, searchService, userService, tokenService, - null, async () => messagingService.send('logout', { expired: false })); + policyService, null, async () => messagingService.send('logout', { expired: false })); const syncService = new SyncService(userService, apiService, settingsService, folderService, cipherService, cryptoService, collectionService, storageService, messagingService, policyService, sendService, async (expired: boolean) => messagingService.send('logout', { expired: expired })); diff --git a/src/app/settings/options.component.html b/src/app/settings/options.component.html index b369b90f6f..8619be274a 100644 --- a/src/app/settings/options.component.html +++ b/src/app/settings/options.component.html @@ -5,13 +5,8 @@
-
- - - {{'vaultTimeoutDesc' | i18n}} -
+ +
diff --git a/src/app/settings/options.component.ts b/src/app/settings/options.component.ts index 755d31a792..aad583f5a1 100644 --- a/src/app/settings/options.component.ts +++ b/src/app/settings/options.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit, } from '@angular/core'; +import { FormControl } from '@angular/forms'; import { ToasterService } from 'angular2-toaster'; @@ -21,15 +22,16 @@ import { Utils } from 'jslib-common/misc/utils'; templateUrl: 'options.component.html', }) export class OptionsComponent implements OnInit { - vaultTimeout: number = null; vaultTimeoutAction: string = 'lock'; disableIcons: boolean; enableGravatars: boolean; enableFullWidth: boolean; locale: string; - vaultTimeouts: any[]; + vaultTimeouts: { name: string; value: number; }[]; localeOptions: any[]; + vaultTimeout: FormControl = new FormControl(null); + private startingLocale: string; constructor(private storageService: StorageService, private stateService: StateService, @@ -63,7 +65,7 @@ export class OptionsComponent implements OnInit { } async ngOnInit() { - this.vaultTimeout = await this.storageService.get(ConstantsService.vaultTimeoutKey); + this.vaultTimeout.setValue(await this.vaultTimeoutService.getVaultTimeout()); this.vaultTimeoutAction = await this.storageService.get(ConstantsService.vaultTimeoutActionKey); this.disableIcons = await this.storageService.get(ConstantsService.disableFaviconKey); this.enableGravatars = await this.storageService.get('enableGravatars'); @@ -72,8 +74,12 @@ export class OptionsComponent implements OnInit { } async submit() { - await this.vaultTimeoutService.setVaultTimeoutOptions(this.vaultTimeout != null ? this.vaultTimeout : null, - this.vaultTimeoutAction); + if (!this.vaultTimeout.valid) { + this.toasterService.popAsync('error', null, this.i18nService.t('vaultTimeoutToLarge')); + return; + } + + await this.vaultTimeoutService.setVaultTimeoutOptions(this.vaultTimeout.value, this.vaultTimeoutAction); await this.storageService.save(ConstantsService.disableFaviconKey, this.disableIcons); await this.stateService.save(ConstantsService.disableFaviconKey, this.disableIcons); await this.storageService.save('enableGravatars', this.enableGravatars); diff --git a/src/app/settings/vault-timeout-input.component.html b/src/app/settings/vault-timeout-input.component.html new file mode 100644 index 0000000000..9fd5d63102 --- /dev/null +++ b/src/app/settings/vault-timeout-input.component.html @@ -0,0 +1,28 @@ + + {{'vaultTimeoutPolicyInEffect' | i18n : vaultTimeoutPolicyHours : vaultTimeoutPolicyMinutes}} + + +
+
+ + + {{'vaultTimeoutDesc' | i18n}} +
+
+ +
+
+ + {{'hours' | i18n }} +
+
+ + {{'minutes' | i18n }} +
+
+
+
diff --git a/src/app/settings/vault-timeout-input.component.ts b/src/app/settings/vault-timeout-input.component.ts new file mode 100644 index 0000000000..47335e51c4 --- /dev/null +++ b/src/app/settings/vault-timeout-input.component.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; +import { + NG_VALIDATORS, + NG_VALUE_ACCESSOR, +} from '@angular/forms'; + +import { + VaultTimeoutInputComponent as VaultTimeoutInputComponentBase +} from 'jslib-angular/components/settings/vault-timeout-input.component'; + +@Component({ + selector: 'app-vault-timeout-input', + templateUrl: 'vault-timeout-input.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: VaultTimeoutInputComponent, + }, + { + provide: NG_VALIDATORS, + multi: true, + useExisting: VaultTimeoutInputComponent, + }, + ], +}) +export class VaultTimeoutInputComponent extends VaultTimeoutInputComponentBase { +} diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index c8c46ec2ed..b396298e8f 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -4198,5 +4198,39 @@ }, "updateMasterPasswordWarning": { "message": "Your Master Password was recently changed by an administrator in your organization. In order to access the vault, you must update your Master Password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + }, + "maximumVaultTimeout": { + "message": "Vault Timeout" + }, + "maximumVaultTimeoutDesc": { + "message": "Configure a maximum vault timeout for all users." + }, + "maximumVaultTimeoutLabel": { + "message": "Maximum Vault Timeout" + }, + "hours": { + "message": "Hours" + }, + "minutes": { + "message": "Minutes" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies are affecting your vault timeout. Maximum allowed Vault Timeout is $HOURS$ hour(s) and $MINUTES$ minute(s)", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "customVaultTimeout": { + "message": "Custom Vault Timeout" + }, + "vaultTimeoutToLarge": { + "message": "Your vault timeout exceeds the restriction set by your organization." } } diff --git a/src/services/webPlatformUtils.service.ts b/src/services/webPlatformUtils.service.ts index fb321f5bae..2a881600e0 100644 --- a/src/services/webPlatformUtils.service.ts +++ b/src/services/webPlatformUtils.service.ts @@ -82,10 +82,6 @@ export class WebPlatformUtilsService implements PlatformUtilsService { return Promise.resolve(false); } - lockTimeout(): number { - return null; - } - launchUri(uri: string, options?: any): void { const a = document.createElement('a'); a.href = uri;