diff --git a/jslib b/jslib index 0fa88b44b8..3b3b71d841 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit 0fa88b44b81730679fedf88a083b4b4b1f5c40ac +Subproject commit 3b3b71d84192cc195f4626d6294b34d788641215 diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 9b14e5afeb..2e745a869e 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -55,9 +55,11 @@ import { UserGroupsComponent as OrgUserGroupsComponent } from './organizations/m import { AccountComponent as OrgAccountComponent } from './organizations/settings/account.component'; import { AdjustSeatsComponent } from './organizations/settings/adjust-seats.component'; +import { ApiKeyComponent as OrgApiKeyComponent } from './organizations/settings/api-key.component'; import { DeleteOrganizationComponent } from './organizations/settings/delete-organization.component'; import { OrganizationBillingComponent } from './organizations/settings/organization-billing.component'; import { OrganizationSubscriptionComponent } from './organizations/settings/organization-subscription.component'; +import { RotateApiKeyComponent as OrgRotateApiKeyComponent } from './organizations/settings/rotate-api-key.component'; import { SettingsComponent as OrgSettingComponent } from './organizations/settings/settings.component'; import { TwoFactorSetupComponent as OrgTwoFactorSetupComponent, @@ -274,6 +276,7 @@ registerLocaleData(localeZhTw, 'zh-TW'); OptionsComponent, OrgAccountComponent, OrgAddEditComponent, + OrgApiKeyComponent, OrganizationBillingComponent, OrganizationSubscriptionComponent, OrgAttachmentsComponent, @@ -294,6 +297,7 @@ registerLocaleData(localeZhTw, 'zh-TW'); OrgManageComponent, OrgPeopleComponent, OrgReusedPasswordsReportComponent, + OrgRotateApiKeyComponent, OrgSettingComponent, OrgToolsComponent, OrgTwoFactorSetupComponent, @@ -359,12 +363,14 @@ registerLocaleData(localeZhTw, 'zh-TW'); FolderAddEditComponent, ModalComponent, OrgAddEditComponent, + OrgApiKeyComponent, OrgAttachmentsComponent, OrgCollectionAddEditComponent, OrgCollectionsComponent, OrgEntityEventsComponent, OrgEntityUsersComponent, OrgGroupAddEditComponent, + OrgRotateApiKeyComponent, OrgUserAddEditComponent, OrgUserConfirmComponent, OrgUserGroupsComponent, diff --git a/src/app/organizations/settings/account.component.html b/src/app/organizations/settings/account.component.html index 22680d8075..62802e23f0 100644 --- a/src/app/organizations/settings/account.component.html +++ b/src/app/organizations/settings/account.component.html @@ -31,6 +31,19 @@ {{'save' | i18n}} + +
+

{{'apiKey' | i18n}}

+
+

+ {{'apiKeyDesc' | i18n}} + + {{'learnMore' | i18n}} + +

+ + +

{{'taxInformation' | i18n}}

