mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 16:53:34 +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,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<ApiKeyResponse>;
|
||||
getOrganizationApiKeyInformation: (
|
||||
id: string
|
||||
id: string,
|
||||
type?: OrganizationApiKeyType
|
||||
) => Promise<ListResponse<OrganizationApiKeyInformationResponse>>;
|
||||
postOrganizationRotateApiKey: (
|
||||
id: string,
|
||||
|
||||
@@ -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<void>;
|
||||
setUrls: (urls: Urls) => Promise<Urls>;
|
||||
getUrls: () => Urls;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum OrganizationApiKeyType {
|
||||
Default = 0,
|
||||
BillingSync = 1,
|
||||
Scim = 2,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export enum OrganizationConnectionType {
|
||||
CloudBillingSync = 1,
|
||||
Scim = 2,
|
||||
}
|
||||
|
||||
@@ -25,4 +25,5 @@ export enum Permissions {
|
||||
DeleteAssignedCollections,
|
||||
ManageSso,
|
||||
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;
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
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 { 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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<ListResponse<OrganizationApiKeyInformationResponse>> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
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<Urls> {
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user