mirror of
https://github.com/bitwarden/directory-connector
synced 2026-02-11 22:13:34 +00:00
Compare commits
4 Commits
dev-clarit
...
state-serv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0e74948bd | ||
|
|
9f8018e8f8 | ||
|
|
0bff38c459 | ||
|
|
94ff20f69f |
@@ -1,7 +1,3 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
# Bitwarden Directory Connector
|
||||
|
||||
## Project Overview
|
||||
@@ -24,392 +20,6 @@ Directory Connector is a TypeScript application that synchronizes users and grou
|
||||
- Node
|
||||
- Jest for testing
|
||||
|
||||
### Current Project Status
|
||||
|
||||
**Mission Critical but Deprioritized:** Directory Connector is used to sync customer directory services with their Bitwarden organization. While SCIM is the more modern cloud-hosted solution, not all directory services support SCIM, and SCIM is only available on Enterprise plans. Therefore, DC remains mission-critical infrastructure for many paying customers, but it's deprioritized in the codebase due to infrequent changes.
|
||||
|
||||
**Isolated Repository:** Unlike other Bitwarden client applications that live in a monorepo with shared core libraries, Directory Connector was kept separate when other TypeScript clients moved to the monorepo. It got its own copy of the jslib repo to avoid unnecessary regressions from apparently unrelated code changes in other clients. This severed it from the rest of the codebase, causing:
|
||||
|
||||
- Outdated dependencies that can't be updated (ES modules vs CommonJS conflicts)
|
||||
- File/folder structure that doesn't match modern Bitwarden client patterns
|
||||
- Accumulated technical debt requiring significant investment to pay down
|
||||
- jslib contains unused code from all clients, but cannot be deleted due to monolithic/tightly coupled architecture
|
||||
|
||||
**Critical Issues (Current Status):**
|
||||
|
||||
- ✅ ~~Electron, Node, and Angular are on unmaintained versions~~ **RESOLVED** - All updated (Electron 39, Node 20, Angular 21, TypeScript 5.9)
|
||||
- ❌ `keytar` is archived (Dec 2022) and incompatible with Node v22, **blocking Node upgrades beyond v20** - **PRIMARY BLOCKER**
|
||||
- ❌ No ESM support blocks dependency upgrades: googleapis, lowdb, chalk, inquirer, node-fetch, electron-store
|
||||
- ⚠️ 70 dev dependencies + 31 runtime dependencies = excessive maintenance burden (count increased with Angular 21 tooling)
|
||||
- ❌ StateService is a large pre-StateProvider monolith containing every getter/setter for all clients (PM-31159 In Progress)
|
||||
- ✅ ~~Angular CLI not used~~ **RESOLVED** - Angular CLI 21.1.2 now integrated with angular.json configuration
|
||||
|
||||
**Development Approach:** When working on this codebase, prioritize sustainability and maintainability over adding new features. Consider how changes will affect long-term maintenance burden.
|
||||
|
||||
## Tech Debt Roadmap
|
||||
|
||||
### Progress Summary
|
||||
|
||||
**Completed:**
|
||||
|
||||
- ✅ Phase 0 (Immediate Priority): All major dependencies upgraded (Node 20, Angular 21, TypeScript 5.9, Electron 39)
|
||||
- ✅ Phase 6: Angular CLI integration complete
|
||||
|
||||
**In Progress:**
|
||||
|
||||
- 🔄 Phase 1: StateService rewrite (PM-31159)
|
||||
|
||||
**Blocked/Todo:**
|
||||
|
||||
- ❌ Phase 2: Remove remaining jslib code (blocked by Phase 1)
|
||||
- ❌ Phase 3: Repository restructure (should be done before Phase 5)
|
||||
- ⚠️ Phase 4: Replace Keytar **[CRITICAL BLOCKER]** - blocking Node v22+ upgrades
|
||||
- ❌ Phase 5: ESM Support (blocked by Phase 3, needed for googleapis, lowdb, chalk, inquirer, etc.)
|
||||
|
||||
**Primary Blocker:** Keytar removal (Phase 4) is the most critical task as it blocks Node upgrades beyond v20.
|
||||
|
||||
---
|
||||
|
||||
### ✅ Immediate Priority: Unsupported Dependencies (COMPLETED)
|
||||
|
||||
**Upgrade Path (July 2025 release) - STATUS: COMPLETE**
|
||||
|
||||
All major version upgrades have been completed and exceeded targets:
|
||||
|
||||
1. ✅ Node 18.20.8 → 20.18 → **COMPLETE** (engines: `~20`, .nvmrc: `v20`)
|
||||
2. ✅ Angular 17 → 18.2.x → **EXCEEDED** (now at **21.1.1**)
|
||||
3. ✅ TypeScript 5.4.5 → 5.6.0 → **EXCEEDED** (now at **5.9.3**)
|
||||
4. ✅ Electron 34 → 36 → **EXCEEDED** (now at **39.2.1**)
|
||||
5. ✅ Angular matches clients monorepo version (21.x)
|
||||
|
||||
**Current Versions:**
|
||||
|
||||
- Node: v20 (project target), blocked from v22+ by keytar
|
||||
- TypeScript: 5.9.3
|
||||
- Angular: 21.1.1 (all packages)
|
||||
- Electron: 39.2.1 (well beyond EOL target of 36)
|
||||
- @yao-pkg/pkg: 5.16.1 (community fork replacing archived pkg)
|
||||
|
||||
**Note:** Further Node upgrades to v22+ are **blocked by keytar** (see Phase 4). Electron 36 was EOL October 2028, but we're already on 39.2.1.
|
||||
|
||||
### Phase 1: StateService Rewrite (PM-31159, In Progress)
|
||||
|
||||
**Problem:** StateService is a post-account-switching, pre-StateProvider monolith containing every getter/setter for all clients. This prevents deletion of unused data models and code. Never very stable, and more complex than DC needs (DC doesn't need account switching).
|
||||
|
||||
**Current Status:** 🔄 **Active PR** - [#990](https://github.com/bitwarden/directory-connector/pull/990) (Open, Author: @BTreston)
|
||||
|
||||
- PR created: Feb 2, 2026
|
||||
- Last updated: Feb 5, 2026
|
||||
- Files changed: 17 files (+1,512, -41 lines)
|
||||
- Commits: 4 (scaffold, add tests, fix type issues, fix integration test)
|
||||
|
||||
**Implementation Details:**
|
||||
|
||||
**New Architecture:**
|
||||
|
||||
- Created `StateServiceVNext` interface (`src/abstractions/state-vNext.service.ts`)
|
||||
- New implementation: `StateServiceVNextImplementation` (`src/services/state-service/state-vNext.service.ts`)
|
||||
- New state model with flat key-value structure (`src/models/state.model.ts`)
|
||||
- Comprehensive test suite: `state-vNext.service.spec.ts` (488 lines of tests)
|
||||
|
||||
**Storage Key Structure:**
|
||||
|
||||
```typescript
|
||||
// vNext Storage Keys (Flat key-value structure)
|
||||
StorageKeysVNext = {
|
||||
stateVersion: "stateVersion",
|
||||
directoryType: "directoryType",
|
||||
organizationId: "organizationId",
|
||||
directory_ldap: "directory_ldap",
|
||||
directory_gsuite: "directory_gsuite",
|
||||
directory_entra: "directory_entra",
|
||||
directory_okta: "directory_okta",
|
||||
directory_onelogin: "directory_onelogin",
|
||||
sync: "sync",
|
||||
syncingDir: "syncingDir",
|
||||
};
|
||||
|
||||
// Secure storage keys for sensitive data
|
||||
SecureStorageKeysVNext = {
|
||||
ldap: "secret_ldap",
|
||||
gsuite: "secret_gsuite",
|
||||
azure: "secret_azure", // Backwards compatible with old name
|
||||
entra: "secret_entra",
|
||||
okta: "secret_okta",
|
||||
oneLogin: "secret_oneLogin",
|
||||
userDelta: "userDeltaToken",
|
||||
groupDelta: "groupDeltaToken",
|
||||
lastUserSync: "lastUserSync",
|
||||
lastGroupSync: "lastGroupSync",
|
||||
lastSyncHash: "lastSyncHash",
|
||||
};
|
||||
```
|
||||
|
||||
**Migration Strategy:**
|
||||
|
||||
- State version bumped to `StateVersion.Five` (`jslib/common/src/enums/stateVersion.ts`)
|
||||
- Enhanced `StateMigrationService` to handle migration from old account-based structure to new flat structure
|
||||
- Migration keys defined for backwards compatibility (`MigrationKeys`, `SecureStorageKeysMigration`)
|
||||
- Temporary keys used during migration (`TempKeys`) to preserve data during transition
|
||||
|
||||
**File Organization:**
|
||||
|
||||
- State-related files moved to `src/services/state-service/` subdirectory:
|
||||
- `state-vNext.service.ts` (new implementation)
|
||||
- `state-vNext.service.spec.ts` (488 lines of tests)
|
||||
- `state.service.ts` (legacy, moved from `src/services/`)
|
||||
- `stateMigration.service.ts` (enhanced for v5 migration)
|
||||
- New abstraction: `src/abstractions/state-vNext.service.ts`
|
||||
- New model: `src/models/state.model.ts` (defines all storage keys)
|
||||
|
||||
**Integration:**
|
||||
|
||||
- Both old `StateService` and new `StateServiceVNext` injected in parallel during migration phase
|
||||
- `DirectoryFactoryService` updated to accept both services
|
||||
- Services module provides both implementations
|
||||
- CLI (`bwdc.ts`) and GUI (`main.ts`) both instantiate new service alongside old one
|
||||
|
||||
**Chosen Approach Benefits:**
|
||||
|
||||
- Clean break with old StateService - high degree of certainty
|
||||
- Simple and focused on DC's needs (no account switching, no rxjs)
|
||||
- Flat key-value structure easier to maintain
|
||||
- Versioning and migration capabilities included
|
||||
- Keeps existing data.json around during transition
|
||||
- All getters/setters in one place (acceptable for small application)
|
||||
|
||||
**Rejected Approaches:**
|
||||
|
||||
- Copy StateProvider from clients: Too complex (supports account switching, rxjs, syncing background/foreground contexts)
|
||||
- Rewrite simplified StateService keeping current data structure: Commits us to previous decisions, keeps monolithic account objects
|
||||
|
||||
**Next Steps:**
|
||||
|
||||
- Complete PR review and merge
|
||||
- Monitor for regressions during initial rollout
|
||||
- After several releases, can remove old StateService and migration code
|
||||
- Begin Phase 2: Remove remaining jslib code that was only needed by old StateService
|
||||
|
||||
### Phase 2: Remove Remaining jslib Code
|
||||
|
||||
After StateService is removed, review and delete old models and remaining services that referenced each other. jslib contains unused code from all clients that DC doesn't need.
|
||||
|
||||
### Phase 3: Restructure Repository (PM-31852, To Do)
|
||||
|
||||
**Current Structure:**
|
||||
|
||||
```
|
||||
src/ # Both Electron and CLI app code
|
||||
src-cli/ # package.json entry point for CLI only, no code
|
||||
jslib/
|
||||
├── common/ # Shared common code
|
||||
├── node/ # Node specific code used in CLI
|
||||
└── electron/ # Electron specific code used in GUI
|
||||
```
|
||||
|
||||
**Target Structure:**
|
||||
|
||||
```
|
||||
src-gui/ # Electron specific code only (combining src (partial) + jslib/electron)
|
||||
src-cli/ # Node and CLI specific code only (combining src (partial) + jslib/node)
|
||||
libs/ # Shared app-independent DC code, e.g. sync services (combining src (partial) + jslib/common)
|
||||
```
|
||||
|
||||
**Why:** Makes subsequent changes (code reorganizing, ESM support) much easier. This should be done early in the modernization process.
|
||||
|
||||
### Phase 4: Replace Keytar (PM-12436, To Do) ⚠️ **CRITICAL BLOCKER**
|
||||
|
||||
**Problem:** `keytar` (OS secure storage for secrets) was archived December 2022 and is incompatible with Node v22, **actively blocking Node upgrades beyond v20**.
|
||||
|
||||
**Current Status:**
|
||||
|
||||
- `keytar`: **7.9.0** (still present in dependencies)
|
||||
- **This is the #1 blocker preventing Node v22+ upgrades**
|
||||
- All "Immediate Priority" dependencies have been upgraded, but further progress requires removing keytar
|
||||
|
||||
**Solution:** Migrate to Bitwarden's Rust implementation in `desktop_native` (same as clients monorepo did)
|
||||
|
||||
1. Implement Rust <-> NAPI integration (like `desktop_native/napi`) from Electron app to Rust code
|
||||
2. Copy, rename, and expose necessary functions
|
||||
3. Point to `desktop_native` crate using git link from DC repo (no need for SDK yet):
|
||||
```rust
|
||||
desktop_core = { git = "https://github.com/bitwarden/clients", rev = "00cf24972d944638bbd1adc00a0ae3eeabb6eb9a" }
|
||||
```
|
||||
|
||||
**Important:** `keytar` uses wrong encoding on Windows (UTF-8 instead of UTF-16). Bitwarden uses UTF-16. Code should contain a migration - ensure old values are migrated correctly during testing.
|
||||
|
||||
**Priority:** This should be prioritized as it's blocking the Node upgrade path and has been archived for over 2 years.
|
||||
|
||||
### Phase 5: Add ESM Support (PM-31850, To Do)
|
||||
|
||||
**Problem:** No ESM module support prevents upgrading key dependencies.
|
||||
|
||||
**Blocked Dependencies (Current Status):**
|
||||
|
||||
- ❌ `googleapis`: **149.0.0** → current (major dependency, disabled in renovate.json5)
|
||||
- ❌ `lowdb`: **1.0.0** → v7
|
||||
- ❌ `@types/lowdb`: **1.0.15** (can be deleted once inquirer is upgraded)
|
||||
- ❌ `@electron/notarize`: **2.5.0** → v3.0.1
|
||||
- ❌ `chalk`: **4.1.2** → v5.3.0
|
||||
- ❌ `inquirer`: **8.2.6** → v12.1.0
|
||||
- ❌ `@types/inquirer`: **8.2.10** (should be deleted when inquirer upgraded)
|
||||
- ❌ `node-fetch`: **2.7.0** → v3.3.2 (should use native Node fetch API when on Node >=21)
|
||||
- ❌ `electron-store`: **8.2.0** → v10.1.0
|
||||
|
||||
**Status:** These dependencies remain blocked as expected. They will stay on old versions until:
|
||||
|
||||
1. Phase 3 (Repository Restructure) is complete
|
||||
2. ESM support is implemented
|
||||
3. Note: These ESM dependencies are primarily used in CLI build, so restructuring first (Phase 3) will limit the impact of ESM migration.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. Update tsconfig.json and package.json configurations
|
||||
2. Update import/export syntax to no longer use `require` statements
|
||||
3. Upgrade dependencies to move away from CommonJS (ESM can import CommonJS, but not vice versa)
|
||||
4. Trial and error
|
||||
|
||||
**Reference:** [Pure ESM package guide](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c)
|
||||
|
||||
### Phase 6: Add Angular CLI (PM-31849, In Progress / Possibly Complete?)
|
||||
|
||||
**Problem:** Angular CLI provides great DX and makes it easier to manage Angular changes (e.g. auto-migrations). DC didn't use it.
|
||||
|
||||
**Current Status:**
|
||||
|
||||
- ✅ `@angular/cli`: **21.1.2** is now present in **runtime dependencies**
|
||||
- ✅ `@angular/build`: **21.1.2** is present in dev dependencies
|
||||
- ✅ All Angular tooling has been updated to v21.x
|
||||
|
||||
**Status:** ✅ **COMPLETE** - Angular CLI has been successfully integrated:
|
||||
|
||||
- `angular.json` configuration file exists
|
||||
- `.angular/` cache directory present
|
||||
- `@angular/cli` 21.1.2 in runtime dependencies
|
||||
- `@angular/build` 21.1.2 in dev dependencies
|
||||
- All Angular packages updated to v21.x
|
||||
|
||||
This migration provides improved DX and access to Angular's auto-migration tools for future updates.
|
||||
|
||||
### Additional Considerations
|
||||
|
||||
**Reduce Dependency Count:** Current state is 70 dev dependencies + 31 runtime dependencies (101 total). The dev dependency count increased from the original 66 due to Angular 21 upgrade adding additional tooling. After removing old code, review dependency list:
|
||||
|
||||
- Can we remove some after code cleanup?
|
||||
- Could we reintegrate with monorepo to leverage Component Library and shared platform dependencies?
|
||||
- **Risk:** Becomes tightly coupled with monorepo code → regression risk, move slower due to coupling
|
||||
|
||||
**GitHub Workflows:** Need review and modernization:
|
||||
|
||||
- PM-20478: Add check-run workflow for CI on community PRs
|
||||
- PM-18290: Add linting workflow
|
||||
- PM-18289: Update build workflow
|
||||
- `pkg` and `pkg-fetch` for packaging Node runtime in CLI release are archived (fork exists but untrusted; clients vets all changes manually)
|
||||
- Options: Make our own fork, or use Node's single executable binary support (investigate)
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
### Desktop App (Electron + Angular)
|
||||
|
||||
**Initial Setup:**
|
||||
|
||||
```bash
|
||||
npm install # Install dependencies (runs git submodule init automatically)
|
||||
npm run rebuild # Rebuild native modules for Electron
|
||||
```
|
||||
|
||||
**Development:**
|
||||
|
||||
```bash
|
||||
npm run electron # Build and run desktop app with hot reload and debugging
|
||||
npm run electron:ignore # Same as above but ignores certificate errors
|
||||
```
|
||||
|
||||
**Building:**
|
||||
|
||||
```bash
|
||||
npm run build # Build both main and renderer processes
|
||||
npm run build:main # Build Electron main process only
|
||||
npm run build:renderer # Build Angular renderer process only
|
||||
npm run build:renderer:watch # Build renderer with file watching
|
||||
```
|
||||
|
||||
**Distribution:**
|
||||
|
||||
```bash
|
||||
npm run dist:mac # Create macOS distributable
|
||||
npm run dist:win # Create Windows distributable
|
||||
npm run dist:lin # Create Linux distributable
|
||||
```
|
||||
|
||||
### CLI (bwdc)
|
||||
|
||||
**Development:**
|
||||
|
||||
```bash
|
||||
npm run build:cli:watch # Build CLI with file watching
|
||||
node ./build-cli/bwdc.js --help # Run the CLI from build output
|
||||
```
|
||||
|
||||
**Production Build:**
|
||||
|
||||
```bash
|
||||
npm run build:cli:prod # Build CLI for production
|
||||
npm run dist:cli # Create platform-specific CLI executables (all platforms)
|
||||
npm run dist:cli:mac # Create macOS CLI executable only
|
||||
npm run dist:cli:win # Create Windows CLI executable only
|
||||
npm run dist:cli:lin # Create Linux CLI executable only
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
**Unit Tests:**
|
||||
|
||||
```bash
|
||||
npm test # Run unit tests (excludes integration tests)
|
||||
npm run test:watch # Run unit tests in watch mode
|
||||
npm run test:watch:all # Run unit tests in watch mode (all files)
|
||||
npm run test:types # Run TypeScript type checking without emitting files
|
||||
```
|
||||
|
||||
**Integration Tests:**
|
||||
|
||||
```bash
|
||||
npm run test:integration:setup # Set up Docker containers for LDAP testing
|
||||
npm run test:integration # Run integration tests
|
||||
npm run test:integration:watch # Run integration tests in watch mode
|
||||
```
|
||||
|
||||
Integration tests require Docker and test against live directory services. The setup command creates OpenLDAP containers using docker-compose.yml.
|
||||
|
||||
### Linting & Formatting
|
||||
|
||||
```bash
|
||||
npm run lint # Run ESLint and Prettier checks
|
||||
npm run lint:fix # Auto-fix ESLint issues
|
||||
npm run prettier # Format all files with Prettier
|
||||
```
|
||||
|
||||
### Submodule Management
|
||||
|
||||
The `jslib` folder is a git submodule containing shared Bitwarden libraries:
|
||||
|
||||
```bash
|
||||
npm run sub:update # Update submodule to latest remote version
|
||||
npm run sub:pull # Pull latest changes in submodule
|
||||
npm run sub:commit # Pull and commit submodule update
|
||||
```
|
||||
|
||||
### Utility Commands
|
||||
|
||||
```bash
|
||||
npm run reset # Remove keytar modules and reinstall (use when switching between CLI/desktop)
|
||||
npm run clean:dist # Clean desktop distribution files
|
||||
npm run clean:dist:cli # Clean CLI distribution files
|
||||
```
|
||||
|
||||
**Important:** When switching between developing the desktop app and CLI, run `npm run reset` to avoid native module conflicts.
|
||||
|
||||
## Code Architecture & Structure
|
||||
|
||||
### Directory Organization
|
||||
@@ -435,32 +45,6 @@ jslib/ # Legacy folder structure (mix of deprecated/unused and c
|
||||
3. **Directory Service Pattern**: Each directory provider implements `IDirectoryService` interface
|
||||
4. **Separation of Concerns**: GUI (Angular app) and CLI (commands) share the same service layer
|
||||
|
||||
### Core Synchronization Flow
|
||||
|
||||
The sync process follows this pattern:
|
||||
|
||||
1. **DirectoryFactoryService** (`src/services/directory-factory.service.ts`) - Creates the appropriate directory service based on DirectoryType configuration
|
||||
2. **IDirectoryService** implementation (`src/services/directory-services/*.service.ts`) - Each provider (LDAP, Entra ID, Google, Okta, OneLogin) implements:
|
||||
- `getEntries(force, test)` - Returns `[GroupEntry[], UserEntry[]]`
|
||||
- Provider-specific authentication and API calls
|
||||
3. **SyncService** (`src/services/sync.service.ts`) - Orchestrates the sync:
|
||||
- Calls directory service to get entries
|
||||
- Filters and deduplicates users/groups
|
||||
- Uses BatchRequestBuilder or SingleRequestBuilder to format API requests
|
||||
- Generates hash to detect changes and avoid redundant syncs
|
||||
- Sends data to Bitwarden API via ApiService
|
||||
4. **Request Builders** (`src/services/*-request-builder.ts`) - Transform directory entries into Bitwarden API format
|
||||
|
||||
### Shared Library (jslib)
|
||||
|
||||
The `jslib` folder is a git submodule containing shared Bitwarden code:
|
||||
|
||||
- Common services (API, Crypto, Storage, Auth)
|
||||
- Platform utilities
|
||||
- Shared models and abstractions
|
||||
|
||||
**Important:** This is legacy structure - do not add new code to jslib. New code should go in `src/`.
|
||||
|
||||
## Development Conventions
|
||||
|
||||
### Code Organization
|
||||
|
||||
23
.github/PULL_REQUEST_TEMPLATE.md
vendored
23
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -9,3 +9,26 @@
|
||||
## 📸 Screenshots
|
||||
|
||||
<!-- Required for any UI changes; delete if not applicable. Use fixed width images for better display. -->
|
||||
|
||||
## ⏰ Reminders before review
|
||||
|
||||
- Contributor guidelines followed
|
||||
- All formatters and local linters executed and passed
|
||||
- Written new unit and / or integration tests where applicable
|
||||
- Used internationalization (i18n) for all UI strings
|
||||
- CI builds passed
|
||||
- Communicated to DevOps any deployment requirements
|
||||
- Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team
|
||||
|
||||
## 🦮 Reviewer guidelines
|
||||
|
||||
<!-- Suggested interactions but feel free to use (or not) as you desire! -->
|
||||
|
||||
- 👍 (`:+1:`) or similar for great changes
|
||||
- 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info
|
||||
- ❓ (`:question:`) for questions
|
||||
- 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
|
||||
- 🎨 (`:art:`) for suggestions / improvements
|
||||
- ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention
|
||||
- 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt
|
||||
- ⛏ (`:pick:`) for minor or nitpick changes
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, ModuleWithProviders, NgModule } from "@angular/core";
|
||||
import { DefaultNoComponentGlobalConfig, GlobalConfig, Toast, TOAST_CONFIG } from "ngx-toastr";
|
||||
import {
|
||||
DefaultNoComponentGlobalConfig,
|
||||
GlobalConfig,
|
||||
Toast as BaseToast,
|
||||
ToastPackage,
|
||||
ToastrService,
|
||||
TOAST_CONFIG,
|
||||
} from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: "[toast-component2]",
|
||||
template: `
|
||||
@if (options().closeButton) {
|
||||
@if (options.closeButton) {
|
||||
<button (click)="remove()" type="button" class="toast-close-button" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
@@ -14,64 +22,59 @@ import { DefaultNoComponentGlobalConfig, GlobalConfig, Toast, TOAST_CONFIG } fro
|
||||
<i></i>
|
||||
</div>
|
||||
<div>
|
||||
@if (title()) {
|
||||
<div [class]="options().titleClass" [attr.aria-label]="title()">
|
||||
{{ title() }}
|
||||
@if (title) {
|
||||
<div [class]="options.titleClass" [attr.aria-label]="title">
|
||||
{{ title }}
|
||||
@if (duplicatesCount) {
|
||||
[{{ duplicatesCount + 1 }}]
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (message() && options().enableHtml) {
|
||||
@if (message && options.enableHtml) {
|
||||
<div
|
||||
role="alertdialog"
|
||||
aria-live="polite"
|
||||
[class]="options().messageClass"
|
||||
[innerHTML]="message()"
|
||||
[class]="options.messageClass"
|
||||
[innerHTML]="message"
|
||||
></div>
|
||||
}
|
||||
@if (message() && !options().enableHtml) {
|
||||
@if (message && !options.enableHtml) {
|
||||
<div
|
||||
role="alertdialog"
|
||||
aria-live="polite"
|
||||
[class]="options().messageClass"
|
||||
[attr.aria-label]="message()"
|
||||
[class]="options.messageClass"
|
||||
[attr.aria-label]="message"
|
||||
>
|
||||
{{ message() }}
|
||||
{{ message }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (options().progressBar) {
|
||||
@if (options.progressBar) {
|
||||
<div>
|
||||
<div class="toast-progress" [style.width]="width + '%'"></div>
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
&.toast-in {
|
||||
animation: toast-animation var(--animation-duration) var(--animation-easing);
|
||||
}
|
||||
|
||||
&.toast-out {
|
||||
animation: toast-animation var(--animation-duration) var(--animation-easing) reverse
|
||||
forwards;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes toast-animation {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`,
|
||||
animations: [
|
||||
trigger("flyInOut", [
|
||||
state("inactive", style({ opacity: 0 })),
|
||||
state("active", style({ opacity: 1 })),
|
||||
state("removed", style({ opacity: 0 })),
|
||||
transition("inactive => active", animate("{{ easeTime }}ms {{ easing }}")),
|
||||
transition("active => removed", animate("{{ easeTime }}ms {{ easing }}")),
|
||||
]),
|
||||
],
|
||||
preserveWhitespaces: false,
|
||||
standalone: false,
|
||||
})
|
||||
export class BitwardenToast extends Toast {}
|
||||
export class BitwardenToast extends BaseToast {
|
||||
constructor(
|
||||
protected toastrService: ToastrService,
|
||||
public toastPackage: ToastPackage,
|
||||
) {
|
||||
super(toastrService, toastPackage);
|
||||
}
|
||||
}
|
||||
|
||||
export const BitwardenToastGlobalConfig: GlobalConfig = {
|
||||
...DefaultNoComponentGlobalConfig,
|
||||
|
||||
195
jslib/common/spec/domain/encString.spec.ts
Normal file
195
jslib/common/spec/domain/encString.spec.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { Substitute, Arg } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
||||
import { EncryptionType } from "@/jslib/common/src/enums/encryptionType";
|
||||
import { EncString } from "@/jslib/common/src/models/domain/encString";
|
||||
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
|
||||
import { ContainerService } from "@/jslib/common/src/services/container.service";
|
||||
|
||||
describe("EncString", () => {
|
||||
afterEach(() => {
|
||||
(window as any).bitwardenContainerService = undefined;
|
||||
});
|
||||
|
||||
describe("Rsa2048_OaepSha256_B64", () => {
|
||||
it("constructor", () => {
|
||||
const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data");
|
||||
|
||||
expect(encString).toEqual({
|
||||
data: "data",
|
||||
encryptedString: "3.data",
|
||||
encryptionType: 3,
|
||||
});
|
||||
});
|
||||
|
||||
describe("parse existing", () => {
|
||||
it("valid", () => {
|
||||
const encString = new EncString("3.data");
|
||||
|
||||
expect(encString).toEqual({
|
||||
data: "data",
|
||||
encryptedString: "3.data",
|
||||
encryptionType: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it("invalid", () => {
|
||||
const encString = new EncString("3.data|test");
|
||||
|
||||
expect(encString).toEqual({
|
||||
encryptedString: "3.data|test",
|
||||
encryptionType: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("decrypt", () => {
|
||||
const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data");
|
||||
|
||||
const cryptoService = Substitute.for<CryptoService>();
|
||||
cryptoService.getOrgKey(null).resolves(null);
|
||||
cryptoService.decryptToUtf8(encString, Arg.any()).resolves("decrypted");
|
||||
|
||||
beforeEach(() => {
|
||||
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
|
||||
});
|
||||
|
||||
it("decrypts correctly", async () => {
|
||||
const decrypted = await encString.decrypt(null);
|
||||
|
||||
expect(decrypted).toBe("decrypted");
|
||||
});
|
||||
|
||||
it("result should be cached", async () => {
|
||||
const decrypted = await encString.decrypt(null);
|
||||
cryptoService.received(1).decryptToUtf8(Arg.any(), Arg.any());
|
||||
|
||||
expect(decrypted).toBe("decrypted");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("AesCbc256_B64", () => {
|
||||
it("constructor", () => {
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data", "iv");
|
||||
|
||||
expect(encString).toEqual({
|
||||
data: "data",
|
||||
encryptedString: "0.iv|data",
|
||||
encryptionType: 0,
|
||||
iv: "iv",
|
||||
});
|
||||
});
|
||||
|
||||
describe("parse existing", () => {
|
||||
it("valid", () => {
|
||||
const encString = new EncString("0.iv|data");
|
||||
|
||||
expect(encString).toEqual({
|
||||
data: "data",
|
||||
encryptedString: "0.iv|data",
|
||||
encryptionType: 0,
|
||||
iv: "iv",
|
||||
});
|
||||
});
|
||||
|
||||
it("invalid", () => {
|
||||
const encString = new EncString("0.iv|data|mac");
|
||||
|
||||
expect(encString).toEqual({
|
||||
encryptedString: "0.iv|data|mac",
|
||||
encryptionType: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("AesCbc256_HmacSha256_B64", () => {
|
||||
it("constructor", () => {
|
||||
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data", "iv", "mac");
|
||||
|
||||
expect(encString).toEqual({
|
||||
data: "data",
|
||||
encryptedString: "2.iv|data|mac",
|
||||
encryptionType: 2,
|
||||
iv: "iv",
|
||||
mac: "mac",
|
||||
});
|
||||
});
|
||||
|
||||
it("valid", () => {
|
||||
const encString = new EncString("2.iv|data|mac");
|
||||
|
||||
expect(encString).toEqual({
|
||||
data: "data",
|
||||
encryptedString: "2.iv|data|mac",
|
||||
encryptionType: 2,
|
||||
iv: "iv",
|
||||
mac: "mac",
|
||||
});
|
||||
});
|
||||
|
||||
it("invalid", () => {
|
||||
const encString = new EncString("2.iv|data");
|
||||
|
||||
expect(encString).toEqual({
|
||||
encryptedString: "2.iv|data",
|
||||
encryptionType: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("Exit early if null", () => {
|
||||
const encString = new EncString(null);
|
||||
|
||||
expect(encString).toEqual({
|
||||
encryptedString: null,
|
||||
});
|
||||
});
|
||||
|
||||
describe("decrypt", () => {
|
||||
it("throws exception when bitwarden container not initialized", async () => {
|
||||
const encString = new EncString(null);
|
||||
|
||||
expect.assertions(1);
|
||||
try {
|
||||
await encString.decrypt(null);
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual("global bitwardenContainerService not initialized.");
|
||||
}
|
||||
});
|
||||
|
||||
it("handles value it can't decrypt", async () => {
|
||||
const encString = new EncString(null);
|
||||
|
||||
const cryptoService = Substitute.for<CryptoService>();
|
||||
cryptoService.getOrgKey(null).resolves(null);
|
||||
cryptoService.decryptToUtf8(encString, Arg.any()).throws("error");
|
||||
|
||||
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
|
||||
|
||||
const decrypted = await encString.decrypt(null);
|
||||
|
||||
expect(decrypted).toBe("[error: cannot decrypt]");
|
||||
|
||||
expect(encString).toEqual({
|
||||
decryptedValue: "[error: cannot decrypt]",
|
||||
encryptedString: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("passes along key", async () => {
|
||||
const encString = new EncString(null);
|
||||
const key = Substitute.for<SymmetricCryptoKey>();
|
||||
|
||||
const cryptoService = Substitute.for<CryptoService>();
|
||||
cryptoService.getOrgKey(null).resolves(null);
|
||||
|
||||
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
|
||||
|
||||
await encString.decrypt(null, key);
|
||||
|
||||
cryptoService.received().decryptToUtf8(encString, key);
|
||||
});
|
||||
});
|
||||
});
|
||||
84
jslib/common/spec/services/stateMigration.service.ts
Normal file
84
jslib/common/spec/services/stateMigration.service.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
|
||||
import { StateVersion } from "@/jslib/common/src/enums/stateVersion";
|
||||
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
|
||||
import { Account } from "@/jslib/common/src/models/domain/account";
|
||||
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
|
||||
import { StateMigrationService } from "@/jslib/common/src/services/stateMigration.service";
|
||||
|
||||
const userId = "USER_ID";
|
||||
|
||||
describe("State Migration Service", () => {
|
||||
let storageService: SubstituteOf<StorageService>;
|
||||
let secureStorageService: SubstituteOf<StorageService>;
|
||||
let stateFactory: SubstituteOf<StateFactory>;
|
||||
|
||||
let stateMigrationService: StateMigrationService;
|
||||
|
||||
beforeEach(() => {
|
||||
storageService = Substitute.for<StorageService>();
|
||||
secureStorageService = Substitute.for<StorageService>();
|
||||
stateFactory = Substitute.for<StateFactory>();
|
||||
|
||||
stateMigrationService = new StateMigrationService(
|
||||
storageService,
|
||||
secureStorageService,
|
||||
stateFactory,
|
||||
);
|
||||
});
|
||||
|
||||
describe("StateVersion 3 to 4 migration", async () => {
|
||||
beforeEach(() => {
|
||||
const globalVersion3: Partial<GlobalState> = {
|
||||
stateVersion: StateVersion.Three,
|
||||
};
|
||||
|
||||
storageService.get("global", Arg.any()).resolves(globalVersion3);
|
||||
storageService.get("authenticatedAccounts", Arg.any()).resolves([userId]);
|
||||
});
|
||||
|
||||
it("clears everBeenUnlocked", async () => {
|
||||
const accountVersion3: Account = {
|
||||
profile: {
|
||||
apiKeyClientId: null,
|
||||
convertAccountToKeyConnector: null,
|
||||
email: "EMAIL",
|
||||
emailVerified: true,
|
||||
everBeenUnlocked: true,
|
||||
hasPremiumPersonally: false,
|
||||
kdfIterations: 100000,
|
||||
kdfType: 0,
|
||||
keyHash: "KEY_HASH",
|
||||
lastSync: "LAST_SYNC",
|
||||
userId: userId,
|
||||
usesKeyConnector: false,
|
||||
forcePasswordReset: false,
|
||||
},
|
||||
};
|
||||
|
||||
const expectedAccountVersion4: Account = {
|
||||
profile: {
|
||||
...accountVersion3.profile,
|
||||
},
|
||||
};
|
||||
delete expectedAccountVersion4.profile.everBeenUnlocked;
|
||||
|
||||
storageService.get(userId, Arg.any()).resolves(accountVersion3);
|
||||
|
||||
await stateMigrationService.migrate();
|
||||
|
||||
storageService.received(1).save(userId, expectedAccountVersion4, Arg.any());
|
||||
});
|
||||
|
||||
it("updates StateVersion number", async () => {
|
||||
await stateMigrationService.migrate();
|
||||
|
||||
storageService.received(1).save(
|
||||
"global",
|
||||
Arg.is((globals: GlobalState) => globals.stateVersion === StateVersion.Four),
|
||||
Arg.any(),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,7 @@
|
||||
import { Substitute, Arg } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { EncString } from "@/jslib/common/src/models/domain/encString";
|
||||
|
||||
function newGuid() {
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
@@ -17,6 +21,13 @@ export function BuildTestObject<T, K extends keyof T = keyof T>(
|
||||
return Object.assign(constructor === null ? {} : new constructor(), def) as T;
|
||||
}
|
||||
|
||||
export function mockEnc(s: string): EncString {
|
||||
const mock = Substitute.for<EncString>();
|
||||
mock.decrypt(Arg.any(), Arg.any()).resolves(s);
|
||||
|
||||
return mock;
|
||||
}
|
||||
|
||||
export function makeStaticByteArray(length: number, start = 0) {
|
||||
const arr = new Uint8Array(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
|
||||
@@ -3,5 +3,6 @@ export enum StateVersion {
|
||||
Two = 2, // Move to a typed State object
|
||||
Three = 3, // Fix migration of users' premium status
|
||||
Four = 4, // Fix 'Never Lock' option by removing stale data
|
||||
Latest = Four,
|
||||
Five = 5, // New state service implementation
|
||||
Latest = Five,
|
||||
}
|
||||
|
||||
@@ -127,13 +127,6 @@ export class WindowMain {
|
||||
},
|
||||
});
|
||||
|
||||
// Enable SharedArrayBuffer. See https://developer.chrome.com/blog/enabling-shared-array-buffer/#cross-origin-isolation
|
||||
this.win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
||||
details.responseHeaders["Cross-Origin-Opener-Policy"] = ["same-origin"];
|
||||
details.responseHeaders["Cross-Origin-Embedder-Policy"] = ["require-corp"];
|
||||
callback({ responseHeaders: details.responseHeaders });
|
||||
});
|
||||
|
||||
if (this.windowStates[mainWindowSizeKey].isMaximized) {
|
||||
this.win.maximize();
|
||||
}
|
||||
|
||||
4207
package-lock.json
generated
4207
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "@bitwarden/directory-connector",
|
||||
"productName": "Bitwarden Directory Connector",
|
||||
"description": "Sync your user directory to your Bitwarden organization.",
|
||||
"version": "2026.2.0",
|
||||
"version": "2026.1.0",
|
||||
"keywords": [
|
||||
"bitwarden",
|
||||
"password",
|
||||
@@ -75,12 +75,13 @@
|
||||
"devDependencies": {
|
||||
"@angular-eslint/eslint-plugin-template": "21.1.0",
|
||||
"@angular-eslint/template-parser": "21.1.0",
|
||||
"@angular/build": "21.1.2",
|
||||
"@angular/compiler-cli": "21.1.1",
|
||||
"@angular/build": "21.0.5",
|
||||
"@angular/compiler-cli": "21.0.8",
|
||||
"@electron/notarize": "2.5.0",
|
||||
"@electron/rebuild": "4.0.1",
|
||||
"@fluffy-spoon/substitute": "1.208.0",
|
||||
"@microsoft/microsoft-graph-types": "2.43.1",
|
||||
"@ngtools/webpack": "21.1.2",
|
||||
"@ngtools/webpack": "21.0.5",
|
||||
"@types/inquirer": "8.2.10",
|
||||
"@types/jest": "30.0.0",
|
||||
"@types/lowdb": "1.0.15",
|
||||
@@ -90,10 +91,10 @@
|
||||
"@types/proper-lockfile": "4.1.4",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/tldjs": "2.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "8.54.0",
|
||||
"@typescript-eslint/parser": "8.54.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.0",
|
||||
"@typescript-eslint/parser": "8.50.0",
|
||||
"@yao-pkg/pkg": "5.16.1",
|
||||
"babel-loader": "10.0.0",
|
||||
"babel-loader": "9.2.1",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
"jest-environment-jsdom": "30.2.0",
|
||||
"concurrently": "9.2.0",
|
||||
@@ -123,11 +124,11 @@
|
||||
"jest-mock-extended": "4.0.0",
|
||||
"jest-preset-angular": "16.0.0",
|
||||
"lint-staged": "16.2.6",
|
||||
"mini-css-extract-plugin": "2.10.0",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"minimatch": "5.1.2",
|
||||
"node-forge": "1.3.2",
|
||||
"node-loader": "2.1.0",
|
||||
"prettier": "3.8.1",
|
||||
"prettier": "3.7.4",
|
||||
"rimraf": "6.1.0",
|
||||
"rxjs": "7.8.2",
|
||||
"sass": "1.97.1",
|
||||
@@ -135,25 +136,25 @@
|
||||
"ts-jest": "29.4.1",
|
||||
"ts-loader": "9.5.2",
|
||||
"tsconfig-paths-webpack-plugin": "4.2.0",
|
||||
"type-fest": "5.4.2",
|
||||
"type-fest": "5.3.0",
|
||||
"typescript": "5.9.3",
|
||||
"webpack": "5.104.1",
|
||||
"webpack-cli": "6.0.1",
|
||||
"webpack-merge": "6.0.1",
|
||||
"webpack-node-externals": "3.0.0",
|
||||
"zone.js": "0.16.0"
|
||||
"zone.js": "0.15.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "21.1.1",
|
||||
"@angular/cdk": "21.1.1",
|
||||
"@angular/cli": "21.1.2",
|
||||
"@angular/common": "21.1.1",
|
||||
"@angular/compiler": "21.1.1",
|
||||
"@angular/core": "21.1.1",
|
||||
"@angular/forms": "21.1.1",
|
||||
"@angular/platform-browser": "21.1.1",
|
||||
"@angular/platform-browser-dynamic": "21.1.1",
|
||||
"@angular/router": "21.1.1",
|
||||
"@angular/animations": "21.0.8",
|
||||
"@angular/cdk": "21.0.6",
|
||||
"@angular/cli": "21.0.5",
|
||||
"@angular/common": "21.0.8",
|
||||
"@angular/compiler": "21.0.8",
|
||||
"@angular/core": "21.0.8",
|
||||
"@angular/forms": "21.0.8",
|
||||
"@angular/platform-browser": "21.0.8",
|
||||
"@angular/platform-browser-dynamic": "21.0.8",
|
||||
"@angular/router": "21.0.8",
|
||||
"@microsoft/microsoft-graph-client": "3.0.7",
|
||||
"big-integer": "1.6.52",
|
||||
"bootstrap": "5.3.7",
|
||||
@@ -167,14 +168,14 @@
|
||||
"keytar": "7.9.0",
|
||||
"ldapts": "8.1.3",
|
||||
"lowdb": "1.0.0",
|
||||
"ngx-toastr": "20.0.4",
|
||||
"ngx-toastr": "19.1.0",
|
||||
"node-fetch": "2.7.0",
|
||||
"parse5": "8.0.0",
|
||||
"proper-lockfile": "4.1.2",
|
||||
"rxjs": "7.8.2",
|
||||
"tldjs": "2.3.1",
|
||||
"uuid": "11.1.0",
|
||||
"zone.js": "0.16.0"
|
||||
"zone.js": "0.15.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "~20",
|
||||
|
||||
54
src/abstractions/state-vNext.service.ts
Normal file
54
src/abstractions/state-vNext.service.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { StorageOptions } from "@/jslib/common/src/models/domain/storageOptions";
|
||||
|
||||
import { DirectoryType } from "@/src/enums/directoryType";
|
||||
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";
|
||||
|
||||
export abstract class StateServiceVNext {
|
||||
getDirectory: <IConfiguration>(type: DirectoryType) => Promise<IConfiguration>;
|
||||
setDirectory: (
|
||||
type: DirectoryType,
|
||||
config:
|
||||
| LdapConfiguration
|
||||
| GSuiteConfiguration
|
||||
| EntraIdConfiguration
|
||||
| OktaConfiguration
|
||||
| OneLoginConfiguration,
|
||||
) => Promise<any>;
|
||||
getLdapConfiguration: (options?: StorageOptions) => Promise<LdapConfiguration>;
|
||||
setLdapConfiguration: (value: LdapConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getGsuiteConfiguration: (options?: StorageOptions) => Promise<GSuiteConfiguration>;
|
||||
setGsuiteConfiguration: (value: GSuiteConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getEntraConfiguration: (options?: StorageOptions) => Promise<EntraIdConfiguration>;
|
||||
setEntraConfiguration: (value: EntraIdConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getOktaConfiguration: (options?: StorageOptions) => Promise<OktaConfiguration>;
|
||||
setOktaConfiguration: (value: OktaConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getOneLoginConfiguration: (options?: StorageOptions) => Promise<OneLoginConfiguration>;
|
||||
setOneLoginConfiguration: (
|
||||
value: OneLoginConfiguration,
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getOrganizationId: (options?: StorageOptions) => Promise<string>;
|
||||
setOrganizationId: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSync: (options?: StorageOptions) => Promise<SyncConfiguration>;
|
||||
setSync: (value: SyncConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getDirectoryType: (options?: StorageOptions) => Promise<DirectoryType>;
|
||||
setDirectoryType: (value: DirectoryType, options?: StorageOptions) => Promise<void>;
|
||||
getUserDelta: (options?: StorageOptions) => Promise<string>;
|
||||
setUserDelta: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getLastUserSync: (options?: StorageOptions) => Promise<Date>;
|
||||
setLastUserSync: (value: Date, options?: StorageOptions) => Promise<void>;
|
||||
getLastGroupSync: (options?: StorageOptions) => Promise<Date>;
|
||||
setLastGroupSync: (value: Date, options?: StorageOptions) => Promise<void>;
|
||||
getGroupDelta: (options?: StorageOptions) => Promise<string>;
|
||||
setGroupDelta: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getLastSyncHash: (options?: StorageOptions) => Promise<string>;
|
||||
setLastSyncHash: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSyncingDir: (options?: StorageOptions) => Promise<boolean>;
|
||||
setSyncingDir: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
clearSyncSettings: (syncHashToo: boolean) => Promise<void>;
|
||||
}
|
||||
@@ -31,12 +31,14 @@ import { DefaultDirectoryFactoryService } from "@/src/services/directory-factory
|
||||
import { SingleRequestBuilder } from "@/src/services/single-request-builder";
|
||||
|
||||
import { AuthService as AuthServiceAbstraction } from "../../abstractions/auth.service";
|
||||
import { StateServiceVNext } from "../../abstractions/state-vNext.service";
|
||||
import { StateService as StateServiceAbstraction } from "../../abstractions/state.service";
|
||||
import { Account } from "../../models/account";
|
||||
import { AuthService } from "../../services/auth.service";
|
||||
import { I18nService } from "../../services/i18n.service";
|
||||
import { StateService } from "../../services/state.service";
|
||||
import { StateMigrationService } from "../../services/stateMigration.service";
|
||||
import { StateServiceVNextImplementation } from "../../services/state-service/state-vNext.service";
|
||||
import { StateService } from "../../services/state-service/state.service";
|
||||
import { StateMigrationService } from "../../services/state-service/stateMigration.service";
|
||||
import { SyncService } from "../../services/sync.service";
|
||||
|
||||
import { AuthGuardService } from "./auth-guard.service";
|
||||
@@ -222,6 +224,29 @@ export function initFactory(
|
||||
StateMigrationServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
// Use new StateServiceVNext with flat key-value structure (new interface)
|
||||
safeProvider({
|
||||
provide: StateServiceVNext,
|
||||
useFactory: (
|
||||
storageService: StorageServiceAbstraction,
|
||||
secureStorageService: StorageServiceAbstraction,
|
||||
logService: LogServiceAbstraction,
|
||||
stateMigrationService: StateMigrationServiceAbstraction,
|
||||
) =>
|
||||
new StateServiceVNextImplementation(
|
||||
storageService,
|
||||
secureStorageService,
|
||||
logService,
|
||||
stateMigrationService,
|
||||
true,
|
||||
),
|
||||
deps: [
|
||||
StorageServiceAbstraction,
|
||||
SECURE_STORAGE,
|
||||
LogServiceAbstraction,
|
||||
StateMigrationServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SingleRequestBuilder,
|
||||
deps: [],
|
||||
@@ -233,7 +258,12 @@ export function initFactory(
|
||||
safeProvider({
|
||||
provide: DirectoryFactoryService,
|
||||
useClass: DefaultDirectoryFactoryService,
|
||||
deps: [LogServiceAbstraction, I18nServiceAbstraction, StateServiceAbstraction],
|
||||
deps: [
|
||||
LogServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
StateServiceVNext,
|
||||
],
|
||||
}),
|
||||
] satisfies SafeProvider[],
|
||||
})
|
||||
|
||||
16
src/bwdc.ts
16
src/bwdc.ts
@@ -18,6 +18,7 @@ import { NodeApiService } from "@/jslib/node/src/services/nodeApi.service";
|
||||
import { NodeCryptoFunctionService } from "@/jslib/node/src/services/nodeCryptoFunction.service";
|
||||
|
||||
import { DirectoryFactoryService } from "./abstractions/directory-factory.service";
|
||||
import { StateServiceVNext } from "./abstractions/state-vNext.service";
|
||||
import { Account } from "./models/account";
|
||||
import { Program } from "./program";
|
||||
import { AuthService } from "./services/auth.service";
|
||||
@@ -27,8 +28,9 @@ import { I18nService } from "./services/i18n.service";
|
||||
import { KeytarSecureStorageService } from "./services/keytarSecureStorage.service";
|
||||
import { LowdbStorageService } from "./services/lowdbStorage.service";
|
||||
import { SingleRequestBuilder } from "./services/single-request-builder";
|
||||
import { StateService } from "./services/state.service";
|
||||
import { StateMigrationService } from "./services/stateMigration.service";
|
||||
import { StateServiceVNextImplementation } from "./services/state-service/state-vNext.service";
|
||||
import { StateService } from "./services/state-service/state.service";
|
||||
import { StateMigrationService } from "./services/state-service/stateMigration.service";
|
||||
import { SyncService } from "./services/sync.service";
|
||||
|
||||
// eslint-disable-next-line
|
||||
@@ -53,6 +55,7 @@ export class Main {
|
||||
cryptoFunctionService: NodeCryptoFunctionService;
|
||||
authService: AuthService;
|
||||
syncService: SyncService;
|
||||
stateServiceVNext: StateServiceVNext;
|
||||
stateService: StateService;
|
||||
stateMigrationService: StateMigrationService;
|
||||
directoryFactoryService: DirectoryFactoryService;
|
||||
@@ -116,6 +119,14 @@ export class Main {
|
||||
process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS !== "true",
|
||||
new StateFactory(GlobalState, Account),
|
||||
);
|
||||
// Use new StateServiceVNext with flat key-value structure
|
||||
this.stateServiceVNext = new StateServiceVNextImplementation(
|
||||
this.storageService,
|
||||
this.secureStorageService,
|
||||
this.logService,
|
||||
this.stateMigrationService,
|
||||
process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS !== "true",
|
||||
);
|
||||
|
||||
this.cryptoService = new CryptoService(
|
||||
this.cryptoFunctionService,
|
||||
@@ -157,6 +168,7 @@ export class Main {
|
||||
this.logService,
|
||||
this.i18nService,
|
||||
this.stateService,
|
||||
this.stateServiceVNext,
|
||||
);
|
||||
|
||||
this.batchRequestBuilder = new BatchRequestBuilder();
|
||||
|
||||
13
src/main.ts
13
src/main.ts
@@ -11,12 +11,14 @@ import { TrayMain } from "@/jslib/electron/src/tray.main";
|
||||
import { UpdaterMain } from "@/jslib/electron/src/updater.main";
|
||||
import { WindowMain } from "@/jslib/electron/src/window.main";
|
||||
|
||||
import { StateServiceVNext } from "./abstractions/state-vNext.service";
|
||||
import { DCCredentialStorageListener } from "./main/credential-storage-listener";
|
||||
import { MenuMain } from "./main/menu.main";
|
||||
import { MessagingMain } from "./main/messaging.main";
|
||||
import { Account } from "./models/account";
|
||||
import { I18nService } from "./services/i18n.service";
|
||||
import { StateService } from "./services/state.service";
|
||||
import { StateServiceVNextImplementation } from "./services/state-service/state-vNext.service";
|
||||
import { StateService } from "./services/state-service/state.service";
|
||||
|
||||
export class Main {
|
||||
logService: ElectronLogService;
|
||||
@@ -24,6 +26,7 @@ export class Main {
|
||||
storageService: ElectronStorageService;
|
||||
messagingService: ElectronMainMessagingService;
|
||||
credentialStorageListener: DCCredentialStorageListener;
|
||||
stateServiceVNext: StateServiceVNext;
|
||||
stateService: StateService;
|
||||
|
||||
windowMain: WindowMain;
|
||||
@@ -66,6 +69,14 @@ export class Main {
|
||||
true,
|
||||
new StateFactory(GlobalState, Account),
|
||||
);
|
||||
// Use new StateServiceVNext with flat key-value structure
|
||||
this.stateServiceVNext = new StateServiceVNextImplementation(
|
||||
this.storageService,
|
||||
null,
|
||||
this.logService,
|
||||
null,
|
||||
true,
|
||||
);
|
||||
|
||||
this.windowMain = new WindowMain(
|
||||
this.stateService,
|
||||
|
||||
108
src/models/state.model.ts
Normal file
108
src/models/state.model.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
// ===================================================================
|
||||
// vNext Storage Keys (Flat key-value structure)
|
||||
// ===================================================================
|
||||
|
||||
export const StorageKeysVNext = {
|
||||
stateVersion: "stateVersion",
|
||||
directoryType: "directoryType",
|
||||
organizationId: "organizationId",
|
||||
directory_ldap: "directory_ldap",
|
||||
directory_gsuite: "directory_gsuite",
|
||||
directory_entra: "directory_entra",
|
||||
directory_okta: "directory_okta",
|
||||
directory_onelogin: "directory_onelogin",
|
||||
sync: "sync",
|
||||
syncingDir: "syncingDir",
|
||||
};
|
||||
|
||||
export const SecureStorageKeysVNext: { [key: string]: any } = {
|
||||
ldap: "secret_ldap",
|
||||
gsuite: "secret_gsuite",
|
||||
// Azure Active Directory was renamed to Entra ID, but we've kept the old property name
|
||||
// to be backwards compatible with existing configurations.
|
||||
azure: "secret_azure",
|
||||
entra: "secret_entra",
|
||||
okta: "secret_okta",
|
||||
oneLogin: "secret_oneLogin",
|
||||
userDelta: "userDeltaToken",
|
||||
groupDelta: "groupDeltaToken",
|
||||
lastUserSync: "lastUserSync",
|
||||
lastGroupSync: "lastGroupSync",
|
||||
lastSyncHash: "lastSyncHash",
|
||||
};
|
||||
|
||||
// ===================================================================
|
||||
// Legacy Storage Keys (Account-based hierarchy)
|
||||
// ===================================================================
|
||||
|
||||
export const SecureStorageKeysLegacy = {
|
||||
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",
|
||||
};
|
||||
|
||||
export const TempKeys = {
|
||||
tempAccountSettings: "tempAccountSettings",
|
||||
tempDirectoryConfigs: "tempDirectoryConfigs",
|
||||
tempDirectorySettings: "tempDirectorySettings",
|
||||
};
|
||||
|
||||
// ===================================================================
|
||||
// Migration Storage Keys
|
||||
// ===================================================================
|
||||
|
||||
export const SecureStorageKeysMigration: { [key: string]: any } = {
|
||||
ldap: "ldapPassword",
|
||||
gsuite: "gsuitePrivateKey",
|
||||
azure: "azureKey",
|
||||
entra: "entraIdKey",
|
||||
okta: "oktaToken",
|
||||
oneLogin: "oneLoginClientSecret",
|
||||
directoryConfigPrefix: "directoryConfig_",
|
||||
sync: "syncConfig",
|
||||
directoryType: "directoryType",
|
||||
organizationId: "organizationId",
|
||||
};
|
||||
|
||||
export const MigrationKeys: { [key: string]: any } = {
|
||||
entityId: "entityId",
|
||||
directoryType: "directoryType",
|
||||
organizationId: "organizationId",
|
||||
lastUserSync: "lastUserSync",
|
||||
lastGroupSync: "lastGroupSync",
|
||||
lastSyncHash: "lastSyncHash",
|
||||
syncingDir: "syncingDir",
|
||||
syncConfig: "syncConfig",
|
||||
userDelta: "userDeltaToken",
|
||||
groupDelta: "groupDeltaToken",
|
||||
tempDirectoryConfigs: "tempDirectoryConfigs",
|
||||
tempDirectorySettings: "tempDirectorySettings",
|
||||
};
|
||||
|
||||
export const MigrationStateKeys = {
|
||||
global: "global",
|
||||
authenticatedAccounts: "authenticatedAccounts",
|
||||
};
|
||||
|
||||
export const MigrationClientKeys: { [key: string]: any } = {
|
||||
clientIdOld: "clientId",
|
||||
clientId: "apikey_clientId",
|
||||
clientSecretOld: "clientSecret",
|
||||
clientSecret: "apikey_clientSecret",
|
||||
};
|
||||
|
||||
// ===================================================================
|
||||
// Shared Constants
|
||||
// ===================================================================
|
||||
|
||||
export const StoredSecurely = "[STORED SECURELY]";
|
||||
@@ -1,8 +1,7 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
||||
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
|
||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
||||
import {
|
||||
@@ -12,10 +11,11 @@ import {
|
||||
} from "@/jslib/common/src/models/domain/account";
|
||||
import { IdentityTokenResponse } from "@/jslib/common/src/models/response/identityTokenResponse";
|
||||
|
||||
import { MessagingService } from "../../jslib/common/src/abstractions/messaging.service";
|
||||
import { Account, DirectoryConfigurations, DirectorySettings } from "../models/account";
|
||||
|
||||
import { AuthService } from "./auth.service";
|
||||
import { StateService } from "./state.service";
|
||||
import { StateService } from "./state-service/state.service";
|
||||
|
||||
const clientId = "organization.CLIENT_ID";
|
||||
const clientSecret = "CLIENT_SECRET";
|
||||
@@ -35,22 +35,22 @@ export function identityTokenResponseFactory() {
|
||||
}
|
||||
|
||||
describe("AuthService", () => {
|
||||
let apiService: jest.Mocked<ApiService>;
|
||||
let appIdService: jest.Mocked<AppIdService>;
|
||||
let platformUtilsService: jest.Mocked<PlatformUtilsService>;
|
||||
let messagingService: jest.Mocked<MessagingService>;
|
||||
let stateService: jest.Mocked<StateService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let appIdService: SubstituteOf<AppIdService>;
|
||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
||||
let messagingService: SubstituteOf<MessagingService>;
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
|
||||
let authService: AuthService;
|
||||
|
||||
beforeEach(async () => {
|
||||
apiService = mock<ApiService>();
|
||||
appIdService = mock<AppIdService>();
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
stateService = mock<StateService>();
|
||||
messagingService = mock<MessagingService>();
|
||||
apiService = Substitute.for();
|
||||
appIdService = Substitute.for();
|
||||
platformUtilsService = Substitute.for();
|
||||
stateService = Substitute.for();
|
||||
messagingService = Substitute.for();
|
||||
|
||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||
appIdService.getAppId().resolves(deviceId);
|
||||
|
||||
authService = new AuthService(
|
||||
apiService,
|
||||
@@ -62,12 +62,11 @@ describe("AuthService", () => {
|
||||
});
|
||||
|
||||
it("sets the local environment after a successful login", async () => {
|
||||
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
|
||||
await authService.logIn({ clientId, clientSecret });
|
||||
|
||||
expect(stateService.addAccount).toHaveBeenCalledTimes(1);
|
||||
expect(stateService.addAccount).toHaveBeenCalledWith(
|
||||
stateService.received(1).addAccount(
|
||||
new Account({
|
||||
profile: {
|
||||
...new AccountProfile(),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
|
||||
import { DirectoryFactoryService } from "../abstractions/directory-factory.service";
|
||||
import { StateServiceVNext } from "../abstractions/state-vNext.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { DirectoryType } from "../enums/directoryType";
|
||||
|
||||
@@ -16,12 +17,18 @@ export class DefaultDirectoryFactoryService implements DirectoryFactoryService {
|
||||
private logService: LogService,
|
||||
private i18nService: I18nService,
|
||||
private stateService: StateService,
|
||||
private stateServiceVNext: StateServiceVNext,
|
||||
) {}
|
||||
|
||||
createService(directoryType: DirectoryType) {
|
||||
switch (directoryType) {
|
||||
case DirectoryType.GSuite:
|
||||
return new GSuiteDirectoryService(this.logService, this.i18nService, this.stateService);
|
||||
return new GSuiteDirectoryService(
|
||||
this.logService,
|
||||
this.i18nService,
|
||||
this.stateService,
|
||||
this.stateServiceVNext,
|
||||
);
|
||||
case DirectoryType.EntraID:
|
||||
return new EntraIdDirectoryService(this.logService, this.i18nService, this.stateService);
|
||||
case DirectoryType.Ldap:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { config as dotenvConfig } from "dotenv";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { StateServiceVNext } from "@/src/abstractions/state-vNext.service";
|
||||
|
||||
import { I18nService } from "../../../jslib/common/src/abstractions/i18n.service";
|
||||
import { LogService } from "../../../jslib/common/src/abstractions/log.service";
|
||||
import {
|
||||
@@ -10,7 +12,7 @@ import {
|
||||
import { groupFixtures } from "../../../utils/google-workspace/group-fixtures";
|
||||
import { userFixtures } from "../../../utils/google-workspace/user-fixtures";
|
||||
import { DirectoryType } from "../../enums/directoryType";
|
||||
import { StateService } from "../state.service";
|
||||
import { StateService } from "../state-service/state.service";
|
||||
|
||||
import { GSuiteDirectoryService } from "./gsuite-directory.service";
|
||||
|
||||
@@ -35,6 +37,7 @@ describe("gsuiteDirectoryService", () => {
|
||||
let logService: MockProxy<LogService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let stateServiceVNext: MockProxy<StateServiceVNext>;
|
||||
|
||||
let directoryService: GSuiteDirectoryService;
|
||||
|
||||
@@ -42,23 +45,31 @@ describe("gsuiteDirectoryService", () => {
|
||||
logService = mock();
|
||||
i18nService = mock();
|
||||
stateService = mock();
|
||||
stateServiceVNext = mock();
|
||||
|
||||
stateService.getDirectoryType.mockResolvedValue(DirectoryType.GSuite);
|
||||
stateServiceVNext.getDirectoryType.mockResolvedValue(DirectoryType.GSuite);
|
||||
stateService.getLastUserSync.mockResolvedValue(null); // do not filter results by last modified date
|
||||
i18nService.t.mockImplementation((id) => id); // passthrough implementation for any error messages
|
||||
|
||||
directoryService = new GSuiteDirectoryService(logService, i18nService, stateService);
|
||||
directoryService = new GSuiteDirectoryService(
|
||||
logService,
|
||||
i18nService,
|
||||
stateService,
|
||||
stateServiceVNext,
|
||||
);
|
||||
});
|
||||
|
||||
it("syncs without using filters (includes test data)", async () => {
|
||||
const directoryConfig = getGSuiteConfiguration();
|
||||
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig);
|
||||
stateServiceVNext.getDirectory
|
||||
.calledWith(DirectoryType.GSuite)
|
||||
.mockResolvedValue(directoryConfig);
|
||||
|
||||
const syncConfig = getSyncConfiguration({
|
||||
groups: true,
|
||||
users: true,
|
||||
});
|
||||
stateService.getSync.mockResolvedValue(syncConfig);
|
||||
stateServiceVNext.getSync.mockResolvedValue(syncConfig);
|
||||
|
||||
const result = await directoryService.getEntries(true, true);
|
||||
|
||||
@@ -68,7 +79,9 @@ describe("gsuiteDirectoryService", () => {
|
||||
|
||||
it("syncs using user and group filters (exact match for test data)", async () => {
|
||||
const directoryConfig = getGSuiteConfiguration();
|
||||
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig);
|
||||
stateServiceVNext.getDirectory
|
||||
.calledWith(DirectoryType.GSuite)
|
||||
.mockResolvedValue(directoryConfig);
|
||||
|
||||
const syncConfig = getSyncConfiguration({
|
||||
groups: true,
|
||||
@@ -76,7 +89,7 @@ describe("gsuiteDirectoryService", () => {
|
||||
userFilter: INTEGRATION_USER_FILTER,
|
||||
groupFilter: INTEGRATION_GROUP_FILTER,
|
||||
});
|
||||
stateService.getSync.mockResolvedValue(syncConfig);
|
||||
stateServiceVNext.getSync.mockResolvedValue(syncConfig);
|
||||
|
||||
const result = await directoryService.getEntries(true, true);
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import { admin_directory_v1, google } from "googleapis";
|
||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
|
||||
import { StateServiceVNext } from "@/src/abstractions/state-vNext.service";
|
||||
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
import { DirectoryType } from "../../enums/directoryType";
|
||||
import { GroupEntry } from "../../models/groupEntry";
|
||||
@@ -25,25 +27,26 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
|
||||
private logService: LogService,
|
||||
private i18nService: I18nService,
|
||||
private stateService: StateService,
|
||||
private stateServiceVNext: StateServiceVNext,
|
||||
) {
|
||||
super();
|
||||
this.service = google.admin("directory_v1");
|
||||
}
|
||||
|
||||
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
||||
const type = await this.stateService.getDirectoryType();
|
||||
const type = await this.stateServiceVNext.getDirectoryType();
|
||||
if (type !== DirectoryType.GSuite) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dirConfig = await this.stateService.getDirectory<GSuiteConfiguration>(
|
||||
this.dirConfig = await this.stateServiceVNext.getDirectory<GSuiteConfiguration>(
|
||||
DirectoryType.GSuite,
|
||||
);
|
||||
if (this.dirConfig == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.syncConfig = await this.stateService.getSync();
|
||||
this.syncConfig = await this.stateServiceVNext.getSync();
|
||||
if (this.syncConfig == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { groupFixtures } from "../../../utils/openldap/group-fixtures";
|
||||
import { userFixtures } from "../../../utils/openldap/user-fixtures";
|
||||
import { DirectoryType } from "../../enums/directoryType";
|
||||
import { StateService } from "../state.service";
|
||||
import { StateService } from "../state-service/state.service";
|
||||
|
||||
import { LdapDirectoryService } from "./ldap-directory.service";
|
||||
|
||||
|
||||
488
src/services/state-service/state-vNext.service.spec.ts
Normal file
488
src/services/state-service/state-vNext.service.spec.ts
Normal file
@@ -0,0 +1,488 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
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 { DirectoryType } from "@/src/enums/directoryType";
|
||||
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 { StorageKeysVNext as StorageKeys, StoredSecurely } from "@/src/models/state.model";
|
||||
import { SyncConfiguration } from "@/src/models/syncConfiguration";
|
||||
|
||||
import { StateServiceVNextImplementation } from "./state-vNext.service";
|
||||
|
||||
describe("StateServiceVNextImplementation", () => {
|
||||
let storageService: MockProxy<StorageService>;
|
||||
let secureStorageService: MockProxy<StorageService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let stateMigrationService: MockProxy<StateMigrationService>;
|
||||
let stateService: StateServiceVNextImplementation;
|
||||
|
||||
beforeEach(() => {
|
||||
storageService = mock<StorageService>();
|
||||
secureStorageService = mock<StorageService>();
|
||||
logService = mock<LogService>();
|
||||
stateMigrationService = mock<StateMigrationService>();
|
||||
|
||||
stateService = new StateServiceVNextImplementation(
|
||||
storageService,
|
||||
secureStorageService,
|
||||
logService,
|
||||
stateMigrationService,
|
||||
true, // useSecureStorageForSecrets
|
||||
);
|
||||
});
|
||||
|
||||
describe("init", () => {
|
||||
it("should run migration if needed", async () => {
|
||||
stateMigrationService.needsMigration.mockResolvedValue(true);
|
||||
|
||||
await stateService.init();
|
||||
|
||||
expect(stateMigrationService.needsMigration).toHaveBeenCalled();
|
||||
expect(stateMigrationService.migrate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not run migration if not needed", async () => {
|
||||
stateMigrationService.needsMigration.mockResolvedValue(false);
|
||||
|
||||
await stateService.init();
|
||||
|
||||
expect(stateMigrationService.needsMigration).toHaveBeenCalled();
|
||||
expect(stateMigrationService.migrate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("clean", () => {
|
||||
it("should clear all directory settings and configurations", async () => {
|
||||
await stateService.clean();
|
||||
|
||||
// Verify all directory types are cleared
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.directoryType, null);
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.organizationId, null);
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.sync, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Directory Type", () => {
|
||||
it("should store and retrieve directory type", async () => {
|
||||
storageService.get.mockResolvedValue(DirectoryType.Ldap);
|
||||
|
||||
await stateService.setDirectoryType(DirectoryType.Ldap);
|
||||
const result = await stateService.getDirectoryType();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalledWith(
|
||||
StorageKeys.directoryType,
|
||||
DirectoryType.Ldap,
|
||||
);
|
||||
expect(result).toBe(DirectoryType.Ldap);
|
||||
});
|
||||
|
||||
it("should return null when directory type is not set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getDirectoryType();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Organization Id", () => {
|
||||
it("should store and retrieve organization ID", async () => {
|
||||
const orgId = "test-org-123";
|
||||
|
||||
storageService.get.mockResolvedValue(orgId);
|
||||
|
||||
await stateService.setOrganizationId(orgId);
|
||||
const result = await stateService.getOrganizationId();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.organizationId, orgId);
|
||||
expect(result).toBe(orgId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("LDAP Configuration", () => {
|
||||
it("should store and retrieve LDAP configuration with secrets in secure storage", async () => {
|
||||
const config: LdapConfiguration = {
|
||||
ssl: true,
|
||||
startTls: false,
|
||||
tlsCaPath: null,
|
||||
sslAllowUnauthorized: false,
|
||||
sslCertPath: null,
|
||||
sslKeyPath: null,
|
||||
sslCaPath: null,
|
||||
hostname: "ldap.example.com",
|
||||
port: 636,
|
||||
domain: null,
|
||||
rootPath: null,
|
||||
ad: true,
|
||||
username: "admin",
|
||||
password: "secret-password",
|
||||
currentUser: false,
|
||||
pagedSearch: true,
|
||||
};
|
||||
|
||||
secureStorageService.get.mockResolvedValue("secret-password");
|
||||
storageService.get.mockResolvedValue({
|
||||
...config,
|
||||
password: StoredSecurely,
|
||||
});
|
||||
|
||||
await stateService.setDirectory(DirectoryType.Ldap, config);
|
||||
const result = await stateService.getDirectory<LdapConfiguration>(DirectoryType.Ldap);
|
||||
|
||||
// Verify password is stored in secure storage
|
||||
expect(secureStorageService.save).toHaveBeenCalled();
|
||||
|
||||
// Verify configuration is stored
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
|
||||
// Verify retrieved config has real password from secure storage
|
||||
expect(result?.password).toBe("secret-password");
|
||||
});
|
||||
|
||||
it("should return null when LDAP configuration is not set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getLdapConfiguration();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle null password in LDAP configuration", async () => {
|
||||
const config: LdapConfiguration = {
|
||||
ssl: true,
|
||||
startTls: false,
|
||||
tlsCaPath: null,
|
||||
sslAllowUnauthorized: false,
|
||||
sslCertPath: null,
|
||||
sslKeyPath: null,
|
||||
sslCaPath: null,
|
||||
hostname: "ldap.example.com",
|
||||
port: 636,
|
||||
domain: null,
|
||||
rootPath: null,
|
||||
ad: true,
|
||||
username: "admin",
|
||||
password: null,
|
||||
currentUser: false,
|
||||
pagedSearch: true,
|
||||
};
|
||||
|
||||
await stateService.setDirectory(DirectoryType.Ldap, config);
|
||||
|
||||
// Null passwords should call remove on the secure storage secret key
|
||||
expect(secureStorageService.remove).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GSuite Configuration", () => {
|
||||
it("should store and retrieve GSuite configuration with privateKey in secure storage", async () => {
|
||||
const config: GSuiteConfiguration = {
|
||||
domain: "example.com",
|
||||
clientEmail: "service@example.com",
|
||||
adminUser: "admin@example.com",
|
||||
privateKey: "private-key-content",
|
||||
customer: "customer-id",
|
||||
};
|
||||
|
||||
secureStorageService.get.mockResolvedValue("private-key-content");
|
||||
storageService.get.mockResolvedValue({
|
||||
...config,
|
||||
privateKey: StoredSecurely,
|
||||
});
|
||||
|
||||
await stateService.setDirectory(DirectoryType.GSuite, config);
|
||||
const result = await stateService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite);
|
||||
|
||||
expect(secureStorageService.save).toHaveBeenCalled();
|
||||
expect(result?.privateKey).toBe("private-key-content");
|
||||
});
|
||||
|
||||
it("should handle null privateKey in GSuite configuration", async () => {
|
||||
const config: GSuiteConfiguration = {
|
||||
domain: "example.com",
|
||||
clientEmail: "service@example.com",
|
||||
adminUser: "admin@example.com",
|
||||
privateKey: null,
|
||||
customer: "customer-id",
|
||||
};
|
||||
|
||||
await stateService.setDirectory(DirectoryType.GSuite, config);
|
||||
|
||||
// Null privateKey should call remove on the secure storage secret key
|
||||
expect(secureStorageService.remove).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Entra ID Configuration", () => {
|
||||
it("should store and retrieve Entra ID configuration with key in secure storage", async () => {
|
||||
const config: EntraIdConfiguration = {
|
||||
identityAuthority: "https://login.microsoftonline.com",
|
||||
tenant: "tenant-id",
|
||||
applicationId: "app-id",
|
||||
key: "secret-key",
|
||||
};
|
||||
|
||||
secureStorageService.get.mockResolvedValue("secret-key");
|
||||
storageService.get.mockResolvedValue({
|
||||
...config,
|
||||
key: StoredSecurely,
|
||||
});
|
||||
|
||||
await stateService.setDirectory(DirectoryType.EntraID, config);
|
||||
const result = await stateService.getDirectory<EntraIdConfiguration>(DirectoryType.EntraID);
|
||||
|
||||
expect(secureStorageService.save).toHaveBeenCalled();
|
||||
expect(result?.key).toBe("secret-key");
|
||||
});
|
||||
|
||||
it("should maintain backwards compatibility with Azure key storage", async () => {
|
||||
const config: EntraIdConfiguration = {
|
||||
identityAuthority: "https://login.microsoftonline.com",
|
||||
tenant: "tenant-id",
|
||||
applicationId: "app-id",
|
||||
key: StoredSecurely,
|
||||
};
|
||||
|
||||
storageService.get.mockResolvedValue(config);
|
||||
secureStorageService.get.mockResolvedValueOnce(null); // entra key not found
|
||||
secureStorageService.get.mockResolvedValueOnce("azure-secret-key"); // fallback to azure key
|
||||
|
||||
const result = await stateService.getDirectory<EntraIdConfiguration>(DirectoryType.EntraID);
|
||||
|
||||
expect(secureStorageService.get).toHaveBeenCalled();
|
||||
expect(result?.key).toBe("azure-secret-key");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Okta Configuration", () => {
|
||||
it("should store and retrieve Okta configuration with token in secure storage", async () => {
|
||||
const config: OktaConfiguration = {
|
||||
orgUrl: "https://example.okta.com",
|
||||
token: "okta-token",
|
||||
};
|
||||
|
||||
secureStorageService.get.mockResolvedValue("okta-token");
|
||||
storageService.get.mockResolvedValue({
|
||||
...config,
|
||||
token: StoredSecurely,
|
||||
});
|
||||
|
||||
await stateService.setDirectory(DirectoryType.Okta, config);
|
||||
const result = await stateService.getDirectory<OktaConfiguration>(DirectoryType.Okta);
|
||||
|
||||
expect(secureStorageService.save).toHaveBeenCalled();
|
||||
expect(result?.token).toBe("okta-token");
|
||||
});
|
||||
});
|
||||
|
||||
describe("OneLogin Configuration", () => {
|
||||
it("should store and retrieve OneLogin configuration with clientSecret in secure storage", async () => {
|
||||
const config: OneLoginConfiguration = {
|
||||
region: "us",
|
||||
clientId: "client-id",
|
||||
clientSecret: "client-secret",
|
||||
};
|
||||
|
||||
secureStorageService.get.mockResolvedValue("client-secret");
|
||||
storageService.get.mockResolvedValue({
|
||||
...config,
|
||||
clientSecret: StoredSecurely,
|
||||
});
|
||||
|
||||
await stateService.setDirectory(DirectoryType.OneLogin, config);
|
||||
const result = await stateService.getDirectory<OneLoginConfiguration>(DirectoryType.OneLogin);
|
||||
|
||||
expect(secureStorageService.save).toHaveBeenCalled();
|
||||
expect(result?.clientSecret).toBe("client-secret");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sync Configuration", () => {
|
||||
it("should store and retrieve sync configuration", async () => {
|
||||
const syncConfig: SyncConfiguration = {
|
||||
users: true,
|
||||
groups: true,
|
||||
interval: 5,
|
||||
userFilter: null,
|
||||
groupFilter: null,
|
||||
removeDisabled: true,
|
||||
overwriteExisting: false,
|
||||
largeImport: false,
|
||||
groupObjectClass: null,
|
||||
userObjectClass: null,
|
||||
groupPath: null,
|
||||
userPath: null,
|
||||
groupNameAttribute: null,
|
||||
userEmailAttribute: null,
|
||||
memberAttribute: "member",
|
||||
creationDateAttribute: "whenCreated",
|
||||
revisionDateAttribute: "whenChanged",
|
||||
useEmailPrefixSuffix: false,
|
||||
emailPrefixAttribute: null,
|
||||
emailSuffix: null,
|
||||
};
|
||||
|
||||
storageService.get.mockResolvedValue(syncConfig);
|
||||
|
||||
await stateService.setSync(syncConfig);
|
||||
const result = await stateService.getSync();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.sync, syncConfig);
|
||||
expect(result).toEqual(syncConfig);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sync Settings", () => {
|
||||
it("should clear sync settings when clearSyncSettings is called", async () => {
|
||||
await stateService.clearSyncSettings(false);
|
||||
|
||||
// Should set delta and sync values to null
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should clear lastSyncHash when hashToo is true", async () => {
|
||||
await stateService.clearSyncSettings(true);
|
||||
|
||||
// Should set all values including lastSyncHash to null
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not clear lastSyncHash when hashToo is false", async () => {
|
||||
await stateService.clearSyncSettings(false);
|
||||
|
||||
// Should set delta and sync values but not lastSyncHash
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Last Sync Hash", () => {
|
||||
it("should store and retrieve last sync hash", async () => {
|
||||
const hash = "hash";
|
||||
|
||||
storageService.get.mockResolvedValue(hash);
|
||||
|
||||
await stateService.setLastSyncHash(hash);
|
||||
const result = await stateService.getLastSyncHash();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
expect(result).toBe(hash);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Delta Tokens", () => {
|
||||
it("should store and retrieve user delta token", async () => {
|
||||
const token = "user-delta-token";
|
||||
|
||||
storageService.get.mockResolvedValue(token);
|
||||
|
||||
await stateService.setUserDelta(token);
|
||||
const result = await stateService.getUserDelta();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
expect(result).toBe(token);
|
||||
});
|
||||
|
||||
it("should store and retrieve group delta token", async () => {
|
||||
const token = "group-delta-token";
|
||||
|
||||
storageService.get.mockResolvedValue(token);
|
||||
|
||||
await stateService.setGroupDelta(token);
|
||||
const result = await stateService.getGroupDelta();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
expect(result).toBe(token);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Last Sync Timestamps", () => {
|
||||
it("should store and retrieve last user sync timestamp", async () => {
|
||||
const timestamp = new Date("2024-01-01T00:00:00Z");
|
||||
|
||||
storageService.get.mockResolvedValue(timestamp.toISOString());
|
||||
|
||||
await stateService.setLastUserSync(timestamp);
|
||||
const result = await stateService.getLastUserSync();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
expect(result?.toISOString()).toBe(timestamp.toISOString());
|
||||
});
|
||||
|
||||
it("should store and retrieve last group sync timestamp", async () => {
|
||||
const timestamp = new Date("2024-01-01T00:00:00Z");
|
||||
|
||||
storageService.get.mockResolvedValue(timestamp.toISOString());
|
||||
|
||||
await stateService.setLastGroupSync(timestamp);
|
||||
const result = await stateService.getLastGroupSync();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
expect(result?.toISOString()).toBe(timestamp.toISOString());
|
||||
});
|
||||
|
||||
it("should return null when last user sync timestamp is not set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getLastUserSync();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null when last group sync timestamp is not set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getLastGroupSync();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Secure Storage Flag", () => {
|
||||
it("should not separate secrets when useSecureStorageForSecrets is false", async () => {
|
||||
const insecureStateService = new StateServiceVNextImplementation(
|
||||
storageService,
|
||||
secureStorageService,
|
||||
logService,
|
||||
stateMigrationService,
|
||||
false, // useSecureStorageForSecrets = false
|
||||
);
|
||||
|
||||
const config: LdapConfiguration = {
|
||||
ssl: true,
|
||||
startTls: false,
|
||||
tlsCaPath: null,
|
||||
sslAllowUnauthorized: false,
|
||||
sslCertPath: null,
|
||||
sslKeyPath: null,
|
||||
sslCaPath: null,
|
||||
hostname: "ldap.example.com",
|
||||
port: 636,
|
||||
domain: null,
|
||||
rootPath: null,
|
||||
ad: true,
|
||||
username: "admin",
|
||||
password: "secret-password",
|
||||
currentUser: false,
|
||||
pagedSearch: true,
|
||||
};
|
||||
|
||||
storageService.get.mockResolvedValue(config);
|
||||
|
||||
// When useSecureStorageForSecrets is false, setDirectory doesn't process secrets
|
||||
await insecureStateService.setDirectory(DirectoryType.Ldap, config);
|
||||
|
||||
// Retrieve config - should return password as-is from storage (not from secure storage)
|
||||
const result = await insecureStateService.getDirectory<LdapConfiguration>(DirectoryType.Ldap);
|
||||
|
||||
// Password should be retrieved directly from storage, not secure storage
|
||||
expect(result?.password).toBe("secret-password");
|
||||
expect(secureStorageService.get).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
409
src/services/state-service/state-vNext.service.ts
Normal file
409
src/services/state-service/state-vNext.service.ts
Normal file
@@ -0,0 +1,409 @@
|
||||
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 { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls";
|
||||
import { StorageOptions } from "@/jslib/common/src/models/domain/storageOptions";
|
||||
|
||||
import { StateServiceVNext as StateServiceVNextAbstraction } from "@/src/abstractions/state-vNext.service";
|
||||
import { DirectoryType } from "@/src/enums/directoryType";
|
||||
import { IConfiguration } from "@/src/models/IConfiguration";
|
||||
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 {
|
||||
SecureStorageKeysVNext as SecureStorageKeys,
|
||||
StorageKeysVNext as StorageKeys,
|
||||
StoredSecurely,
|
||||
} from "@/src/models/state.model";
|
||||
import { SyncConfiguration } from "@/src/models/syncConfiguration";
|
||||
|
||||
export class StateServiceVNextImplementation implements StateServiceVNextAbstraction {
|
||||
constructor(
|
||||
protected storageService: StorageService,
|
||||
protected secureStorageService: StorageService,
|
||||
protected logService: LogService,
|
||||
protected stateMigrationService: StateMigrationService,
|
||||
private useSecureStorageForSecrets = true,
|
||||
) {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
if (await this.stateMigrationService.needsMigration()) {
|
||||
await this.stateMigrationService.migrate();
|
||||
}
|
||||
}
|
||||
|
||||
async clean(options?: StorageOptions): Promise<void> {
|
||||
// Clear all directory settings and configurations
|
||||
// but preserve version and environment settings
|
||||
await this.setDirectoryType(null);
|
||||
await this.setOrganizationId(null);
|
||||
await this.setSync(null);
|
||||
await this.setLdapConfiguration(null);
|
||||
await this.setGsuiteConfiguration(null);
|
||||
await this.setEntraConfiguration(null);
|
||||
await this.setOktaConfiguration(null);
|
||||
await this.setOneLoginConfiguration(null);
|
||||
await this.clearSyncSettings(true);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Directory Configuration Methods
|
||||
// ===================================================================
|
||||
|
||||
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) {
|
||||
// Create a copy to avoid modifying the cached config
|
||||
const configWithSecrets = Object.assign({}, config);
|
||||
|
||||
switch (type) {
|
||||
case DirectoryType.Ldap:
|
||||
(configWithSecrets as any).password = await this.getLdapSecret();
|
||||
break;
|
||||
case DirectoryType.EntraID:
|
||||
(configWithSecrets as any).key = await this.getEntraSecret();
|
||||
break;
|
||||
case DirectoryType.Okta:
|
||||
(configWithSecrets as any).token = await this.getOktaSecret();
|
||||
break;
|
||||
case DirectoryType.GSuite:
|
||||
(configWithSecrets as any).privateKey = await this.getGsuiteSecret();
|
||||
break;
|
||||
case DirectoryType.OneLogin:
|
||||
(configWithSecrets as any).clientSecret = await this.getOneLoginSecret();
|
||||
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.setLdapSecret(ldapConfig.password);
|
||||
ldapConfig.password = StoredSecurely;
|
||||
await this.setLdapConfiguration(ldapConfig);
|
||||
break;
|
||||
}
|
||||
case DirectoryType.EntraID: {
|
||||
const entraConfig = config as EntraIdConfiguration;
|
||||
await this.setEntraSecret(entraConfig.key);
|
||||
entraConfig.key = StoredSecurely;
|
||||
await this.setEntraConfiguration(entraConfig);
|
||||
break;
|
||||
}
|
||||
case DirectoryType.Okta: {
|
||||
const oktaConfig = config as OktaConfiguration;
|
||||
await this.setOktaSecret(oktaConfig.token);
|
||||
oktaConfig.token = StoredSecurely;
|
||||
await this.setOktaConfiguration(oktaConfig);
|
||||
break;
|
||||
}
|
||||
case DirectoryType.GSuite: {
|
||||
const gsuiteConfig = config as GSuiteConfiguration;
|
||||
if (gsuiteConfig.privateKey == null) {
|
||||
await this.setGsuiteSecret(null);
|
||||
} else {
|
||||
const normalizedPrivateKey = gsuiteConfig.privateKey.replace(/\\n/g, "\n");
|
||||
await this.setGsuiteSecret(normalizedPrivateKey);
|
||||
gsuiteConfig.privateKey = StoredSecurely;
|
||||
}
|
||||
await this.setGsuiteConfiguration(gsuiteConfig);
|
||||
break;
|
||||
}
|
||||
case DirectoryType.OneLogin: {
|
||||
const oneLoginConfig = config as OneLoginConfiguration;
|
||||
await this.setOneLoginSecret(oneLoginConfig.clientSecret);
|
||||
oneLoginConfig.clientSecret = StoredSecurely;
|
||||
await this.setOneLoginConfiguration(oneLoginConfig);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Secret Storage Methods (Secure Storage)
|
||||
// ===================================================================
|
||||
|
||||
private async getLdapSecret(): Promise<string> {
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.ldap);
|
||||
}
|
||||
|
||||
private async setLdapSecret(value: string): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove(SecureStorageKeys.ldap);
|
||||
} else {
|
||||
await this.secureStorageService.save(SecureStorageKeys.ldap, value);
|
||||
}
|
||||
}
|
||||
|
||||
private async getGsuiteSecret(): Promise<string> {
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.gsuite);
|
||||
}
|
||||
|
||||
private async setGsuiteSecret(value: string): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove(SecureStorageKeys.gsuite);
|
||||
} else {
|
||||
await this.secureStorageService.save(SecureStorageKeys.gsuite, value);
|
||||
}
|
||||
}
|
||||
|
||||
private async getEntraSecret(): Promise<string> {
|
||||
// Try new key first, fall back to old azure key for backwards compatibility
|
||||
const entraKey = await this.secureStorageService.get<string>(SecureStorageKeys.entra);
|
||||
if (entraKey != null) {
|
||||
return entraKey;
|
||||
}
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.azure);
|
||||
}
|
||||
|
||||
private async setEntraSecret(value: string): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove(SecureStorageKeys.entra);
|
||||
await this.secureStorageService.remove(SecureStorageKeys.azure);
|
||||
} else {
|
||||
await this.secureStorageService.save(SecureStorageKeys.entra, value);
|
||||
}
|
||||
}
|
||||
|
||||
private async getOktaSecret(): Promise<string> {
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.okta);
|
||||
}
|
||||
|
||||
private async setOktaSecret(value: string): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove(SecureStorageKeys.okta);
|
||||
} else {
|
||||
await this.secureStorageService.save(SecureStorageKeys.okta, value);
|
||||
}
|
||||
}
|
||||
|
||||
private async getOneLoginSecret(): Promise<string> {
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.oneLogin);
|
||||
}
|
||||
|
||||
private async setOneLoginSecret(value: string): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove(SecureStorageKeys.oneLogin);
|
||||
} else {
|
||||
await this.secureStorageService.save(SecureStorageKeys.oneLogin, value);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Directory-Specific Configuration Methods
|
||||
// ===================================================================
|
||||
|
||||
async getLdapConfiguration(options?: StorageOptions): Promise<LdapConfiguration> {
|
||||
return await this.storageService.get<LdapConfiguration>(StorageKeys.directory_ldap);
|
||||
}
|
||||
|
||||
async setLdapConfiguration(value: LdapConfiguration, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.directory_ldap, value);
|
||||
}
|
||||
|
||||
async getGsuiteConfiguration(options?: StorageOptions): Promise<GSuiteConfiguration> {
|
||||
return await this.storageService.get<GSuiteConfiguration>(StorageKeys.directory_gsuite);
|
||||
}
|
||||
|
||||
async setGsuiteConfiguration(
|
||||
value: GSuiteConfiguration,
|
||||
options?: StorageOptions,
|
||||
): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.directory_gsuite, value);
|
||||
}
|
||||
|
||||
async getEntraConfiguration(options?: StorageOptions): Promise<EntraIdConfiguration> {
|
||||
return await this.storageService.get<EntraIdConfiguration>(StorageKeys.directory_entra);
|
||||
}
|
||||
|
||||
async setEntraConfiguration(
|
||||
value: EntraIdConfiguration,
|
||||
options?: StorageOptions,
|
||||
): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.directory_entra, value);
|
||||
}
|
||||
|
||||
async getOktaConfiguration(options?: StorageOptions): Promise<OktaConfiguration> {
|
||||
return await this.storageService.get<OktaConfiguration>(StorageKeys.directory_okta);
|
||||
}
|
||||
|
||||
async setOktaConfiguration(value: OktaConfiguration, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.directory_okta, value);
|
||||
}
|
||||
|
||||
async getOneLoginConfiguration(options?: StorageOptions): Promise<OneLoginConfiguration> {
|
||||
return await this.storageService.get<OneLoginConfiguration>(StorageKeys.directory_onelogin);
|
||||
}
|
||||
|
||||
async setOneLoginConfiguration(
|
||||
value: OneLoginConfiguration,
|
||||
options?: StorageOptions,
|
||||
): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.directory_onelogin, value);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Directory Settings Methods
|
||||
// ===================================================================
|
||||
|
||||
async getOrganizationId(options?: StorageOptions): Promise<string> {
|
||||
return await this.storageService.get<string>(StorageKeys.organizationId);
|
||||
}
|
||||
|
||||
async setOrganizationId(value: string, options?: StorageOptions): Promise<void> {
|
||||
const currentId = await this.getOrganizationId();
|
||||
if (currentId !== value) {
|
||||
await this.clearSyncSettings();
|
||||
}
|
||||
await this.storageService.save(StorageKeys.organizationId, value);
|
||||
}
|
||||
|
||||
async getSync(options?: StorageOptions): Promise<SyncConfiguration> {
|
||||
return await this.storageService.get<SyncConfiguration>(StorageKeys.sync);
|
||||
}
|
||||
|
||||
async setSync(value: SyncConfiguration, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.sync, value);
|
||||
}
|
||||
|
||||
async getDirectoryType(options?: StorageOptions): Promise<DirectoryType> {
|
||||
return await this.storageService.get<DirectoryType>(StorageKeys.directoryType);
|
||||
}
|
||||
|
||||
async setDirectoryType(value: DirectoryType, options?: StorageOptions): Promise<void> {
|
||||
const currentType = await this.getDirectoryType();
|
||||
if (value !== currentType) {
|
||||
await this.clearSyncSettings();
|
||||
}
|
||||
await this.storageService.save(StorageKeys.directoryType, value);
|
||||
}
|
||||
|
||||
async getLastUserSync(options?: StorageOptions): Promise<Date> {
|
||||
const dateString = await this.storageService.get<string>(SecureStorageKeys.lastUserSync);
|
||||
return dateString ? new Date(dateString) : null;
|
||||
}
|
||||
|
||||
async setLastUserSync(value: Date, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(SecureStorageKeys.lastUserSync, value);
|
||||
}
|
||||
|
||||
async getLastGroupSync(options?: StorageOptions): Promise<Date> {
|
||||
const dateString = await this.storageService.get<string>(SecureStorageKeys.lastGroupSync);
|
||||
return dateString ? new Date(dateString) : null;
|
||||
}
|
||||
|
||||
async setLastGroupSync(value: Date, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(SecureStorageKeys.lastGroupSync, value);
|
||||
}
|
||||
|
||||
async getLastSyncHash(options?: StorageOptions): Promise<string> {
|
||||
return await this.storageService.get<string>(SecureStorageKeys.lastSyncHash);
|
||||
}
|
||||
|
||||
async setLastSyncHash(value: string, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(SecureStorageKeys.lastSyncHash, value);
|
||||
}
|
||||
|
||||
async getSyncingDir(options?: StorageOptions): Promise<boolean> {
|
||||
return await this.storageService.get<boolean>(StorageKeys.syncingDir);
|
||||
}
|
||||
|
||||
async setSyncingDir(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.syncingDir, value);
|
||||
}
|
||||
|
||||
async getUserDelta(options?: StorageOptions): Promise<string> {
|
||||
return await this.storageService.get<string>(SecureStorageKeys.userDelta);
|
||||
}
|
||||
|
||||
async setUserDelta(value: string, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(SecureStorageKeys.userDelta, value);
|
||||
}
|
||||
|
||||
async getGroupDelta(options?: StorageOptions): Promise<string> {
|
||||
return await this.storageService.get<string>(SecureStorageKeys.groupDelta);
|
||||
}
|
||||
|
||||
async setGroupDelta(value: string, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(SecureStorageKeys.groupDelta, value);
|
||||
}
|
||||
|
||||
async clearSyncSettings(hashToo = false): Promise<void> {
|
||||
await this.setUserDelta(null);
|
||||
await this.setGroupDelta(null);
|
||||
await this.setLastGroupSync(null);
|
||||
await this.setLastUserSync(null);
|
||||
if (hashToo) {
|
||||
await this.setLastSyncHash(null);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Environment URLs (inherited from base, simplified implementation)
|
||||
// ===================================================================
|
||||
|
||||
async getEnvironmentUrls(options?: StorageOptions): Promise<EnvironmentUrls> {
|
||||
return await this.storageService.get<EnvironmentUrls>("environmentUrls");
|
||||
}
|
||||
|
||||
async setEnvironmentUrls(value: EnvironmentUrls): Promise<void> {
|
||||
await this.storageService.save("environmentUrls", value);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Additional State Methods
|
||||
// ===================================================================
|
||||
|
||||
async getLocale(options?: StorageOptions): Promise<string> {
|
||||
return await this.storageService.get<string>("locale");
|
||||
}
|
||||
|
||||
async setLocale(value: string, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save("locale", value);
|
||||
}
|
||||
|
||||
async getInstalledVersion(options?: StorageOptions): Promise<string> {
|
||||
return await this.storageService.get<string>("installedVersion");
|
||||
}
|
||||
|
||||
async setInstalledVersion(value: string, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save("installedVersion", value);
|
||||
}
|
||||
}
|
||||
@@ -16,32 +16,13 @@ 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 {
|
||||
SecureStorageKeysLegacy as SecureStorageKeys,
|
||||
StoredSecurely,
|
||||
TempKeys as keys,
|
||||
} from "@/src/models/state.model";
|
||||
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
|
||||
@@ -8,48 +8,14 @@ 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 {
|
||||
MigrationClientKeys as ClientKeys,
|
||||
MigrationKeys as Keys,
|
||||
MigrationStateKeys as StateKeys,
|
||||
SecureStorageKeysMigration as SecureStorageKeys,
|
||||
} from "@/src/models/state.model";
|
||||
import { SyncConfiguration } from "@/src/models/syncConfiguration";
|
||||
|
||||
const SecureStorageKeys: { [key: string]: any } = {
|
||||
ldap: "ldapPassword",
|
||||
gsuite: "gsuitePrivateKey",
|
||||
azure: "azureKey",
|
||||
entra: "entraIdKey",
|
||||
okta: "oktaToken",
|
||||
oneLogin: "oneLoginClientSecret",
|
||||
directoryConfigPrefix: "directoryConfig_",
|
||||
sync: "syncConfig",
|
||||
directoryType: "directoryType",
|
||||
organizationId: "organizationId",
|
||||
};
|
||||
|
||||
const Keys: { [key: string]: any } = {
|
||||
entityId: "entityId",
|
||||
directoryType: "directoryType",
|
||||
organizationId: "organizationId",
|
||||
lastUserSync: "lastUserSync",
|
||||
lastGroupSync: "lastGroupSync",
|
||||
lastSyncHash: "lastSyncHash",
|
||||
syncingDir: "syncingDir",
|
||||
syncConfig: "syncConfig",
|
||||
userDelta: "userDeltaToken",
|
||||
groupDelta: "groupDeltaToken",
|
||||
tempDirectoryConfigs: "tempDirectoryConfigs",
|
||||
tempDirectorySettings: "tempDirectorySettings",
|
||||
};
|
||||
|
||||
const StateKeys = {
|
||||
global: "global",
|
||||
authenticatedAccounts: "authenticatedAccounts",
|
||||
};
|
||||
|
||||
const ClientKeys: { [key: string]: any } = {
|
||||
clientIdOld: "clientId",
|
||||
clientId: "apikey_clientId",
|
||||
clientSecretOld: "clientSecret",
|
||||
clientSecret: "apikey_clientSecret",
|
||||
};
|
||||
|
||||
export class StateMigrationService extends BaseStateMigrationService {
|
||||
async migrate(): Promise<void> {
|
||||
let currentStateVersion = await this.getCurrentStateVersion();
|
||||
@@ -61,6 +27,13 @@ export class StateMigrationService extends BaseStateMigrationService {
|
||||
break;
|
||||
case StateVersion.Two:
|
||||
await this.migrateStateFrom2To3();
|
||||
break;
|
||||
case StateVersion.Three:
|
||||
await this.migrateStateFrom3To4();
|
||||
break;
|
||||
case StateVersion.Four:
|
||||
await this.migrateStateFrom4To5();
|
||||
break;
|
||||
}
|
||||
currentStateVersion += 1;
|
||||
}
|
||||
@@ -198,4 +171,124 @@ export class StateMigrationService extends BaseStateMigrationService {
|
||||
globals.stateVersion = StateVersion.Three;
|
||||
await this.set(StateKeys.global, globals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate from State v4 (Account-based hierarchy) to v5 (flat key-value structure)
|
||||
*
|
||||
* This is a clean break from the Account-based structure. Data is extracted from
|
||||
* the account and saved into flat keys for simpler access.
|
||||
*
|
||||
* Old structure: authenticatedAccounts -> userId -> account.directorySettings/directoryConfigurations
|
||||
* New structure: flat keys like "directoryType", "organizationId", "directory_ldap", etc.
|
||||
*
|
||||
* Secrets migrate from: {userId}_{secretKey} -> secret_{secretKey}
|
||||
*/
|
||||
protected async migrateStateFrom4To5(useSecureStorageForSecrets = true): Promise<void> {
|
||||
// Get the authenticated user IDs from v3 structure
|
||||
const authenticatedUserIds = await this.get<string[]>(StateKeys.authenticatedAccounts);
|
||||
|
||||
if (
|
||||
!authenticatedUserIds ||
|
||||
!Array.isArray(authenticatedUserIds) ||
|
||||
authenticatedUserIds.length === 0
|
||||
) {
|
||||
// No accounts to migrate, just update version
|
||||
const globals = await this.getGlobals();
|
||||
globals.stateVersion = StateVersion.Five;
|
||||
await this.set(StateKeys.global, globals);
|
||||
return;
|
||||
}
|
||||
|
||||
// DC is single-user, so we take the first (and likely only) account
|
||||
const userId = authenticatedUserIds[0];
|
||||
const account = await this.get<Account>(userId);
|
||||
|
||||
if (!account) {
|
||||
// No account data found, just update version
|
||||
const globals = await this.getGlobals();
|
||||
globals.stateVersion = StateVersion.Five;
|
||||
await this.set(StateKeys.global, globals);
|
||||
return;
|
||||
}
|
||||
|
||||
// Migrate directory configurations to flat structure
|
||||
if (account.directoryConfigurations) {
|
||||
if (account.directoryConfigurations.ldap) {
|
||||
await this.set("directory_ldap", account.directoryConfigurations.ldap);
|
||||
}
|
||||
if (account.directoryConfigurations.gsuite) {
|
||||
await this.set("directory_gsuite", account.directoryConfigurations.gsuite);
|
||||
}
|
||||
if (account.directoryConfigurations.entra) {
|
||||
await this.set("directory_entra", account.directoryConfigurations.entra);
|
||||
} else if (account.directoryConfigurations.azure) {
|
||||
// Backwards compatibility: migrate azure to entra
|
||||
await this.set("directory_entra", account.directoryConfigurations.azure);
|
||||
}
|
||||
if (account.directoryConfigurations.okta) {
|
||||
await this.set("directory_okta", account.directoryConfigurations.okta);
|
||||
}
|
||||
if (account.directoryConfigurations.oneLogin) {
|
||||
await this.set("directory_onelogin", account.directoryConfigurations.oneLogin);
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate directory settings to flat structure
|
||||
if (account.directorySettings) {
|
||||
if (account.directorySettings.organizationId) {
|
||||
await this.set("organizationId", account.directorySettings.organizationId);
|
||||
}
|
||||
if (account.directorySettings.directoryType != null) {
|
||||
await this.set("directoryType", account.directorySettings.directoryType);
|
||||
}
|
||||
if (account.directorySettings.sync) {
|
||||
await this.set("sync", account.directorySettings.sync);
|
||||
}
|
||||
if (account.directorySettings.lastUserSync) {
|
||||
await this.set("lastUserSync", account.directorySettings.lastUserSync);
|
||||
}
|
||||
if (account.directorySettings.lastGroupSync) {
|
||||
await this.set("lastGroupSync", account.directorySettings.lastGroupSync);
|
||||
}
|
||||
if (account.directorySettings.lastSyncHash) {
|
||||
await this.set("lastSyncHash", account.directorySettings.lastSyncHash);
|
||||
}
|
||||
if (account.directorySettings.userDelta) {
|
||||
await this.set("userDelta", account.directorySettings.userDelta);
|
||||
}
|
||||
if (account.directorySettings.groupDelta) {
|
||||
await this.set("groupDelta", account.directorySettings.groupDelta);
|
||||
}
|
||||
if (account.directorySettings.syncingDir != null) {
|
||||
await this.set("syncingDir", account.directorySettings.syncingDir);
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate secrets from {userId}_* to secret_* pattern
|
||||
if (useSecureStorageForSecrets) {
|
||||
const oldSecretKeys = [
|
||||
{ old: `${userId}_${SecureStorageKeys.ldap}`, new: "secret_ldap" },
|
||||
{ old: `${userId}_${SecureStorageKeys.gsuite}`, new: "secret_gsuite" },
|
||||
{ old: `${userId}_${SecureStorageKeys.azure}`, new: "secret_azure" },
|
||||
{ old: `${userId}_${SecureStorageKeys.entra}`, new: "secret_entra" },
|
||||
{ old: `${userId}_${SecureStorageKeys.okta}`, new: "secret_okta" },
|
||||
{ old: `${userId}_${SecureStorageKeys.oneLogin}`, new: "secret_onelogin" },
|
||||
];
|
||||
|
||||
for (const { old: oldKey, new: newKey } of oldSecretKeys) {
|
||||
if (await this.secureStorageService.has(oldKey)) {
|
||||
const value = await this.secureStorageService.get(oldKey);
|
||||
if (value) {
|
||||
await this.secureStorageService.save(newKey, value);
|
||||
}
|
||||
// @TODO Keep old key for now - will remove in future release
|
||||
// await this.secureStorageService.remove(oldKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const globals = await this.getGlobals();
|
||||
globals.stateVersion = StateVersion.Five;
|
||||
await this.set(StateKeys.global, globals);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import { DirectoryType } from "../enums/directoryType";
|
||||
import { BatchRequestBuilder } from "./batch-request-builder";
|
||||
import { LdapDirectoryService } from "./directory-services/ldap-directory.service";
|
||||
import { SingleRequestBuilder } from "./single-request-builder";
|
||||
import { StateService } from "./state.service";
|
||||
import { StateService } from "./state-service/state.service";
|
||||
import { SyncService } from "./sync.service";
|
||||
import * as constants from "./sync.service";
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { BatchRequestBuilder } from "./batch-request-builder";
|
||||
import { LdapDirectoryService } from "./directory-services/ldap-directory.service";
|
||||
import { I18nService } from "./i18n.service";
|
||||
import { SingleRequestBuilder } from "./single-request-builder";
|
||||
import { StateService } from "./state.service";
|
||||
import { StateService } from "./state-service/state.service";
|
||||
import { SyncService } from "./sync.service";
|
||||
import * as constants from "./sync.service";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user