1
0
mirror of https://github.com/bitwarden/directory-connector synced 2026-01-09 20:13:20 +00:00
Files
directory-connector/src/services/state.service.ts
Jared McCannon 284206b735 [PM-32177] - Fixing Backward Compatibility with Azure AD (#813)
* Updating the fetching of the config and key to check entra and check azure afterwards.

* Making this camelCase to match other values.
2025-07-02 08:20:56 -05:00

614 lines
21 KiB
TypeScript

import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { StateMigrationService } from "@/jslib/common/src/abstractions/stateMigration.service";
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls";
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
import { StorageOptions } from "@/jslib/common/src/models/domain/storageOptions";
import { StateService as BaseStateService } from "@/jslib/common/src/services/state.service";
import { StateService as StateServiceAbstraction } from "@/src/abstractions/state.service";
import { DirectoryType } from "@/src/enums/directoryType";
import { IConfiguration } from "@/src/models/IConfiguration";
import { Account } from "@/src/models/account";
import { EntraIdConfiguration } from "@/src/models/entraIdConfiguration";
import { GSuiteConfiguration } from "@/src/models/gsuiteConfiguration";
import { LdapConfiguration } from "@/src/models/ldapConfiguration";
import { OktaConfiguration } from "@/src/models/oktaConfiguration";
import { OneLoginConfiguration } from "@/src/models/oneLoginConfiguration";
import { SyncConfiguration } from "@/src/models/syncConfiguration";
const SecureStorageKeys = {
ldap: "ldapPassword",
gsuite: "gsuitePrivateKey",
// Azure Active Directory was renamed to Entra ID, but we've kept the old property name
// to be backwards compatible with existing configurations.
azure: "azureKey",
entra: "entraKey",
okta: "oktaToken",
oneLogin: "oneLoginClientSecret",
userDelta: "userDeltaToken",
groupDelta: "groupDeltaToken",
lastUserSync: "lastUserSync",
lastGroupSync: "lastGroupSync",
lastSyncHash: "lastSyncHash",
};
const keys = {
tempAccountSettings: "tempAccountSettings",
tempDirectoryConfigs: "tempDirectoryConfigs",
tempDirectorySettings: "tempDirectorySettings",
};
const StoredSecurely = "[STORED SECURELY]";
export class StateService
extends BaseStateService<GlobalState, Account>
implements StateServiceAbstraction
{
constructor(
protected storageService: StorageService,
protected secureStorageService: StorageService,
protected logService: LogService,
protected stateMigrationService: StateMigrationService,
private useSecureStorageForSecrets = true,
protected stateFactory: StateFactory<GlobalState, Account>,
) {
super(storageService, secureStorageService, logService, stateMigrationService, stateFactory);
}
async getDirectory<T extends IConfiguration>(type: DirectoryType): Promise<T> {
const config = await this.getConfiguration(type);
if (config == null) {
return config as T;
}
if (this.useSecureStorageForSecrets) {
// Do not introduce secrets into the in-memory account object
const configWithSecrets = Object.assign({}, config);
switch (type) {
case DirectoryType.Ldap:
(configWithSecrets as any).password = await this.getLdapKey();
break;
case DirectoryType.EntraID:
(configWithSecrets as any).key = await this.getEntraKey();
break;
case DirectoryType.Okta:
(configWithSecrets as any).token = await this.getOktaKey();
break;
case DirectoryType.GSuite:
(configWithSecrets as any).privateKey = await this.getGsuiteKey();
break;
case DirectoryType.OneLogin:
(configWithSecrets as any).clientSecret = await this.getOneLoginKey();
break;
}
return configWithSecrets as T;
}
return config as T;
}
async setDirectory(
type: DirectoryType,
config:
| LdapConfiguration
| GSuiteConfiguration
| EntraIdConfiguration
| OktaConfiguration
| OneLoginConfiguration,
): Promise<any> {
if (this.useSecureStorageForSecrets) {
switch (type) {
case DirectoryType.Ldap: {
const ldapConfig = config as LdapConfiguration;
await this.setLdapKey(ldapConfig.password);
ldapConfig.password = StoredSecurely;
await this.setLdapConfiguration(ldapConfig);
break;
}
case DirectoryType.EntraID: {
const entraConfig = config as EntraIdConfiguration;
await this.setEntraKey(entraConfig.key);
entraConfig.key = StoredSecurely;
await this.setEntraConfiguration(entraConfig);
break;
}
case DirectoryType.Okta: {
const oktaConfig = config as OktaConfiguration;
await this.setOktaKey(oktaConfig.token);
oktaConfig.token = StoredSecurely;
await this.setOktaConfiguration(oktaConfig);
break;
}
case DirectoryType.GSuite: {
const gsuiteConfig = config as GSuiteConfiguration;
if (gsuiteConfig.privateKey == null) {
await this.setGsuiteKey(null);
} else {
const normalizedPrivateKey = gsuiteConfig.privateKey.replace(/\\n/g, "\n");
await this.setGsuiteKey(normalizedPrivateKey);
gsuiteConfig.privateKey = StoredSecurely;
}
await this.setGsuiteConfiguration(gsuiteConfig);
break;
}
case DirectoryType.OneLogin: {
const oneLoginConfig = config as OneLoginConfiguration;
await this.setOneLoginKey(oneLoginConfig.clientSecret);
oneLoginConfig.clientSecret = StoredSecurely;
await this.setOneLoginConfiguration(oneLoginConfig);
break;
}
}
}
}
private async getLdapKey(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return null;
}
return await this.secureStorageService.get<string>(
`${options.userId}_${SecureStorageKeys.ldap}`,
);
}
private async setLdapKey(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return;
}
await this.secureStorageService.save(
`${options.userId}_${SecureStorageKeys.ldap}`,
value,
options,
);
}
private async getGsuiteKey(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return null;
}
return await this.secureStorageService.get<string>(
`${options.userId}_${SecureStorageKeys.gsuite}`,
);
}
private async setGsuiteKey(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return;
}
await this.secureStorageService.save(
`${options.userId}_${SecureStorageKeys.gsuite}`,
value,
options,
);
}
private async getEntraKey(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return null;
}
const entraKey = await this.secureStorageService.get<string>(
`${options.userId}_${SecureStorageKeys.entra}`,
);
if (entraKey != null) {
return entraKey;
}
return await this.secureStorageService.get<string>(
`${options.userId}_${SecureStorageKeys.azure}`,
);
}
private async setEntraKey(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return;
}
await this.secureStorageService.save(
`${options.userId}_${SecureStorageKeys.entra}`,
value,
options,
);
}
private async getOktaKey(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return null;
}
return await this.secureStorageService.get<string>(
`${options.userId}_${SecureStorageKeys.okta}`,
);
}
private async setOktaKey(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return;
}
await this.secureStorageService.save(
`${options.userId}_${SecureStorageKeys.okta}`,
value,
options,
);
}
private async getOneLoginKey(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return null;
}
return await this.secureStorageService.get<string>(
`${options.userId}_${SecureStorageKeys.oneLogin}`,
);
}
private async setOneLoginKey(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return;
}
await this.secureStorageService.save(
`${options.userId}_${SecureStorageKeys.oneLogin}`,
value,
options,
);
}
async getConfiguration(type: DirectoryType): Promise<IConfiguration> {
switch (type) {
case DirectoryType.Ldap:
return await this.getLdapConfiguration();
case DirectoryType.GSuite:
return await this.getGsuiteConfiguration();
case DirectoryType.EntraID:
return await this.getEntraConfiguration();
case DirectoryType.Okta:
return await this.getOktaConfiguration();
case DirectoryType.OneLogin:
return await this.getOneLoginConfiguration();
}
}
async getLdapConfiguration(options?: StorageOptions): Promise<LdapConfiguration> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directoryConfigurations?.ldap;
}
async setLdapConfiguration(value: LdapConfiguration, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.directoryConfigurations.ldap = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getGsuiteConfiguration(options?: StorageOptions): Promise<GSuiteConfiguration> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directoryConfigurations?.gsuite;
}
async setGsuiteConfiguration(
value: GSuiteConfiguration,
options?: StorageOptions,
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.directoryConfigurations.gsuite = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getEntraConfiguration(options?: StorageOptions): Promise<EntraIdConfiguration> {
const entraConfig = (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directoryConfigurations?.entra;
if (entraConfig != null) {
return entraConfig;
}
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directoryConfigurations?.azure;
}
async setEntraConfiguration(
value: EntraIdConfiguration,
options?: StorageOptions,
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.directoryConfigurations.entra = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getOktaConfiguration(options?: StorageOptions): Promise<OktaConfiguration> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directoryConfigurations?.okta;
}
async setOktaConfiguration(value: OktaConfiguration, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.directoryConfigurations.okta = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getOneLoginConfiguration(options?: StorageOptions): Promise<OneLoginConfiguration> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directoryConfigurations?.oneLogin;
}
async setOneLoginConfiguration(
value: OneLoginConfiguration,
options?: StorageOptions,
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.directoryConfigurations.oneLogin = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getOrganizationId(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directorySettings?.organizationId;
}
async setOrganizationId(value: string, options?: StorageOptions): Promise<void> {
const currentId = await this.getOrganizationId();
if (currentId !== value) {
await this.clearSyncSettings();
}
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.directorySettings.organizationId = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getSync(options?: StorageOptions): Promise<SyncConfiguration> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directorySettings?.sync;
}
async setSync(value: SyncConfiguration, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.directorySettings.sync = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getDirectoryType(options?: StorageOptions): Promise<DirectoryType> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directorySettings?.directoryType;
}
async setDirectoryType(value: DirectoryType, options?: StorageOptions): Promise<void> {
const currentType = await this.getDirectoryType();
if (value !== currentType) {
await this.clearSyncSettings();
}
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.directorySettings.directoryType = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getLastUserSync(options?: StorageOptions): Promise<Date> {
const userSyncDate = (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directorySettings?.lastUserSync;
return userSyncDate ? new Date(userSyncDate) : null;
}
async setLastUserSync(value: Date, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.directorySettings.lastUserSync = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getLastGroupSync(options?: StorageOptions): Promise<Date> {
const groupSyncDate = (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directorySettings?.lastGroupSync;
return groupSyncDate ? new Date(groupSyncDate) : null;
}
async setLastGroupSync(value: Date, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.directorySettings.lastGroupSync = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getLastSyncHash(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directorySettings?.lastSyncHash;
}
async setLastSyncHash(value: string, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.directorySettings.lastSyncHash = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getSyncingDir(options?: StorageOptions): Promise<boolean> {
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
?.directorySettings?.syncingDir;
}
async setSyncingDir(value: boolean, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, this.defaultInMemoryOptions),
);
account.directorySettings.syncingDir = value;
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
}
async getUserDelta(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directorySettings?.userDelta;
}
async setUserDelta(value: string, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.directorySettings.userDelta = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getGroupDelta(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directorySettings?.groupDelta;
}
async setGroupDelta(value: string, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.directorySettings.groupDelta = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async clearSyncSettings(hashToo = false) {
await this.setUserDelta(null);
await this.setGroupDelta(null);
await this.setLastGroupSync(null);
await this.setLastUserSync(null);
if (hashToo) {
await this.setLastSyncHash(null);
}
}
protected async scaffoldNewAccountStorage(account: Account): Promise<void> {
await this.scaffoldNewAccountDiskStorage(account);
}
protected async scaffoldNewAccountDiskStorage(account: Account): Promise<void> {
const storageOptions = this.reconcileOptions(
{ userId: account.profile.userId },
await this.defaultOnDiskLocalOptions(),
);
const storedAccount = await this.getAccount(storageOptions);
if (storedAccount != null) {
account.settings = storedAccount.settings;
account.directorySettings = storedAccount.directorySettings;
account.directoryConfigurations = storedAccount.directoryConfigurations;
} else if (await this.hasTemporaryStorage()) {
// If migrating to state V2 with an no actively authed account we store temporary data to be copied on auth - this will only be run once.
account.settings = await this.storageService.get<any>(keys.tempAccountSettings);
account.directorySettings = await this.storageService.get<any>(keys.tempDirectorySettings);
account.directoryConfigurations = await this.storageService.get<any>(
keys.tempDirectoryConfigs,
);
await this.storageService.remove(keys.tempAccountSettings);
await this.storageService.remove(keys.tempDirectorySettings);
await this.storageService.remove(keys.tempDirectoryConfigs);
}
await this.saveAccount(account, storageOptions);
}
protected async pushAccounts(): Promise<void> {
if (this.state?.accounts == null || Object.keys(this.state.accounts).length < 1) {
this.accountsSubject.next(null);
return;
}
this.accountsSubject.next(this.state.accounts);
}
protected async hasTemporaryStorage(): Promise<boolean> {
return (
(await this.storageService.has(keys.tempAccountSettings)) ||
(await this.storageService.has(keys.tempDirectorySettings)) ||
(await this.storageService.has(keys.tempDirectoryConfigs))
);
}
protected resetAccount(account: Account) {
const persistentAccountInformation = {
settings: account.settings,
directorySettings: account.directorySettings,
directoryConfigurations: account.directoryConfigurations,
};
return Object.assign(this.createAccount(), persistentAccountInformation);
}
async getEnvironmentUrls(options?: StorageOptions): Promise<EnvironmentUrls> {
return this.getGlobalEnvironmentUrls(options);
}
}