mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 21:33:27 +00:00
EC-265 - SCIM configuration page in org admin (#3065)
* EC-265 - Initial stubs for SCIM config UI * EC-265 - Scim config screen and plumbing * EC-265 - Scim config component works! Needs cleanup * EC-265 - Finalize scim config screen * EC-265 - Remove scim url from storage and env urls * EC-265 - Refactor to use new component library * EC-265 - Angular warnings on disabled attr resolved * EC-265 - Continued transition to new components * EC-265 - Page loading spinner pattern * EC-265 - final SCIM configuration form changes * scim cleanup * use scim urls * suggested changes * feedback fixes * remove return * Move scimUrl logic to EnvironmentService * Refactor scim url handling Co-authored-by: Kyle Spearrin <kyle.spearrin@gmail.com> Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"urls": {
|
"urls": {
|
||||||
"icons": "https://icons.bitwarden.net",
|
"icons": "https://icons.bitwarden.net",
|
||||||
"notifications": "https://notifications.bitwarden.com"
|
"notifications": "https://notifications.bitwarden.com",
|
||||||
|
"scim": "https://scim.bitwarden.com"
|
||||||
},
|
},
|
||||||
"stripeKey": "pk_live_bpN0P37nMxrMQkcaHXtAybJk",
|
"stripeKey": "pk_live_bpN0P37nMxrMQkcaHXtAybJk",
|
||||||
"braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd",
|
"braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"urls": {
|
"urls": {
|
||||||
"notifications": "http://localhost:61840"
|
"notifications": "http://localhost:61840",
|
||||||
|
"scim": "http://localhost:44559"
|
||||||
},
|
},
|
||||||
"dev": {
|
"dev": {
|
||||||
"proxyApi": "http://localhost:4000",
|
"proxyApi": "http://localhost:4000",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"urls": {
|
"urls": {
|
||||||
"icons": "https://icons.qa.bitwarden.pw",
|
"icons": "https://icons.qa.bitwarden.pw",
|
||||||
"notifications": "https://notifications.qa.bitwarden.pw"
|
"notifications": "https://notifications.qa.bitwarden.pw",
|
||||||
|
"scim": "https://scim.qa.bitwarden.pw"
|
||||||
},
|
},
|
||||||
"dev": {
|
"dev": {
|
||||||
"proxyApi": "https://api.qa.bitwarden.pw",
|
"proxyApi": "https://api.qa.bitwarden.pw",
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
|||||||
case this.organization.canManageSso:
|
case this.organization.canManageSso:
|
||||||
route = "manage/sso";
|
route = "manage/sso";
|
||||||
break;
|
break;
|
||||||
|
case this.organization.canManageScim:
|
||||||
|
route = "manage/scim";
|
||||||
|
break;
|
||||||
case this.organization.canAccessEventLogs:
|
case this.organization.canAccessEventLogs:
|
||||||
route = "manage/events";
|
route = "manage/events";
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -44,6 +44,14 @@
|
|||||||
>
|
>
|
||||||
{{ "singleSignOn" | i18n }}
|
{{ "singleSignOn" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
routerLink="scim"
|
||||||
|
class="list-group-item"
|
||||||
|
routerLinkActive="active"
|
||||||
|
*ngIf="organization.canManageScim && accessScim"
|
||||||
|
>
|
||||||
|
{{ "scim" | i18n }}
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
routerLink="events"
|
routerLink="events"
|
||||||
class="list-group-item"
|
class="list-group-item"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export class ManageComponent implements OnInit {
|
|||||||
accessGroups = false;
|
accessGroups = false;
|
||||||
accessEvents = false;
|
accessEvents = false;
|
||||||
accessSso = false;
|
accessSso = false;
|
||||||
|
accessScim = false;
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute, private organizationService: OrganizationService) {}
|
constructor(private route: ActivatedRoute, private organizationService: OrganizationService) {}
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ export class ManageComponent implements OnInit {
|
|||||||
this.accessSso = this.organization.useSso;
|
this.accessSso = this.organization.useSso;
|
||||||
this.accessEvents = this.organization.useEvents;
|
this.accessEvents = this.organization.useEvents;
|
||||||
this.accessGroups = this.organization.useGroups;
|
this.accessGroups = this.organization.useGroups;
|
||||||
|
this.accessScim = this.organization.useScim;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const permissions = {
|
|||||||
Permissions.ManageUsers,
|
Permissions.ManageUsers,
|
||||||
Permissions.ManagePolicies,
|
Permissions.ManagePolicies,
|
||||||
Permissions.ManageSso,
|
Permissions.ManageSso,
|
||||||
|
Permissions.ManageScim,
|
||||||
],
|
],
|
||||||
tools: [Permissions.AccessImportExport, Permissions.AccessReports],
|
tools: [Permissions.AccessImportExport, Permissions.AccessReports],
|
||||||
settings: [Permissions.ManageOrganization],
|
settings: [Permissions.ManageOrganization],
|
||||||
|
|||||||
@@ -5184,6 +5184,60 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"scim": {
|
||||||
|
"message": "SCIM Provisioning",
|
||||||
|
"description": "The text, 'SCIM', is an acronymn and should not be translated."
|
||||||
|
},
|
||||||
|
"scimDescription": {
|
||||||
|
"message": "Automatically provision users and groups with your preferred identity provider via SCIM provisioning",
|
||||||
|
"description": "the text, 'SCIM', is an acronymn and should not be translated."
|
||||||
|
},
|
||||||
|
"scimEnabledCheckboxDesc": {
|
||||||
|
"message": "Enable SCIM",
|
||||||
|
"description": "the text, 'SCIM', is an acronymn and should not be translated."
|
||||||
|
},
|
||||||
|
"scimEnabledCheckboxDescHelpText": {
|
||||||
|
"message": "Set up your preferred identity provider by configuring the URL and SCIM API Key",
|
||||||
|
"description": "the text, 'SCIM', is an acronymn and should not be translated."
|
||||||
|
},
|
||||||
|
"scimApiKeyHelperText": {
|
||||||
|
"message": "This API key has access to manage users within your organization. It should be kept secret."
|
||||||
|
},
|
||||||
|
"copyScimKey": {
|
||||||
|
"message": "Copy the SCIM API Key to your clipboard",
|
||||||
|
"description": "the text, 'SCIM' and 'API', are acronymns and should not be translated."
|
||||||
|
},
|
||||||
|
"rotateScimKey": {
|
||||||
|
"message": "Rotate the SCIM API Key",
|
||||||
|
"description": "the text, 'SCIM' and 'API', are acronymns and should not be translated."
|
||||||
|
},
|
||||||
|
"rotateScimKeyWarning": {
|
||||||
|
"message": "Are you sure you want to rotate the SCIM API Key? The current key will no longer work for any existing integrations.",
|
||||||
|
"description": "the text, 'SCIM' and 'API', are acronymns and should not be translated."
|
||||||
|
},
|
||||||
|
"rotateKey": {
|
||||||
|
"message": "Rotate Key"
|
||||||
|
},
|
||||||
|
"scimApiKey": {
|
||||||
|
"message": "SCIM API Key",
|
||||||
|
"description": "the text, 'SCIM' and 'API', are acronymns and should not be translated."
|
||||||
|
},
|
||||||
|
"copyScimUrl": {
|
||||||
|
"message": "Copy the SCIM endpoint URL to your clipboard",
|
||||||
|
"description": "the text, 'SCIM' and 'URL', are acronymns and should not be translated."
|
||||||
|
},
|
||||||
|
"scimUrl": {
|
||||||
|
"message": "SCIM URL",
|
||||||
|
"description": "the text, 'SCIM' and 'URL', are acronymns and should not be translated."
|
||||||
|
},
|
||||||
|
"scimApiKeyRotated": {
|
||||||
|
"message": "The SCIM API Key has been successfully rotated",
|
||||||
|
"description": "the text, 'SCIM' and 'API', are acronymns and should not be translated."
|
||||||
|
},
|
||||||
|
"scimSettingsSaved": {
|
||||||
|
"message": "SCIM settings have been saved successfully",
|
||||||
|
"description": "the text, 'SCIM', is an acronymn and should not be translated."
|
||||||
|
},
|
||||||
"inputRequired": {
|
"inputRequired": {
|
||||||
"message": "Input is required."
|
"message": "Input is required."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<div class="page-header">
|
||||||
|
<h1>{{ "scim" | i18n }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>{{ "scimDescription" | i18n }}</p>
|
||||||
|
|
||||||
|
<div *ngIf="loading">
|
||||||
|
<i
|
||||||
|
class="bwi bwi-spinner bwi-spin text-muted"
|
||||||
|
title="{{ 'loading' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
#form
|
||||||
|
(ngSubmit)="submit()"
|
||||||
|
[appApiAction]="formPromise"
|
||||||
|
[formGroup]="formData"
|
||||||
|
*ngIf="!loading"
|
||||||
|
>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="enabled"
|
||||||
|
[formControl]="enabled"
|
||||||
|
name="Enabled"
|
||||||
|
aria-describedby="scimEnabledCheckboxDescHelpText"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="enabled">{{ "scimEnabledCheckboxDesc" | i18n }}</label>
|
||||||
|
<div id="scimEnabledCheckboxDescHelpText">
|
||||||
|
<small class="form-text text-muted">{{ "scimEnabledCheckboxDescHelpText" | i18n }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<bit-form-field *ngIf="showScimSettings">
|
||||||
|
<bit-label>{{ "scimUrl" | i18n }}</bit-label>
|
||||||
|
<input bitInput type="text" formControlName="endpointUrl" />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitSuffix
|
||||||
|
bitButton
|
||||||
|
(click)="copyScimUrl()"
|
||||||
|
[appA11yTitle]="'copyScimUrl' | i18n"
|
||||||
|
>
|
||||||
|
<i aria-hidden="true" class="bwi bwi-lg bwi-clone"></i>
|
||||||
|
</button>
|
||||||
|
</bit-form-field>
|
||||||
|
|
||||||
|
<bit-form-field *ngIf="showScimSettings">
|
||||||
|
<bit-label>{{ "scimApiKey" | i18n }}</bit-label>
|
||||||
|
<input bitInput type="text" formControlName="clientSecret" />
|
||||||
|
<ng-container #rotateButton [appApiAction]="rotatePromise">
|
||||||
|
<button
|
||||||
|
[disabled]="rotateButton.loading"
|
||||||
|
type="button"
|
||||||
|
bitSuffix
|
||||||
|
bitButton
|
||||||
|
(click)="rotateScimKey()"
|
||||||
|
[appA11yTitle]="'rotateScimKey' | i18n"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
aria-hidden="true"
|
||||||
|
class="bwi bwi-lg bwi-generate"
|
||||||
|
[ngClass]="{ 'bwi-spin': rotateButton.loading }"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitSuffix
|
||||||
|
bitButton
|
||||||
|
(click)="copyScimKey()"
|
||||||
|
[appA11yTitle]="'copyScimKey' | i18n"
|
||||||
|
>
|
||||||
|
<i aria-hidden="true" class="bwi bwi-lg bwi-clone"></i>
|
||||||
|
</button>
|
||||||
|
<bit-hint>{{ "scimApiKeyHelperText" | i18n }}</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
|
||||||
|
<bit-submit-button buttonType="primary" [loading]="form.loading" [disabled]="form.loading">
|
||||||
|
{{ "save" | i18n }}
|
||||||
|
</bit-submit-button>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { FormBuilder, FormControl } from "@angular/forms";
|
||||||
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
|
import { OrganizationApiKeyType } from "@bitwarden/common/enums/organizationApiKeyType";
|
||||||
|
import { OrganizationConnectionType } from "@bitwarden/common/enums/organizationConnectionType";
|
||||||
|
import { ScimConfigApi } from "@bitwarden/common/models/api/scimConfigApi";
|
||||||
|
import { OrganizationApiKeyRequest } from "@bitwarden/common/models/request/organizationApiKeyRequest";
|
||||||
|
import { OrganizationConnectionRequest } from "@bitwarden/common/models/request/organizationConnectionRequest";
|
||||||
|
import { ScimConfigRequest } from "@bitwarden/common/models/request/scimConfigRequest";
|
||||||
|
import { OrganizationConnectionResponse } from "@bitwarden/common/models/response/organizationConnectionResponse";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-org-manage-scim",
|
||||||
|
templateUrl: "scim.component.html",
|
||||||
|
})
|
||||||
|
export class ScimComponent implements OnInit {
|
||||||
|
loading = true;
|
||||||
|
organizationId: string;
|
||||||
|
existingConnectionId: string;
|
||||||
|
formPromise: Promise<any>;
|
||||||
|
rotatePromise: Promise<any>;
|
||||||
|
enabled = new FormControl(false);
|
||||||
|
showScimSettings = false;
|
||||||
|
|
||||||
|
formData = this.formBuilder.group({
|
||||||
|
endpointUrl: new FormControl({ value: "", disabled: true }),
|
||||||
|
clientSecret: new FormControl({ value: "", disabled: true }),
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private apiService: ApiService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private environmentService: EnvironmentService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.route.parent.parent.params.subscribe(async (params) => {
|
||||||
|
this.organizationId = params.organizationId;
|
||||||
|
await this.load();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
const connection = await this.apiService.getOrganizationConnection(
|
||||||
|
this.organizationId,
|
||||||
|
OrganizationConnectionType.Scim,
|
||||||
|
ScimConfigApi
|
||||||
|
);
|
||||||
|
await this.setConnectionFormValues(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadApiKey() {
|
||||||
|
const apiKeyRequest = new OrganizationApiKeyRequest();
|
||||||
|
apiKeyRequest.type = OrganizationApiKeyType.Scim;
|
||||||
|
apiKeyRequest.masterPasswordHash = "N/A";
|
||||||
|
const apiKeyResponse = await this.apiService.postOrganizationApiKey(
|
||||||
|
this.organizationId,
|
||||||
|
apiKeyRequest
|
||||||
|
);
|
||||||
|
this.formData.setValue({
|
||||||
|
endpointUrl: this.getScimEndpointUrl(),
|
||||||
|
clientSecret: apiKeyResponse.apiKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async copyScimUrl() {
|
||||||
|
this.platformUtilsService.copyToClipboard(this.getScimEndpointUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
async rotateScimKey() {
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("rotateScimKeyWarning"),
|
||||||
|
this.i18nService.t("rotateScimKey"),
|
||||||
|
this.i18nService.t("rotateKey"),
|
||||||
|
this.i18nService.t("cancel"),
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = new OrganizationApiKeyRequest();
|
||||||
|
request.type = OrganizationApiKeyType.Scim;
|
||||||
|
request.masterPasswordHash = "N/A";
|
||||||
|
|
||||||
|
this.rotatePromise = this.apiService.postOrganizationRotateApiKey(this.organizationId, request);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.rotatePromise;
|
||||||
|
this.formData.setValue({
|
||||||
|
endpointUrl: this.getScimEndpointUrl(),
|
||||||
|
clientSecret: response.apiKey,
|
||||||
|
});
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("scimApiKeyRotated"));
|
||||||
|
} catch {
|
||||||
|
// Logged by appApiAction, do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rotatePromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async copyScimKey() {
|
||||||
|
this.platformUtilsService.copyToClipboard(this.formData.get("clientSecret").value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
try {
|
||||||
|
const request = new OrganizationConnectionRequest(
|
||||||
|
this.organizationId,
|
||||||
|
OrganizationConnectionType.Scim,
|
||||||
|
true,
|
||||||
|
new ScimConfigRequest(this.enabled.value)
|
||||||
|
);
|
||||||
|
if (this.existingConnectionId == null) {
|
||||||
|
this.formPromise = this.apiService.createOrganizationConnection(request, ScimConfigApi);
|
||||||
|
} else {
|
||||||
|
this.formPromise = this.apiService.updateOrganizationConnection(
|
||||||
|
request,
|
||||||
|
ScimConfigApi,
|
||||||
|
this.existingConnectionId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const response = (await this.formPromise) as OrganizationConnectionResponse<ScimConfigApi>;
|
||||||
|
await this.setConnectionFormValues(response);
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("scimSettingsSaved"));
|
||||||
|
} catch (e) {
|
||||||
|
// Logged by appApiAction, do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
this.formPromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getScimEndpointUrl() {
|
||||||
|
return this.environmentService.getScimUrl() + "/" + this.organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setConnectionFormValues(connection: OrganizationConnectionResponse<ScimConfigApi>) {
|
||||||
|
this.existingConnectionId = connection?.id;
|
||||||
|
if (connection !== null && connection.config?.enabled) {
|
||||||
|
this.showScimSettings = true;
|
||||||
|
this.enabled.setValue(true);
|
||||||
|
this.formData.setValue({
|
||||||
|
endpointUrl: this.getScimEndpointUrl(),
|
||||||
|
clientSecret: "",
|
||||||
|
});
|
||||||
|
await this.loadApiKey();
|
||||||
|
} else {
|
||||||
|
this.showScimSettings = false;
|
||||||
|
this.enabled.setValue(false);
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import { OrganizationLayoutComponent } from "src/app/organizations/layouts/organ
|
|||||||
import { ManageComponent } from "src/app/organizations/manage/manage.component";
|
import { ManageComponent } from "src/app/organizations/manage/manage.component";
|
||||||
import { NavigationPermissionsService } from "src/app/organizations/services/navigation-permissions.service";
|
import { NavigationPermissionsService } from "src/app/organizations/services/navigation-permissions.service";
|
||||||
|
|
||||||
|
import { ScimComponent } from "./manage/scim.component";
|
||||||
import { SsoComponent } from "./manage/sso.component";
|
import { SsoComponent } from "./manage/sso.component";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
@@ -33,6 +34,14 @@ const routes: Routes = [
|
|||||||
permissions: [Permissions.ManageSso],
|
permissions: [Permissions.ManageSso],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "scim",
|
||||||
|
component: ScimComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
permissions: [Permissions.ManageScim],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import { CommonModule } from "@angular/common";
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { SharedModule } from "src/app/modules/shared.module";
|
||||||
|
|
||||||
import { InputCheckboxComponent } from "./components/input-checkbox.component";
|
import { InputCheckboxComponent } from "./components/input-checkbox.component";
|
||||||
import { InputTextReadOnlyComponent } from "./components/input-text-readonly.component";
|
import { InputTextReadOnlyComponent } from "./components/input-text-readonly.component";
|
||||||
import { InputTextComponent } from "./components/input-text.component";
|
import { InputTextComponent } from "./components/input-text.component";
|
||||||
import { SelectComponent } from "./components/select.component";
|
import { SelectComponent } from "./components/select.component";
|
||||||
|
import { ScimComponent } from "./manage/scim.component";
|
||||||
import { SsoComponent } from "./manage/sso.component";
|
import { SsoComponent } from "./manage/sso.component";
|
||||||
import { OrganizationsRoutingModule } from "./organizations-routing.module";
|
import { OrganizationsRoutingModule } from "./organizations-routing.module";
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ import { OrganizationsRoutingModule } from "./organizations-routing.module";
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
JslibModule,
|
SharedModule,
|
||||||
OrganizationsRoutingModule,
|
OrganizationsRoutingModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -27,6 +28,7 @@ import { OrganizationsRoutingModule } from "./organizations-routing.module";
|
|||||||
InputTextReadOnlyComponent,
|
InputTextReadOnlyComponent,
|
||||||
SelectComponent,
|
SelectComponent,
|
||||||
SsoComponent,
|
SsoComponent,
|
||||||
|
ScimComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class OrganizationsModule {}
|
export class OrganizationsModule {}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { OrganizationApiKeyType } from "../enums/organizationApiKeyType";
|
||||||
import { OrganizationConnectionType } from "../enums/organizationConnectionType";
|
import { OrganizationConnectionType } from "../enums/organizationConnectionType";
|
||||||
import { PolicyType } from "../enums/policyType";
|
import { PolicyType } from "../enums/policyType";
|
||||||
import { SetKeyConnectorKeyRequest } from "../models/request/account/setKeyConnectorKeyRequest";
|
import { SetKeyConnectorKeyRequest } from "../models/request/account/setKeyConnectorKeyRequest";
|
||||||
@@ -573,7 +574,8 @@ export abstract class ApiService {
|
|||||||
request: OrganizationApiKeyRequest
|
request: OrganizationApiKeyRequest
|
||||||
) => Promise<ApiKeyResponse>;
|
) => Promise<ApiKeyResponse>;
|
||||||
getOrganizationApiKeyInformation: (
|
getOrganizationApiKeyInformation: (
|
||||||
id: string
|
id: string,
|
||||||
|
type?: OrganizationApiKeyType
|
||||||
) => Promise<ListResponse<OrganizationApiKeyInformationResponse>>;
|
) => Promise<ListResponse<OrganizationApiKeyInformationResponse>>;
|
||||||
postOrganizationRotateApiKey: (
|
postOrganizationRotateApiKey: (
|
||||||
id: string,
|
id: string,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type Urls = {
|
|||||||
notifications?: string;
|
notifications?: string;
|
||||||
events?: string;
|
events?: string;
|
||||||
keyConnector?: string;
|
keyConnector?: string;
|
||||||
|
scim?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PayPalConfig = {
|
export type PayPalConfig = {
|
||||||
@@ -28,6 +29,7 @@ export abstract class EnvironmentService {
|
|||||||
getIdentityUrl: () => string;
|
getIdentityUrl: () => string;
|
||||||
getEventsUrl: () => string;
|
getEventsUrl: () => string;
|
||||||
getKeyConnectorUrl: () => string;
|
getKeyConnectorUrl: () => string;
|
||||||
|
getScimUrl: () => string;
|
||||||
setUrlsFromStorage: () => Promise<void>;
|
setUrlsFromStorage: () => Promise<void>;
|
||||||
setUrls: (urls: Urls) => Promise<Urls>;
|
setUrls: (urls: Urls) => Promise<Urls>;
|
||||||
getUrls: () => Urls;
|
getUrls: () => Urls;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export enum OrganizationApiKeyType {
|
export enum OrganizationApiKeyType {
|
||||||
Default = 0,
|
Default = 0,
|
||||||
BillingSync = 1,
|
BillingSync = 1,
|
||||||
|
Scim = 2,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export enum OrganizationConnectionType {
|
export enum OrganizationConnectionType {
|
||||||
CloudBillingSync = 1,
|
CloudBillingSync = 1,
|
||||||
|
Scim = 2,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,4 +25,5 @@ export enum Permissions {
|
|||||||
DeleteAssignedCollections,
|
DeleteAssignedCollections,
|
||||||
ManageSso,
|
ManageSso,
|
||||||
ManageBilling,
|
ManageBilling,
|
||||||
|
ManageScim,
|
||||||
}
|
}
|
||||||
|
|||||||
9
libs/common/src/enums/scimProviderType.ts
Normal file
9
libs/common/src/enums/scimProviderType.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export enum ScimProviderType {
|
||||||
|
Default = 0,
|
||||||
|
AzureAd = 1,
|
||||||
|
Okta = 2,
|
||||||
|
OneLogin = 3,
|
||||||
|
JumpCloud = 4,
|
||||||
|
GoogleWorkspace = 5,
|
||||||
|
Rippling = 6,
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ export class PermissionsApi extends BaseResponse {
|
|||||||
managePolicies: boolean;
|
managePolicies: boolean;
|
||||||
manageUsers: boolean;
|
manageUsers: boolean;
|
||||||
manageResetPassword: boolean;
|
manageResetPassword: boolean;
|
||||||
|
manageScim: boolean;
|
||||||
|
|
||||||
constructor(data: any = null) {
|
constructor(data: any = null) {
|
||||||
super(data);
|
super(data);
|
||||||
@@ -51,5 +52,6 @@ export class PermissionsApi extends BaseResponse {
|
|||||||
this.managePolicies = this.getResponseProperty("ManagePolicies");
|
this.managePolicies = this.getResponseProperty("ManagePolicies");
|
||||||
this.manageUsers = this.getResponseProperty("ManageUsers");
|
this.manageUsers = this.getResponseProperty("ManageUsers");
|
||||||
this.manageResetPassword = this.getResponseProperty("ManageResetPassword");
|
this.manageResetPassword = this.getResponseProperty("ManageResetPassword");
|
||||||
|
this.manageScim = this.getResponseProperty("ManageScim");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
libs/common/src/models/api/scimConfigApi.ts
Normal file
17
libs/common/src/models/api/scimConfigApi.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { ScimProviderType } from "@bitwarden/common/enums/scimProviderType";
|
||||||
|
|
||||||
|
import { BaseResponse } from "../response/baseResponse";
|
||||||
|
|
||||||
|
export class ScimConfigApi extends BaseResponse {
|
||||||
|
enabled: boolean;
|
||||||
|
scimProvider: ScimProviderType;
|
||||||
|
|
||||||
|
constructor(data: any) {
|
||||||
|
super(data);
|
||||||
|
if (data == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.enabled = this.getResponseProperty("Enabled");
|
||||||
|
this.scimProvider = this.getResponseProperty("ScimProvider");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ export class OrganizationData {
|
|||||||
useApi: boolean;
|
useApi: boolean;
|
||||||
useSso: boolean;
|
useSso: boolean;
|
||||||
useKeyConnector: boolean;
|
useKeyConnector: boolean;
|
||||||
|
useScim: boolean;
|
||||||
useResetPassword: boolean;
|
useResetPassword: boolean;
|
||||||
selfHost: boolean;
|
selfHost: boolean;
|
||||||
usersGetPremium: boolean;
|
usersGetPremium: boolean;
|
||||||
@@ -58,6 +59,7 @@ export class OrganizationData {
|
|||||||
this.useApi = response.useApi;
|
this.useApi = response.useApi;
|
||||||
this.useSso = response.useSso;
|
this.useSso = response.useSso;
|
||||||
this.useKeyConnector = response.useKeyConnector;
|
this.useKeyConnector = response.useKeyConnector;
|
||||||
|
this.useScim = response.useScim;
|
||||||
this.useResetPassword = response.useResetPassword;
|
this.useResetPassword = response.useResetPassword;
|
||||||
this.selfHost = response.selfHost;
|
this.selfHost = response.selfHost;
|
||||||
this.usersGetPremium = response.usersGetPremium;
|
this.usersGetPremium = response.usersGetPremium;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export class Organization {
|
|||||||
useApi: boolean;
|
useApi: boolean;
|
||||||
useSso: boolean;
|
useSso: boolean;
|
||||||
useKeyConnector: boolean;
|
useKeyConnector: boolean;
|
||||||
|
useScim: boolean;
|
||||||
useResetPassword: boolean;
|
useResetPassword: boolean;
|
||||||
selfHost: boolean;
|
selfHost: boolean;
|
||||||
usersGetPremium: boolean;
|
usersGetPremium: boolean;
|
||||||
@@ -63,6 +64,7 @@ export class Organization {
|
|||||||
this.useApi = obj.useApi;
|
this.useApi = obj.useApi;
|
||||||
this.useSso = obj.useSso;
|
this.useSso = obj.useSso;
|
||||||
this.useKeyConnector = obj.useKeyConnector;
|
this.useKeyConnector = obj.useKeyConnector;
|
||||||
|
this.useScim = obj.useScim;
|
||||||
this.useResetPassword = obj.useResetPassword;
|
this.useResetPassword = obj.useResetPassword;
|
||||||
this.selfHost = obj.selfHost;
|
this.selfHost = obj.selfHost;
|
||||||
this.usersGetPremium = obj.usersGetPremium;
|
this.usersGetPremium = obj.usersGetPremium;
|
||||||
@@ -173,6 +175,10 @@ export class Organization {
|
|||||||
return this.isAdmin || this.permissions.manageSso;
|
return this.isAdmin || this.permissions.manageSso;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canManageScim() {
|
||||||
|
return this.isAdmin || this.permissions.manageScim;
|
||||||
|
}
|
||||||
|
|
||||||
get canManagePolicies() {
|
get canManagePolicies() {
|
||||||
return this.isAdmin || this.permissions.managePolicies;
|
return this.isAdmin || this.permissions.managePolicies;
|
||||||
}
|
}
|
||||||
@@ -207,6 +213,7 @@ export class Organization {
|
|||||||
(permissions.includes(Permissions.ManageUsers) && this.canManageUsers) ||
|
(permissions.includes(Permissions.ManageUsers) && this.canManageUsers) ||
|
||||||
(permissions.includes(Permissions.ManageUsersPassword) && this.canManageUsersPassword) ||
|
(permissions.includes(Permissions.ManageUsersPassword) && this.canManageUsersPassword) ||
|
||||||
(permissions.includes(Permissions.ManageSso) && this.canManageSso) ||
|
(permissions.includes(Permissions.ManageSso) && this.canManageSso) ||
|
||||||
|
(permissions.includes(Permissions.ManageScim) && this.canManageScim) ||
|
||||||
(permissions.includes(Permissions.ManageBilling) && this.canManageBilling);
|
(permissions.includes(Permissions.ManageBilling) && this.canManageBilling);
|
||||||
|
|
||||||
return specifiedPermissions && (this.enabled || this.isOwner);
|
return specifiedPermissions && (this.enabled || this.isOwner);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { OrganizationConnectionType } from "../../enums/organizationConnectionType";
|
import { OrganizationConnectionType } from "../../enums/organizationConnectionType";
|
||||||
|
|
||||||
import { BillingSyncConfigRequest } from "./billingSyncConfigRequest";
|
import { BillingSyncConfigRequest } from "./billingSyncConfigRequest";
|
||||||
|
import { ScimConfigRequest } from "./scimConfigRequest";
|
||||||
|
|
||||||
/**API request config types for OrganizationConnectionRequest */
|
/**API request config types for OrganizationConnectionRequest */
|
||||||
export type OrganizationConnectionRequestConfigs = BillingSyncConfigRequest;
|
export type OrganizationConnectionRequestConfigs = BillingSyncConfigRequest | ScimConfigRequest;
|
||||||
|
|
||||||
export class OrganizationConnectionRequest {
|
export class OrganizationConnectionRequest {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
5
libs/common/src/models/request/scimConfigRequest.ts
Normal file
5
libs/common/src/models/request/scimConfigRequest.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { ScimProviderType } from "@bitwarden/common/enums/scimProviderType";
|
||||||
|
|
||||||
|
export class ScimConfigRequest {
|
||||||
|
constructor(private enabled: boolean, private scimProvider: ScimProviderType = null) {}
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { OrganizationConnectionType } from "../../enums/organizationConnectionType";
|
import { OrganizationConnectionType } from "../../enums/organizationConnectionType";
|
||||||
import { BillingSyncConfigApi } from "../api/billingSyncConfigApi";
|
import { BillingSyncConfigApi } from "../api/billingSyncConfigApi";
|
||||||
|
import { ScimConfigApi } from "../api/scimConfigApi";
|
||||||
|
|
||||||
import { BaseResponse } from "./baseResponse";
|
import { BaseResponse } from "./baseResponse";
|
||||||
|
|
||||||
/**API response config types for OrganizationConnectionResponse */
|
/**API response config types for OrganizationConnectionResponse */
|
||||||
export type OrganizationConnectionConfigApis = BillingSyncConfigApi;
|
export type OrganizationConnectionConfigApis = BillingSyncConfigApi | ScimConfigApi;
|
||||||
|
|
||||||
export class OrganizationConnectionResponse<
|
export class OrganizationConnectionResponse<
|
||||||
TConfig extends OrganizationConnectionConfigApis
|
TConfig extends OrganizationConnectionConfigApis
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
|
|||||||
useApi: boolean;
|
useApi: boolean;
|
||||||
useSso: boolean;
|
useSso: boolean;
|
||||||
useKeyConnector: boolean;
|
useKeyConnector: boolean;
|
||||||
|
useScim: boolean;
|
||||||
useResetPassword: boolean;
|
useResetPassword: boolean;
|
||||||
selfHost: boolean;
|
selfHost: boolean;
|
||||||
usersGetPremium: boolean;
|
usersGetPremium: boolean;
|
||||||
@@ -57,6 +58,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
|
|||||||
this.useApi = this.getResponseProperty("UseApi");
|
this.useApi = this.getResponseProperty("UseApi");
|
||||||
this.useSso = this.getResponseProperty("UseSso");
|
this.useSso = this.getResponseProperty("UseSso");
|
||||||
this.useKeyConnector = this.getResponseProperty("UseKeyConnector") ?? false;
|
this.useKeyConnector = this.getResponseProperty("UseKeyConnector") ?? false;
|
||||||
|
this.useScim = this.getResponseProperty("UseScim") ?? false;
|
||||||
this.useResetPassword = this.getResponseProperty("UseResetPassword");
|
this.useResetPassword = this.getResponseProperty("UseResetPassword");
|
||||||
this.selfHost = this.getResponseProperty("SelfHost");
|
this.selfHost = this.getResponseProperty("SelfHost");
|
||||||
this.usersGetPremium = this.getResponseProperty("UsersGetPremium");
|
this.usersGetPremium = this.getResponseProperty("UsersGetPremium");
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { EnvironmentService } from "../abstractions/environment.service";
|
|||||||
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
|
||||||
import { TokenService } from "../abstractions/token.service";
|
import { TokenService } from "../abstractions/token.service";
|
||||||
import { DeviceType } from "../enums/deviceType";
|
import { DeviceType } from "../enums/deviceType";
|
||||||
|
import { OrganizationApiKeyType } from "../enums/organizationApiKeyType";
|
||||||
import { OrganizationConnectionType } from "../enums/organizationConnectionType";
|
import { OrganizationConnectionType } from "../enums/organizationConnectionType";
|
||||||
import { PolicyType } from "../enums/policyType";
|
import { PolicyType } from "../enums/policyType";
|
||||||
import { Utils } from "../misc/utils";
|
import { Utils } from "../misc/utils";
|
||||||
@@ -1822,15 +1823,14 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getOrganizationApiKeyInformation(
|
async getOrganizationApiKeyInformation(
|
||||||
id: string
|
id: string,
|
||||||
|
type: OrganizationApiKeyType = null
|
||||||
): Promise<ListResponse<OrganizationApiKeyInformationResponse>> {
|
): Promise<ListResponse<OrganizationApiKeyInformationResponse>> {
|
||||||
const r = await this.send(
|
const uri =
|
||||||
"GET",
|
type === null
|
||||||
"/organizations/" + id + "/api-key-information",
|
? "/organizations/" + id + "/api-key-information"
|
||||||
null,
|
: "/organizations/" + id + "/api-key-information/" + type;
|
||||||
true,
|
const r = await this.send("GET", uri, null, true, true);
|
||||||
true
|
|
||||||
);
|
|
||||||
return new ListResponse(r, OrganizationApiKeyInformationResponse);
|
return new ListResponse(r, OrganizationApiKeyInformationResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||||||
private notificationsUrl: string;
|
private notificationsUrl: string;
|
||||||
private eventsUrl: string;
|
private eventsUrl: string;
|
||||||
private keyConnectorUrl: string;
|
private keyConnectorUrl: string;
|
||||||
|
private scimUrl: string = null;
|
||||||
|
|
||||||
constructor(private stateService: StateService) {
|
constructor(private stateService: StateService) {
|
||||||
this.stateService.activeAccount.subscribe(async () => {
|
this.stateService.activeAccount.subscribe(async () => {
|
||||||
@@ -111,6 +112,16 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||||||
return this.keyConnectorUrl;
|
return this.keyConnectorUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getScimUrl() {
|
||||||
|
if (this.scimUrl != null) {
|
||||||
|
return this.scimUrl + "/v2";
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getWebVaultUrl() === "https://vault.bitwarden.com"
|
||||||
|
? "https://scim.bitwarden.com/v2"
|
||||||
|
: this.getWebVaultUrl() + "/scim/v2";
|
||||||
|
}
|
||||||
|
|
||||||
async setUrlsFromStorage(): Promise<void> {
|
async setUrlsFromStorage(): Promise<void> {
|
||||||
const urls: any = await this.stateService.getEnvironmentUrls();
|
const urls: any = await this.stateService.getEnvironmentUrls();
|
||||||
const envUrls = new EnvironmentUrls();
|
const envUrls = new EnvironmentUrls();
|
||||||
@@ -123,6 +134,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||||||
this.notificationsUrl = urls.notifications;
|
this.notificationsUrl = urls.notifications;
|
||||||
this.eventsUrl = envUrls.events = urls.events;
|
this.eventsUrl = envUrls.events = urls.events;
|
||||||
this.keyConnectorUrl = urls.keyConnector;
|
this.keyConnectorUrl = urls.keyConnector;
|
||||||
|
// scimUrl is not saved to storage
|
||||||
}
|
}
|
||||||
|
|
||||||
async setUrls(urls: Urls): Promise<Urls> {
|
async setUrls(urls: Urls): Promise<Urls> {
|
||||||
@@ -135,6 +147,9 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||||||
urls.events = this.formatUrl(urls.events);
|
urls.events = this.formatUrl(urls.events);
|
||||||
urls.keyConnector = this.formatUrl(urls.keyConnector);
|
urls.keyConnector = this.formatUrl(urls.keyConnector);
|
||||||
|
|
||||||
|
// scimUrl cannot be cleared
|
||||||
|
urls.scim = this.formatUrl(urls.scim) ?? this.scimUrl;
|
||||||
|
|
||||||
await this.stateService.setEnvironmentUrls({
|
await this.stateService.setEnvironmentUrls({
|
||||||
base: urls.base,
|
base: urls.base,
|
||||||
api: urls.api,
|
api: urls.api,
|
||||||
@@ -144,6 +159,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||||||
notifications: urls.notifications,
|
notifications: urls.notifications,
|
||||||
events: urls.events,
|
events: urls.events,
|
||||||
keyConnector: urls.keyConnector,
|
keyConnector: urls.keyConnector,
|
||||||
|
// scimUrl is not saved to storage
|
||||||
});
|
});
|
||||||
|
|
||||||
this.baseUrl = urls.base;
|
this.baseUrl = urls.base;
|
||||||
@@ -154,6 +170,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||||||
this.notificationsUrl = urls.notifications;
|
this.notificationsUrl = urls.notifications;
|
||||||
this.eventsUrl = urls.events;
|
this.eventsUrl = urls.events;
|
||||||
this.keyConnectorUrl = urls.keyConnector;
|
this.keyConnectorUrl = urls.keyConnector;
|
||||||
|
this.scimUrl = urls.scim;
|
||||||
|
|
||||||
this.urlsSubject.next(urls);
|
this.urlsSubject.next(urls);
|
||||||
|
|
||||||
@@ -170,6 +187,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||||||
notifications: this.notificationsUrl,
|
notifications: this.notificationsUrl,
|
||||||
events: this.eventsUrl,
|
events: this.eventsUrl,
|
||||||
keyConnector: this.keyConnectorUrl,
|
keyConnector: this.keyConnectorUrl,
|
||||||
|
scim: this.scimUrl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user