diff --git a/apps/web/config/cloud.json b/apps/web/config/cloud.json index ae1a7c2675..088b52f6dc 100644 --- a/apps/web/config/cloud.json +++ b/apps/web/config/cloud.json @@ -1,7 +1,8 @@ { "urls": { "icons": "https://icons.bitwarden.net", - "notifications": "https://notifications.bitwarden.com" + "notifications": "https://notifications.bitwarden.com", + "scim": "https://scim.bitwarden.com" }, "stripeKey": "pk_live_bpN0P37nMxrMQkcaHXtAybJk", "braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd", diff --git a/apps/web/config/development.json b/apps/web/config/development.json index 7f89752955..e3048db7a2 100644 --- a/apps/web/config/development.json +++ b/apps/web/config/development.json @@ -1,6 +1,7 @@ { "urls": { - "notifications": "http://localhost:61840" + "notifications": "http://localhost:61840", + "scim": "http://localhost:44559" }, "dev": { "proxyApi": "http://localhost:4000", diff --git a/apps/web/config/qa.json b/apps/web/config/qa.json index badd53cbb0..4371ea1ff9 100644 --- a/apps/web/config/qa.json +++ b/apps/web/config/qa.json @@ -1,7 +1,8 @@ { "urls": { "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": { "proxyApi": "https://api.qa.bitwarden.pw", diff --git a/apps/web/src/app/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/organizations/layouts/organization-layout.component.ts index 2657e70f0a..c9dcbd5cf3 100644 --- a/apps/web/src/app/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/organizations/layouts/organization-layout.component.ts @@ -86,6 +86,9 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { case this.organization.canManageSso: route = "manage/sso"; break; + case this.organization.canManageScim: + route = "manage/scim"; + break; case this.organization.canAccessEventLogs: route = "manage/events"; break; diff --git a/apps/web/src/app/organizations/manage/manage.component.html b/apps/web/src/app/organizations/manage/manage.component.html index 63718db432..5d119bd4bb 100644 --- a/apps/web/src/app/organizations/manage/manage.component.html +++ b/apps/web/src/app/organizations/manage/manage.component.html @@ -44,6 +44,14 @@ > {{ "singleSignOn" | i18n }} + + {{ "scim" | i18n }} + +

{{ "scim" | i18n }}

+ + +

{{ "scimDescription" | i18n }}

+ +
+ + {{ "loading" | i18n }} +
+
+
+
+ + +
+ {{ "scimEnabledCheckboxDescHelpText" | i18n }} +
+
+
+ + + {{ "scimUrl" | i18n }} + + + + + + {{ "scimApiKey" | i18n }} + + + + + + {{ "scimApiKeyHelperText" | i18n }} + + + + {{ "save" | i18n }} + +
diff --git a/bitwarden_license/bit-web/src/app/organizations/manage/scim.component.ts b/bitwarden_license/bit-web/src/app/organizations/manage/scim.component.ts new file mode 100644 index 0000000000..26c93af18a --- /dev/null +++ b/bitwarden_license/bit-web/src/app/organizations/manage/scim.component.ts @@ -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; + rotatePromise: Promise; + 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; + 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) { + 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; + } +} diff --git a/bitwarden_license/bit-web/src/app/organizations/organizations-routing.module.ts b/bitwarden_license/bit-web/src/app/organizations/organizations-routing.module.ts index 0052f0d67b..1bfd51b7d3 100644 --- a/bitwarden_license/bit-web/src/app/organizations/organizations-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/organizations/organizations-routing.module.ts @@ -9,6 +9,7 @@ import { OrganizationLayoutComponent } from "src/app/organizations/layouts/organ import { ManageComponent } from "src/app/organizations/manage/manage.component"; import { NavigationPermissionsService } from "src/app/organizations/services/navigation-permissions.service"; +import { ScimComponent } from "./manage/scim.component"; import { SsoComponent } from "./manage/sso.component"; const routes: Routes = [ @@ -33,6 +34,14 @@ const routes: Routes = [ permissions: [Permissions.ManageSso], }, }, + { + path: "scim", + component: ScimComponent, + canActivate: [PermissionsGuard], + data: { + permissions: [Permissions.ManageScim], + }, + }, ], }, ], diff --git a/bitwarden_license/bit-web/src/app/organizations/organizations.module.ts b/bitwarden_license/bit-web/src/app/organizations/organizations.module.ts index 0833e2ef9d..1e9876b287 100644 --- a/bitwarden_license/bit-web/src/app/organizations/organizations.module.ts +++ b/bitwarden_license/bit-web/src/app/organizations/organizations.module.ts @@ -2,12 +2,13 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; 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 { InputTextReadOnlyComponent } from "./components/input-text-readonly.component"; import { InputTextComponent } from "./components/input-text.component"; import { SelectComponent } from "./components/select.component"; +import { ScimComponent } from "./manage/scim.component"; import { SsoComponent } from "./manage/sso.component"; import { OrganizationsRoutingModule } from "./organizations-routing.module"; @@ -18,7 +19,7 @@ import { OrganizationsRoutingModule } from "./organizations-routing.module"; CommonModule, FormsModule, ReactiveFormsModule, - JslibModule, + SharedModule, OrganizationsRoutingModule, ], declarations: [ @@ -27,6 +28,7 @@ import { OrganizationsRoutingModule } from "./organizations-routing.module"; InputTextReadOnlyComponent, SelectComponent, SsoComponent, + ScimComponent, ], }) export class OrganizationsModule {} diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 24517be522..365e3cd43d 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -1,3 +1,4 @@ +import { OrganizationApiKeyType } from "../enums/organizationApiKeyType"; import { OrganizationConnectionType } from "../enums/organizationConnectionType"; import { PolicyType } from "../enums/policyType"; import { SetKeyConnectorKeyRequest } from "../models/request/account/setKeyConnectorKeyRequest"; @@ -573,7 +574,8 @@ export abstract class ApiService { request: OrganizationApiKeyRequest ) => Promise; getOrganizationApiKeyInformation: ( - id: string + id: string, + type?: OrganizationApiKeyType ) => Promise>; postOrganizationRotateApiKey: ( id: string, diff --git a/libs/common/src/abstractions/environment.service.ts b/libs/common/src/abstractions/environment.service.ts index 8398b4c634..84bceaa197 100644 --- a/libs/common/src/abstractions/environment.service.ts +++ b/libs/common/src/abstractions/environment.service.ts @@ -9,6 +9,7 @@ export type Urls = { notifications?: string; events?: string; keyConnector?: string; + scim?: string; }; export type PayPalConfig = { @@ -28,6 +29,7 @@ export abstract class EnvironmentService { getIdentityUrl: () => string; getEventsUrl: () => string; getKeyConnectorUrl: () => string; + getScimUrl: () => string; setUrlsFromStorage: () => Promise; setUrls: (urls: Urls) => Promise; getUrls: () => Urls; diff --git a/libs/common/src/enums/organizationApiKeyType.ts b/libs/common/src/enums/organizationApiKeyType.ts index 27479a8de0..44ba7f8391 100644 --- a/libs/common/src/enums/organizationApiKeyType.ts +++ b/libs/common/src/enums/organizationApiKeyType.ts @@ -1,4 +1,5 @@ export enum OrganizationApiKeyType { Default = 0, BillingSync = 1, + Scim = 2, } diff --git a/libs/common/src/enums/organizationConnectionType.ts b/libs/common/src/enums/organizationConnectionType.ts index 1291874370..d2f9700a6a 100644 --- a/libs/common/src/enums/organizationConnectionType.ts +++ b/libs/common/src/enums/organizationConnectionType.ts @@ -1,3 +1,4 @@ export enum OrganizationConnectionType { CloudBillingSync = 1, + Scim = 2, } diff --git a/libs/common/src/enums/permissions.ts b/libs/common/src/enums/permissions.ts index bb0021506a..46ee906620 100644 --- a/libs/common/src/enums/permissions.ts +++ b/libs/common/src/enums/permissions.ts @@ -25,4 +25,5 @@ export enum Permissions { DeleteAssignedCollections, ManageSso, ManageBilling, + ManageScim, } diff --git a/libs/common/src/enums/scimProviderType.ts b/libs/common/src/enums/scimProviderType.ts new file mode 100644 index 0000000000..43c518fdfb --- /dev/null +++ b/libs/common/src/enums/scimProviderType.ts @@ -0,0 +1,9 @@ +export enum ScimProviderType { + Default = 0, + AzureAd = 1, + Okta = 2, + OneLogin = 3, + JumpCloud = 4, + GoogleWorkspace = 5, + Rippling = 6, +} diff --git a/libs/common/src/models/api/permissionsApi.ts b/libs/common/src/models/api/permissionsApi.ts index bac79bd3cf..c35cb69cb5 100644 --- a/libs/common/src/models/api/permissionsApi.ts +++ b/libs/common/src/models/api/permissionsApi.ts @@ -25,6 +25,7 @@ export class PermissionsApi extends BaseResponse { managePolicies: boolean; manageUsers: boolean; manageResetPassword: boolean; + manageScim: boolean; constructor(data: any = null) { super(data); @@ -51,5 +52,6 @@ export class PermissionsApi extends BaseResponse { this.managePolicies = this.getResponseProperty("ManagePolicies"); this.manageUsers = this.getResponseProperty("ManageUsers"); this.manageResetPassword = this.getResponseProperty("ManageResetPassword"); + this.manageScim = this.getResponseProperty("ManageScim"); } } diff --git a/libs/common/src/models/api/scimConfigApi.ts b/libs/common/src/models/api/scimConfigApi.ts new file mode 100644 index 0000000000..2269ac5969 --- /dev/null +++ b/libs/common/src/models/api/scimConfigApi.ts @@ -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"); + } +} diff --git a/libs/common/src/models/data/organizationData.ts b/libs/common/src/models/data/organizationData.ts index d093821fc3..5d5e522959 100644 --- a/libs/common/src/models/data/organizationData.ts +++ b/libs/common/src/models/data/organizationData.ts @@ -19,6 +19,7 @@ export class OrganizationData { useApi: boolean; useSso: boolean; useKeyConnector: boolean; + useScim: boolean; useResetPassword: boolean; selfHost: boolean; usersGetPremium: boolean; @@ -58,6 +59,7 @@ export class OrganizationData { this.useApi = response.useApi; this.useSso = response.useSso; this.useKeyConnector = response.useKeyConnector; + this.useScim = response.useScim; this.useResetPassword = response.useResetPassword; this.selfHost = response.selfHost; this.usersGetPremium = response.usersGetPremium; diff --git a/libs/common/src/models/domain/organization.ts b/libs/common/src/models/domain/organization.ts index ba1c8e1a17..c15d936234 100644 --- a/libs/common/src/models/domain/organization.ts +++ b/libs/common/src/models/domain/organization.ts @@ -20,6 +20,7 @@ export class Organization { useApi: boolean; useSso: boolean; useKeyConnector: boolean; + useScim: boolean; useResetPassword: boolean; selfHost: boolean; usersGetPremium: boolean; @@ -63,6 +64,7 @@ export class Organization { this.useApi = obj.useApi; this.useSso = obj.useSso; this.useKeyConnector = obj.useKeyConnector; + this.useScim = obj.useScim; this.useResetPassword = obj.useResetPassword; this.selfHost = obj.selfHost; this.usersGetPremium = obj.usersGetPremium; @@ -173,6 +175,10 @@ export class Organization { return this.isAdmin || this.permissions.manageSso; } + get canManageScim() { + return this.isAdmin || this.permissions.manageScim; + } + get canManagePolicies() { return this.isAdmin || this.permissions.managePolicies; } @@ -207,6 +213,7 @@ export class Organization { (permissions.includes(Permissions.ManageUsers) && this.canManageUsers) || (permissions.includes(Permissions.ManageUsersPassword) && this.canManageUsersPassword) || (permissions.includes(Permissions.ManageSso) && this.canManageSso) || + (permissions.includes(Permissions.ManageScim) && this.canManageScim) || (permissions.includes(Permissions.ManageBilling) && this.canManageBilling); return specifiedPermissions && (this.enabled || this.isOwner); diff --git a/libs/common/src/models/request/organizationConnectionRequest.ts b/libs/common/src/models/request/organizationConnectionRequest.ts index 69964cb09e..2506ac1d08 100644 --- a/libs/common/src/models/request/organizationConnectionRequest.ts +++ b/libs/common/src/models/request/organizationConnectionRequest.ts @@ -1,9 +1,10 @@ import { OrganizationConnectionType } from "../../enums/organizationConnectionType"; import { BillingSyncConfigRequest } from "./billingSyncConfigRequest"; +import { ScimConfigRequest } from "./scimConfigRequest"; /**API request config types for OrganizationConnectionRequest */ -export type OrganizationConnectionRequestConfigs = BillingSyncConfigRequest; +export type OrganizationConnectionRequestConfigs = BillingSyncConfigRequest | ScimConfigRequest; export class OrganizationConnectionRequest { constructor( diff --git a/libs/common/src/models/request/scimConfigRequest.ts b/libs/common/src/models/request/scimConfigRequest.ts new file mode 100644 index 0000000000..7549661cdd --- /dev/null +++ b/libs/common/src/models/request/scimConfigRequest.ts @@ -0,0 +1,5 @@ +import { ScimProviderType } from "@bitwarden/common/enums/scimProviderType"; + +export class ScimConfigRequest { + constructor(private enabled: boolean, private scimProvider: ScimProviderType = null) {} +} diff --git a/libs/common/src/models/response/organizationConnectionResponse.ts b/libs/common/src/models/response/organizationConnectionResponse.ts index a1e9a3de67..7f39d250f3 100644 --- a/libs/common/src/models/response/organizationConnectionResponse.ts +++ b/libs/common/src/models/response/organizationConnectionResponse.ts @@ -1,10 +1,11 @@ import { OrganizationConnectionType } from "../../enums/organizationConnectionType"; import { BillingSyncConfigApi } from "../api/billingSyncConfigApi"; +import { ScimConfigApi } from "../api/scimConfigApi"; import { BaseResponse } from "./baseResponse"; /**API response config types for OrganizationConnectionResponse */ -export type OrganizationConnectionConfigApis = BillingSyncConfigApi; +export type OrganizationConnectionConfigApis = BillingSyncConfigApi | ScimConfigApi; export class OrganizationConnectionResponse< TConfig extends OrganizationConnectionConfigApis diff --git a/libs/common/src/models/response/profileOrganizationResponse.ts b/libs/common/src/models/response/profileOrganizationResponse.ts index 39b7644caa..0287ae96c9 100644 --- a/libs/common/src/models/response/profileOrganizationResponse.ts +++ b/libs/common/src/models/response/profileOrganizationResponse.ts @@ -17,6 +17,7 @@ export class ProfileOrganizationResponse extends BaseResponse { useApi: boolean; useSso: boolean; useKeyConnector: boolean; + useScim: boolean; useResetPassword: boolean; selfHost: boolean; usersGetPremium: boolean; @@ -57,6 +58,7 @@ export class ProfileOrganizationResponse extends BaseResponse { this.useApi = this.getResponseProperty("UseApi"); this.useSso = this.getResponseProperty("UseSso"); this.useKeyConnector = this.getResponseProperty("UseKeyConnector") ?? false; + this.useScim = this.getResponseProperty("UseScim") ?? false; this.useResetPassword = this.getResponseProperty("UseResetPassword"); this.selfHost = this.getResponseProperty("SelfHost"); this.usersGetPremium = this.getResponseProperty("UsersGetPremium"); diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index ab41555d9c..35b7d336d9 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -4,6 +4,7 @@ import { EnvironmentService } from "../abstractions/environment.service"; import { PlatformUtilsService } from "../abstractions/platformUtils.service"; import { TokenService } from "../abstractions/token.service"; import { DeviceType } from "../enums/deviceType"; +import { OrganizationApiKeyType } from "../enums/organizationApiKeyType"; import { OrganizationConnectionType } from "../enums/organizationConnectionType"; import { PolicyType } from "../enums/policyType"; import { Utils } from "../misc/utils"; @@ -1822,15 +1823,14 @@ export class ApiService implements ApiServiceAbstraction { } async getOrganizationApiKeyInformation( - id: string + id: string, + type: OrganizationApiKeyType = null ): Promise> { - const r = await this.send( - "GET", - "/organizations/" + id + "/api-key-information", - null, - true, - true - ); + const uri = + type === null + ? "/organizations/" + id + "/api-key-information" + : "/organizations/" + id + "/api-key-information/" + type; + const r = await this.send("GET", uri, null, true, true); return new ListResponse(r, OrganizationApiKeyInformationResponse); } diff --git a/libs/common/src/services/environment.service.ts b/libs/common/src/services/environment.service.ts index b13afd59ac..c22d7d85e6 100644 --- a/libs/common/src/services/environment.service.ts +++ b/libs/common/src/services/environment.service.ts @@ -19,6 +19,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { private notificationsUrl: string; private eventsUrl: string; private keyConnectorUrl: string; + private scimUrl: string = null; constructor(private stateService: StateService) { this.stateService.activeAccount.subscribe(async () => { @@ -111,6 +112,16 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { 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 { const urls: any = await this.stateService.getEnvironmentUrls(); const envUrls = new EnvironmentUrls(); @@ -123,6 +134,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { this.notificationsUrl = urls.notifications; this.eventsUrl = envUrls.events = urls.events; this.keyConnectorUrl = urls.keyConnector; + // scimUrl is not saved to storage } async setUrls(urls: Urls): Promise { @@ -135,6 +147,9 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { urls.events = this.formatUrl(urls.events); urls.keyConnector = this.formatUrl(urls.keyConnector); + // scimUrl cannot be cleared + urls.scim = this.formatUrl(urls.scim) ?? this.scimUrl; + await this.stateService.setEnvironmentUrls({ base: urls.base, api: urls.api, @@ -144,6 +159,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { notifications: urls.notifications, events: urls.events, keyConnector: urls.keyConnector, + // scimUrl is not saved to storage }); this.baseUrl = urls.base; @@ -154,6 +170,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { this.notificationsUrl = urls.notifications; this.eventsUrl = urls.events; this.keyConnectorUrl = urls.keyConnector; + this.scimUrl = urls.scim; this.urlsSubject.next(urls); @@ -170,6 +187,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { notifications: this.notificationsUrl, events: this.eventsUrl, keyConnector: this.keyConnectorUrl, + scim: this.scimUrl, }; }