diff --git a/jslib b/jslib index 9aa3cbf73d..79b856cb6e 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit 9aa3cbf73d9df9a2641654270911359593bcb5c5 +Subproject commit 79b856cb6e73f126a263a0e4a61d0161828a40dd diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 5d9ffe2cc8..01481f05d4 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -60,13 +60,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 { ChangePlanComponent } from './organizations/settings/change-plan.component'; import { DeleteOrganizationComponent } from './organizations/settings/delete-organization.component'; import { DownloadLicenseComponent } from './organizations/settings/download-license.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, @@ -106,6 +104,7 @@ import { AccountComponent } from './settings/account.component'; import { AddCreditComponent } from './settings/add-credit.component'; import { AdjustPaymentComponent } from './settings/adjust-payment.component'; import { AdjustStorageComponent } from './settings/adjust-storage.component'; +import { ApiKeyComponent } from './settings/api-key.component'; import { ChangeEmailComponent } from './settings/change-email.component'; import { ChangeKdfComponent } from './settings/change-kdf.component'; import { ChangePasswordComponent } from './settings/change-password.component'; @@ -269,6 +268,7 @@ registerLocaleData(localeZhTw, 'zh-TW'); AdjustSeatsComponent, AdjustStorageComponent, ApiActionDirective, + ApiKeyComponent, AppComponent, AttachmentsComponent, AutofocusDirective, @@ -316,7 +316,6 @@ registerLocaleData(localeZhTw, 'zh-TW'); OptionsComponent, OrgAccountComponent, OrgAddEditComponent, - OrgApiKeyComponent, OrganizationBillingComponent, OrganizationPlansComponent, OrganizationSubscriptionComponent, @@ -340,7 +339,6 @@ registerLocaleData(localeZhTw, 'zh-TW'); OrgPolicyEditComponent, OrgPoliciesComponent, OrgReusedPasswordsReportComponent, - OrgRotateApiKeyComponent, OrgSettingComponent, OrgToolsComponent, OrgTwoFactorSetupComponent, @@ -400,6 +398,7 @@ registerLocaleData(localeZhTw, 'zh-TW'); ], entryComponents: [ AddEditComponent, + ApiKeyComponent, AttachmentsComponent, BulkActionsComponent, BulkDeleteComponent, @@ -413,7 +412,6 @@ registerLocaleData(localeZhTw, 'zh-TW'); FolderAddEditComponent, ModalComponent, OrgAddEditComponent, - OrgApiKeyComponent, OrgAttachmentsComponent, OrgCollectionAddEditComponent, OrgCollectionsComponent, @@ -421,7 +419,6 @@ registerLocaleData(localeZhTw, 'zh-TW'); OrgEntityUsersComponent, OrgGroupAddEditComponent, OrgPolicyEditComponent, - OrgRotateApiKeyComponent, OrgUserAddEditComponent, OrgUserConfirmComponent, OrgUserGroupsComponent, diff --git a/src/app/organizations/settings/account.component.ts b/src/app/organizations/settings/account.component.ts index e6285707a8..914c54f619 100644 --- a/src/app/organizations/settings/account.component.ts +++ b/src/app/organizations/settings/account.component.ts @@ -18,11 +18,10 @@ import { OrganizationUpdateRequest } from 'jslib/models/request/organizationUpda import { OrganizationResponse } from 'jslib/models/response/organizationResponse'; import { ModalComponent } from '../../modal.component'; +import { ApiKeyComponent } from '../../settings/api-key.component'; import { PurgeVaultComponent } from '../../settings/purge-vault.component'; import { TaxInfoComponent } from '../../settings/tax-info.component'; -import { ApiKeyComponent } from './api-key.component'; import { DeleteOrganizationComponent } from './delete-organization.component'; -import { RotateApiKeyComponent } from './rotate-api-key.component'; @Component({ selector: 'app-org-account', @@ -125,7 +124,14 @@ export class AccountComponent { 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; + childComponent.keyType = 'organization'; + childComponent.entityId = this.organizationId; + childComponent.postKey = this.apiService.postOrganizationApiKey.bind(this.apiService); + childComponent.scope = 'api.organization'; + childComponent.grantType = 'client_credentials'; + childComponent.apiKeyTitle = 'apiKey'; + childComponent.apiKeyWarning = 'apiKeyWarning'; + childComponent.apiKeyDescription = 'apiKeyDesc'; this.modal.onClosed.subscribe(async () => { this.modal = null; @@ -139,8 +145,16 @@ export class AccountComponent { 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; + const childComponent = this.modal.show(ApiKeyComponent, this.rotateApiKeyModalRef); + childComponent.keyType = 'organization'; + childComponent.isRotation = true; + childComponent.entityId = this.organizationId; + childComponent.postKey = this.apiService.postOrganizationRotateApiKey.bind(this.apiService); + childComponent.scope = 'api.organization'; + childComponent.grantType = 'client_credentials'; + childComponent.apiKeyTitle = 'apiKey'; + childComponent.apiKeyWarning = 'apiKeyWarning'; + childComponent.apiKeyDescription = 'apiKeyRotateDesc'; this.modal.onClosed.subscribe(async () => { this.modal = null; diff --git a/src/app/organizations/settings/rotate-api-key.component.html b/src/app/organizations/settings/rotate-api-key.component.html deleted file mode 100644 index a27c2a2f63..0000000000 --- a/src/app/organizations/settings/rotate-api-key.component.html +++ /dev/null @@ -1,48 +0,0 @@ - diff --git a/src/app/organizations/settings/rotate-api-key.component.ts b/src/app/organizations/settings/rotate-api-key.component.ts deleted file mode 100644 index 6d1d3878ea..0000000000 --- a/src/app/organizations/settings/rotate-api-key.component.ts +++ /dev/null @@ -1,50 +0,0 @@ -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/app/settings/account.component.html b/src/app/settings/account.component.html index 38348e09c7..f4e13587eb 100644 --- a/src/app/settings/account.component.html +++ b/src/app/settings/account.component.html @@ -14,6 +14,14 @@

{{'encKeySettings' | i18n}}

+
+

{{'apiKey' | i18n}}

+
+

+ {{'userApiKeyDesc' | i18n}} +

+ +

{{'dangerZone' | i18n}}

@@ -30,3 +38,5 @@ + + diff --git a/src/app/settings/account.component.ts b/src/app/settings/account.component.ts index 16d872644e..b38d3d0198 100644 --- a/src/app/settings/account.component.ts +++ b/src/app/settings/account.component.ts @@ -6,10 +6,14 @@ import { } from '@angular/core'; import { ModalComponent } from '../modal.component'; +import { ApiKeyComponent } from './api-key.component'; import { DeauthorizeSessionsComponent } from './deauthorize-sessions.component'; import { DeleteAccountComponent } from './delete-account.component'; import { PurgeVaultComponent } from './purge-vault.component'; +import { ApiService } from 'jslib/abstractions/api.service'; +import { UserService } from 'jslib/abstractions/user.service'; + @Component({ selector: 'app-account', templateUrl: 'account.component.html', @@ -18,10 +22,13 @@ export class AccountComponent { @ViewChild('deauthorizeSessionsTemplate', { read: ViewContainerRef, static: true }) deauthModalRef: ViewContainerRef; @ViewChild('purgeVaultTemplate', { read: ViewContainerRef, static: true }) purgeModalRef: ViewContainerRef; @ViewChild('deleteAccountTemplate', { read: ViewContainerRef, static: true }) deleteModalRef: ViewContainerRef; + @ViewChild('viewUserApiKeyTemplate', { read: ViewContainerRef, static: true }) viewUserApiKeyModalRef: ViewContainerRef; + @ViewChild('rotateUserApiKeyTemplate', { read: ViewContainerRef, static: true }) rotateUserApiKeyModalRef: ViewContainerRef; private modal: ModalComponent = null; - constructor(private componentFactoryResolver: ComponentFactoryResolver) { } + constructor(private componentFactoryResolver: ComponentFactoryResolver, private apiService: ApiService, + private userService: UserService) { } deauthorizeSessions() { if (this.modal != null) { @@ -64,4 +71,49 @@ export class AccountComponent { this.modal = null; }); } + + async viewUserApiKey() { + if (this.modal != null) { + this.modal.close(); + } + + const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); + this.modal = this.viewUserApiKeyModalRef.createComponent(factory).instance; + const childComponent = this.modal.show(ApiKeyComponent, this.viewUserApiKeyModalRef); + childComponent.keyType = 'user'; + childComponent.entityId = await this.userService.getUserId(); + childComponent.postKey = this.apiService.postUserApiKey.bind(this.apiService); + childComponent.scope = 'api'; + childComponent.grantType = 'client_credentials'; + childComponent.apiKeyTitle = 'apiKey'; + childComponent.apiKeyWarning = 'userApiKeyWarning'; + childComponent.apiKeyDescription = 'userApiKeyDesc'; + + this.modal.onClosed.subscribe(async () => { + this.modal = null; + }); + } + + async rotateUserApiKey() { + if (this.modal != null) { + this.modal.close(); + } + + const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); + this.modal = this.rotateUserApiKeyModalRef.createComponent(factory).instance; + const childComponent = this.modal.show(ApiKeyComponent, this.rotateUserApiKeyModalRef); + childComponent.keyType = 'user'; + childComponent.isRotation = true; + childComponent.entityId = await this.userService.getUserId(); + childComponent.postKey = this.apiService.postUserRotateApiKey.bind(this.apiService); + childComponent.scope = 'api'; + childComponent.grantType = 'client_credentials'; + childComponent.apiKeyTitle = 'apiKey'; + childComponent.apiKeyWarning = 'userApiKeyWarning'; + childComponent.apiKeyDescription = 'apiKeyRotateDesc'; + + this.modal.onClosed.subscribe(async () => { + this.modal = null; + }); + } } diff --git a/src/app/organizations/settings/api-key.component.html b/src/app/settings/api-key.component.html similarity index 87% rename from src/app/organizations/settings/api-key.component.html rename to src/app/settings/api-key.component.html index 917acd4631..9e5a9bc9ab 100644 --- a/src/app/organizations/settings/api-key.component.html +++ b/src/app/settings/api-key.component.html @@ -2,19 +2,19 @@ diff --git a/src/app/organizations/settings/api-key.component.ts b/src/app/settings/api-key.component.ts similarity index 65% rename from src/app/organizations/settings/api-key.component.ts rename to src/app/settings/api-key.component.ts index 5e74d0c745..fa1be6afa8 100644 --- a/src/app/organizations/settings/api-key.component.ts +++ b/src/app/settings/api-key.component.ts @@ -1,10 +1,8 @@ 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'; @@ -17,17 +15,23 @@ import { ApiKeyResponse } from 'jslib/models/response/apiKeyResponse'; templateUrl: 'api-key.component.html', }) export class ApiKeyComponent { - organizationId: string; + keyType: string; + isRotation: boolean; + postKey: (entityId: string, request: PasswordVerificationRequest) => Promise; + entityId: string; + scope: string; + grantType: string; + apiKeyTitle: string; + apiKeyWarning: string; + apiKeyDescription: 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) { } + constructor(private i18nService: I18nService, private analytics: Angulartics2, + private toasterService: ToasterService, private cryptoService: CryptoService) { } async submit() { if (this.masterPassword == null || this.masterPassword === '') { @@ -39,12 +43,11 @@ export class ApiKeyComponent { const request = new PasswordVerificationRequest(); request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null); try { - this.formPromise = this.apiService.postOrganizationApiKey(this.organizationId, request); + this.formPromise = this.postKey(this.entityId, 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' }); + this.clientId = `${this.keyType}.${this.entityId}`; + this.analytics.eventTrack.next({ action: `Viewed ${this.keyType} API Key` }); } catch { } } } diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 6027a877f8..7a84c56097 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -2953,6 +2953,12 @@ "apiKeyWarning": { "message": "Your API key has full access to the organization. It should be kept secret." }, + "userApiKeyDesc": { + "message": "Your API key can be used to authenticate in the Bitwarden CLI." + }, + "userApiKeyWarning": { + "message": "Your API key is an alternative authentication mechanism. 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."