1
0
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:
Chad Scharf
2022-07-15 09:35:30 -04:00
committed by GitHub
parent cb3e991b2b
commit e32c4083f3
28 changed files with 417 additions and 16 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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;

View File

@@ -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"

View File

@@ -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;
}); });
} }
} }

View File

@@ -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],

View File

@@ -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."
}, },

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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],
},
},
], ],
}, },
], ],

View File

@@ -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 {}

View File

@@ -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,

View File

@@ -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;

View File

@@ -1,4 +1,5 @@
export enum OrganizationApiKeyType { export enum OrganizationApiKeyType {
Default = 0, Default = 0,
BillingSync = 1, BillingSync = 1,
Scim = 2,
} }

View File

@@ -1,3 +1,4 @@
export enum OrganizationConnectionType { export enum OrganizationConnectionType {
CloudBillingSync = 1, CloudBillingSync = 1,
Scim = 2,
} }

View File

@@ -25,4 +25,5 @@ export enum Permissions {
DeleteAssignedCollections, DeleteAssignedCollections,
ManageSso, ManageSso,
ManageBilling, ManageBilling,
ManageScim,
} }

View File

@@ -0,0 +1,9 @@
export enum ScimProviderType {
Default = 0,
AzureAd = 1,
Okta = 2,
OneLogin = 3,
JumpCloud = 4,
GoogleWorkspace = 5,
Rippling = 6,
}

View File

@@ -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");
} }
} }

View 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");
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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(

View File

@@ -0,0 +1,5 @@
import { ScimProviderType } from "@bitwarden/common/enums/scimProviderType";
export class ScimConfigRequest {
constructor(private enabled: boolean, private scimProvider: ScimProviderType = null) {}
}

View File

@@ -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

View File

@@ -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");

View File

@@ -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);
} }

View File

@@ -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,
}; };
} }