@@ -51,3 +64,5 @@ + + diff --git a/src/app/organizations/settings/account.component.ts b/src/app/organizations/settings/account.component.ts index d7b613d467..87c85625c6 100644 --- a/src/app/organizations/settings/account.component.ts +++ b/src/app/organizations/settings/account.component.ts @@ -18,7 +18,9 @@ import { OrganizationResponse } from 'jslib/models/response/organizationResponse import { ModalComponent } from '../../modal.component'; import { PurgeVaultComponent } from '../../settings/purge-vault.component'; +import { ApiKeyComponent } from './api-key.component'; import { DeleteOrganizationComponent } from './delete-organization.component'; +import { RotateApiKeyComponent } from './rotate-api-key.component'; @Component({ selector: 'app-org-account', @@ -27,8 +29,11 @@ import { DeleteOrganizationComponent } from './delete-organization.component'; export class AccountComponent { @ViewChild('deleteOrganizationTemplate', { read: ViewContainerRef }) deleteModalRef: ViewContainerRef; @ViewChild('purgeOrganizationTemplate', { read: ViewContainerRef }) purgeModalRef: ViewContainerRef; + @ViewChild('apiKeyTemplate', { read: ViewContainerRef }) apiKeyModalRef: ViewContainerRef; + @ViewChild('rotateApiKeyTemplate', { read: ViewContainerRef }) rotateApiKeyModalRef: ViewContainerRef; loading = true; + canUseApi = false; org: OrganizationResponse; formPromise: Promise; @@ -45,6 +50,7 @@ export class AccountComponent { this.organizationId = params.organizationId; try { this.org = await this.apiService.getOrganization(this.organizationId); + this.canUseApi = this.org.useApi; } catch { } }); this.loading = false; @@ -95,4 +101,34 @@ export class AccountComponent { this.modal = null; }); } + + viewApiKey() { + if (this.modal != null) { + this.modal.close(); + } + + const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); + this.modal = this.apiKeyModalRef.createComponent(factory).instance; + const childComponent = this.modal.show(ApiKeyComponent, this.apiKeyModalRef); + childComponent.organizationId = this.organizationId; + + this.modal.onClosed.subscribe(async () => { + this.modal = null; + }); + } + + rotateApiKey() { + if (this.modal != null) { + this.modal.close(); + } + + const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); + this.modal = this.rotateApiKeyModalRef.createComponent(factory).instance; + const childComponent = this.modal.show(RotateApiKeyComponent, this.rotateApiKeyModalRef); + childComponent.organizationId = this.organizationId; + + this.modal.onClosed.subscribe(async () => { + this.modal = null; + }); + } } diff --git a/src/app/organizations/settings/api-key.component.html b/src/app/organizations/settings/api-key.component.html new file mode 100644 index 0000000000..9af80677a2 --- /dev/null +++ b/src/app/organizations/settings/api-key.component.html @@ -0,0 +1,48 @@ + diff --git a/src/app/organizations/settings/api-key.component.ts b/src/app/organizations/settings/api-key.component.ts new file mode 100644 index 0000000000..5e74d0c745 --- /dev/null +++ b/src/app/organizations/settings/api-key.component.ts @@ -0,0 +1,50 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { ApiService } from 'jslib/abstractions/api.service'; +import { CryptoService } from 'jslib/abstractions/crypto.service'; +import { I18nService } from 'jslib/abstractions/i18n.service'; + +import { PasswordVerificationRequest } from 'jslib/models/request/passwordVerificationRequest'; + +import { ApiKeyResponse } from 'jslib/models/response/apiKeyResponse'; + +@Component({ + selector: 'app-api-key', + templateUrl: 'api-key.component.html', +}) +export class ApiKeyComponent { + organizationId: string; + + masterPassword: string; + formPromise: Promise; + clientId: string; + clientSecret: string; + scope: string; + + constructor(private apiService: ApiService, private i18nService: I18nService, + private analytics: Angulartics2, private toasterService: ToasterService, + private cryptoService: CryptoService, private router: Router) { } + + async submit() { + if (this.masterPassword == null || this.masterPassword === '') { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassRequired')); + return; + } + + const request = new PasswordVerificationRequest(); + request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null); + try { + this.formPromise = this.apiService.postOrganizationApiKey(this.organizationId, request); + const response = await this.formPromise; + this.clientSecret = response.apiKey; + this.clientId = 'organization.' + this.organizationId; + this.scope = 'api.organization'; + this.analytics.eventTrack.next({ action: 'Viewed Organization API Key' }); + } catch { } + } +} diff --git a/src/app/organizations/settings/rotate-api-key.component.html b/src/app/organizations/settings/rotate-api-key.component.html new file mode 100644 index 0000000000..d74bb7949f --- /dev/null +++ b/src/app/organizations/settings/rotate-api-key.component.html @@ -0,0 +1,48 @@ + diff --git a/src/app/organizations/settings/rotate-api-key.component.ts b/src/app/organizations/settings/rotate-api-key.component.ts new file mode 100644 index 0000000000..6d1d3878ea --- /dev/null +++ b/src/app/organizations/settings/rotate-api-key.component.ts @@ -0,0 +1,50 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { ApiService } from 'jslib/abstractions/api.service'; +import { CryptoService } from 'jslib/abstractions/crypto.service'; +import { I18nService } from 'jslib/abstractions/i18n.service'; + +import { PasswordVerificationRequest } from 'jslib/models/request/passwordVerificationRequest'; + +import { ApiKeyResponse } from 'jslib/models/response/apiKeyResponse'; + +@Component({ + selector: 'app-rotate-api-key', + templateUrl: 'rotate-api-key.component.html', +}) +export class RotateApiKeyComponent { + organizationId: string; + + masterPassword: string; + formPromise: Promise; + clientId: string; + clientSecret: string; + scope: string; + + constructor(private apiService: ApiService, private i18nService: I18nService, + private analytics: Angulartics2, private toasterService: ToasterService, + private cryptoService: CryptoService, private router: Router) { } + + async submit() { + if (this.masterPassword == null || this.masterPassword === '') { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassRequired')); + return; + } + + const request = new PasswordVerificationRequest(); + request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null); + try { + this.formPromise = this.apiService.postOrganizationRotateApiKey(this.organizationId, request); + const response = await this.formPromise; + this.clientSecret = response.apiKey; + this.clientId = 'organization.' + this.organizationId; + this.scope = 'api.organization'; + this.analytics.eventTrack.next({ action: 'Rotated Organization API Key' }); + } catch { } + } +} diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 02dabd08d0..f8c9d877df 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -2795,5 +2795,27 @@ "free": { "message": "Free", "description": "Free, as in 'Free beer'" + }, + "apiKey": { + "message": "API Key" + }, + "apiKeyDesc": { + "message": "Your API key can be used to authenticate to the Bitwarden public API." + }, + "apiKeyRotateDesc": { + "message": "Rotating the API key will invalidate the previous key. You can rotate your API key if you believe that the current key is no longer safe to use." + }, + "apiKeyWarning": { + "message": "Your API key has full access to the organization. It should be kept secret." + }, + "oauth2ClientCredentials": { + "message": "OAuth 2.0 Client Credentials", + "description": "'OAuth 2.0' is a programming protocol. It should probably not be translated." + }, + "viewApiKey": { + "message": "View API Key" + }, + "rotateApiKey": { + "message": "Rotate API Key" } }