mirror of
https://github.com/bitwarden/directory-connector
synced 2026-02-02 17:53:19 +00:00
Compare commits
1 Commits
context-ru
...
googleapi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1d7920d73 |
@@ -1,706 +0,0 @@
|
|||||||
# Bitwarden Directory Connector - Claude Code Configuration
|
|
||||||
|
|
||||||
Sync users and groups from enterprise directory services (LDAP, Entra ID, Google Workspace, Okta, OneLogin) to Bitwarden organizations. Available as both a desktop GUI (Electron + Angular) and a CLI tool (`bwdc`).
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
### What This Project Does
|
|
||||||
|
|
||||||
- Connects to enterprise identity providers and retrieves user/group membership data
|
|
||||||
- Syncs that data to Bitwarden organizations via the Directory Connector API
|
|
||||||
- Provides both a desktop GUI application (Electron) and a command-line interface (`bwdc`)
|
|
||||||
|
|
||||||
### Key Concepts
|
|
||||||
|
|
||||||
- **Directory Service**: An identity provider (LDAP, Entra ID, GSuite, Okta, OneLogin) that stores users and groups
|
|
||||||
- **Sync**: The process of fetching entries from a directory and importing them to Bitwarden
|
|
||||||
- **Delta Sync**: Incremental synchronization that only fetches changes since the last sync
|
|
||||||
- **Entry**: Base class for `UserEntry` and `GroupEntry` - the core data models
|
|
||||||
- **Force Sync**: Ignores delta tokens and fetches all entries fresh
|
|
||||||
- **Test Mode**: Simulates sync without making API calls or updating state
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture & Patterns
|
|
||||||
|
|
||||||
### System Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
User Request (GUI/CLI)
|
|
||||||
↓
|
|
||||||
┌───────────────────────────────────┐
|
|
||||||
│ Entry Points │
|
|
||||||
│ main.ts (GUI) │ bwdc.ts (CLI) │
|
|
||||||
└───────────────────────────────────┘
|
|
||||||
↓
|
|
||||||
┌───────────────────────────────────┐
|
|
||||||
│ SyncService │
|
|
||||||
│ Orchestrates the sync flow │
|
|
||||||
└───────────────────────────────────┘
|
|
||||||
↓
|
|
||||||
┌───────────────────────────────────┐
|
|
||||||
│ DirectoryFactoryService │
|
|
||||||
│ Creates appropriate IDirectory │
|
|
||||||
└───────────────────────────────────┘
|
|
||||||
↓
|
|
||||||
┌─────────────────────────────────────────────────────┐
|
|
||||||
│ Directory Services │
|
|
||||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────┐ │
|
|
||||||
│ │ LDAP │ │ EntraID │ │ GSuite │ │ Okta/1Login │ │
|
|
||||||
│ └─────────┘ └─────────┘ └─────────┘ └─────────────┘ │
|
|
||||||
└─────────────────────────────────────────────────────┘
|
|
||||||
↓
|
|
||||||
┌───────────────────────────────────┐
|
|
||||||
│ [GroupEntry[], UserEntry[]]│
|
|
||||||
└───────────────────────────────────┘
|
|
||||||
↓
|
|
||||||
┌───────────────────────────────────┐
|
|
||||||
│ RequestBuilder (Batched) │
|
|
||||||
│ SingleRequestBuilder (<2000) │
|
|
||||||
│ BatchRequestBuilder (>2000) │
|
|
||||||
└───────────────────────────────────┘
|
|
||||||
↓
|
|
||||||
┌───────────────────────────────────┐
|
|
||||||
│ Bitwarden API │
|
|
||||||
│ POST /import endpoint │
|
|
||||||
└───────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Code Organization
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── abstractions/ # Interface definitions (IDirectoryService, etc.)
|
|
||||||
├── app/ # Angular GUI components
|
|
||||||
│ ├── tabs/ # Tab-based navigation (Dashboard, Settings, More)
|
|
||||||
│ └── services/ # Angular service providers
|
|
||||||
├── commands/ # CLI command implementations
|
|
||||||
├── enums/ # TypeScript enums (DirectoryType, etc.)
|
|
||||||
├── models/ # Data models (Entry, UserEntry, GroupEntry)
|
|
||||||
├── services/ # Business logic implementations
|
|
||||||
│ └── directory-services/ # One service per directory provider
|
|
||||||
├── bwdc.ts # CLI entry point
|
|
||||||
├── main.ts # Electron main process entry point
|
|
||||||
└── program.ts # CLI command routing (Commander.js)
|
|
||||||
|
|
||||||
jslib/ # Legacy shared libraries (do not add new code here)
|
|
||||||
utils/ # Integration test fixtures
|
|
||||||
└── openldap/ # Docker configs, test data, certificates
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Principles
|
|
||||||
|
|
||||||
1. **Shared Service Layer**: GUI (Angular) and CLI share identical service implementations
|
|
||||||
2. **Factory Pattern**: `DirectoryFactoryService` instantiates the correct `IDirectoryService` based on `DirectoryType`
|
|
||||||
3. **Secure Storage**: Credentials stored in system keychain via `KeytarSecureStorageService`
|
|
||||||
4. **Delta Tracking**: Incremental sync via delta tokens to minimize API calls
|
|
||||||
|
|
||||||
### Core Patterns
|
|
||||||
|
|
||||||
#### Directory Service Pattern
|
|
||||||
|
|
||||||
**Purpose**: Abstract different identity providers behind a common interface
|
|
||||||
|
|
||||||
**Interface** (`src/abstractions/directory.service.ts`):
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export interface IDirectoryService {
|
|
||||||
getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Implementations** in `src/services/directory-services/`:
|
|
||||||
|
|
||||||
- `ldap-directory.service.ts` - LDAP/Active Directory
|
|
||||||
- `entra-id-directory.service.ts` - Microsoft Entra ID (Azure AD)
|
|
||||||
- `gsuite-directory.service.ts` - Google Workspace
|
|
||||||
- `okta-directory.service.ts` - Okta
|
|
||||||
- `onelogin-directory.service.ts` - OneLogin
|
|
||||||
|
|
||||||
**Factory** (`src/services/directory-factory.service.ts`):
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
createService(type: DirectoryType): IDirectoryService
|
|
||||||
```
|
|
||||||
|
|
||||||
#### State Service Pattern
|
|
||||||
|
|
||||||
**Purpose**: Manage persistent state and credential storage
|
|
||||||
|
|
||||||
**Implementation** (`src/services/state.service.ts`):
|
|
||||||
|
|
||||||
- Configuration and sync settings stored in LowDB (JSON file)
|
|
||||||
- Sensitive data (passwords, API keys) stored in system keychain
|
|
||||||
- File locking via `proper-lockfile` to prevent concurrent access corruption
|
|
||||||
- Platform-specific app data directories:
|
|
||||||
- macOS: `~/Library/Application Support/Bitwarden Directory Connector`
|
|
||||||
- Windows: `%APPDATA%/Bitwarden Directory Connector`
|
|
||||||
- Linux: `~/.config/Bitwarden Directory Connector` or `$XDG_CONFIG_HOME`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Development Guide
|
|
||||||
|
|
||||||
### Adding a New Directory Service
|
|
||||||
|
|
||||||
**1. Create the enum value** (`src/enums/directoryType.ts`)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export enum DirectoryType {
|
|
||||||
Ldap = 0,
|
|
||||||
EntraID = 1,
|
|
||||||
GSuite = 2,
|
|
||||||
Okta = 3,
|
|
||||||
OneLogin = 4,
|
|
||||||
NewProvider = 5, // Add here
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. Create the configuration model** (`src/models/newProviderConfiguration.ts`)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export class NewProviderConfiguration {
|
|
||||||
apiUrl: string;
|
|
||||||
apiToken: string;
|
|
||||||
// Provider-specific settings
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**3. Implement the directory service** (`src/services/directory-services/newprovider-directory.service.ts`)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { IDirectoryService } from "@/src/abstractions/directory.service";
|
|
||||||
import { GroupEntry } from "@/src/models/groupEntry";
|
|
||||||
import { UserEntry } from "@/src/models/userEntry";
|
|
||||||
import { BaseDirectoryService } from "./base-directory.service";
|
|
||||||
|
|
||||||
export class NewProviderDirectoryService extends BaseDirectoryService implements IDirectoryService {
|
|
||||||
constructor(
|
|
||||||
private logService: LogService,
|
|
||||||
private i18nService: I18nService,
|
|
||||||
private stateService: StateService,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
|
||||||
const config = await this.stateService.getDirectory<NewProviderConfiguration>(
|
|
||||||
DirectoryType.NewProvider,
|
|
||||||
);
|
|
||||||
const syncConfig = await this.stateService.getSync();
|
|
||||||
|
|
||||||
const groups: GroupEntry[] = [];
|
|
||||||
const users: UserEntry[] = [];
|
|
||||||
|
|
||||||
// Fetch from provider API
|
|
||||||
// Apply filters using inherited filter methods
|
|
||||||
|
|
||||||
return [groups, users];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**4. Register in the factory** (`src/services/directory-factory.service.ts`)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
case DirectoryType.NewProvider:
|
|
||||||
return new NewProviderDirectoryService(
|
|
||||||
this.logService,
|
|
||||||
this.i18nService,
|
|
||||||
this.stateService
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
**5. Add state service support** (`src/services/state.service.ts`)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Add to secure storage keys if credentials involved
|
|
||||||
// Add configuration getter/setter methods
|
|
||||||
```
|
|
||||||
|
|
||||||
**6. Write tests** (`src/services/directory-services/newprovider-directory.service.spec.ts`)
|
|
||||||
|
|
||||||
### Common Patterns
|
|
||||||
|
|
||||||
#### Error Handling with State Rollback
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
async sync(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
|
||||||
// Store initial state for rollback
|
|
||||||
const startingUserDelta = await this.stateService.getUserDelta();
|
|
||||||
const startingGroupDelta = await this.stateService.getGroupDelta();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Perform sync operations
|
|
||||||
const [groups, users] = await this.directoryService.getEntries(force, test);
|
|
||||||
// ... process and submit
|
|
||||||
return [groups, users];
|
|
||||||
} catch (e) {
|
|
||||||
if (!test) {
|
|
||||||
// Rollback deltas on failure
|
|
||||||
await this.stateService.setUserDelta(startingUserDelta);
|
|
||||||
await this.stateService.setGroupDelta(startingGroupDelta);
|
|
||||||
}
|
|
||||||
this.messagingService.send("dirSyncCompleted", { successfully: false });
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Filter Processing
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// In BaseDirectoryService
|
|
||||||
protected buildIncludeSet(filter: string): Set<string> {
|
|
||||||
// Parse filter like "include:user1@example.com,user2@example.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
protected buildExcludeSet(filter: string): Set<string> {
|
|
||||||
// Parse filter like "exclude:user1@example.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
protected shouldIncludeUser(user: UserEntry, include: Set<string>, exclude: Set<string>): boolean {
|
|
||||||
if (exclude.has(user.email)) return false;
|
|
||||||
if (include.size === 0) return true;
|
|
||||||
return include.has(user.email);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running the Desktop GUI (Development)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
npm run rebuild # Rebuild native modules (keytar)
|
|
||||||
npm run electron # Run GUI with hot reload
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running the CLI (Development)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
npm run build:cli:watch # Build CLI with watch mode
|
|
||||||
node ./build-cli/bwdc.js --help # Run CLI commands
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Data Models
|
|
||||||
|
|
||||||
### Core Types
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Base entry class (src/models/entry.ts)
|
|
||||||
abstract class Entry {
|
|
||||||
referenceId: string; // Unique ID within the directory (e.g., DN for LDAP)
|
|
||||||
externalId: string; // ID used for Bitwarden import
|
|
||||||
}
|
|
||||||
|
|
||||||
// User entry (src/models/userEntry.ts)
|
|
||||||
class UserEntry extends Entry {
|
|
||||||
email: string;
|
|
||||||
disabled: boolean;
|
|
||||||
deleted: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group entry (src/models/groupEntry.ts)
|
|
||||||
class GroupEntry extends Entry {
|
|
||||||
name: string;
|
|
||||||
userMemberExternalIds: Set<string>; // External IDs of member users
|
|
||||||
groupMemberReferenceIds: Set<string>; // Reference IDs of nested groups
|
|
||||||
users: UserEntry[]; // Populated for display/simulation
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Directory Type Enum
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// src/enums/directoryType.ts
|
|
||||||
enum DirectoryType {
|
|
||||||
Ldap = 0,
|
|
||||||
EntraID = 1,
|
|
||||||
GSuite = 2,
|
|
||||||
Okta = 3,
|
|
||||||
OneLogin = 4,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration Models
|
|
||||||
|
|
||||||
Each directory provider has a configuration class in `src/models/`:
|
|
||||||
|
|
||||||
- `LdapConfiguration` - hostname, port, SSL/TLS, bind credentials, auth mode
|
|
||||||
- `EntraIdConfiguration` - tenant, client ID, secret key
|
|
||||||
- `GSuiteConfiguration` - domain, admin user, client email, private key
|
|
||||||
- `OktaConfiguration` - organization URL, API token
|
|
||||||
- `OneLoginConfiguration` - client ID, client secret, region
|
|
||||||
|
|
||||||
### Sync Configuration
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// src/models/syncConfiguration.ts
|
|
||||||
interface SyncConfiguration {
|
|
||||||
users: boolean; // Sync users
|
|
||||||
groups: boolean; // Sync groups
|
|
||||||
interval: number; // Minutes between syncs (minimum 5)
|
|
||||||
userFilter: string; // Include/exclude filter
|
|
||||||
groupFilter: string; // Include/exclude filter
|
|
||||||
removeDisabled: boolean; // Remove disabled users from org
|
|
||||||
overwriteExisting: boolean; // Overwrite existing entries
|
|
||||||
largeImport: boolean; // Enable for >2000 entries
|
|
||||||
// LDAP-specific
|
|
||||||
groupObjectClass: string;
|
|
||||||
userObjectClass: string;
|
|
||||||
groupPath: string;
|
|
||||||
userPath: string;
|
|
||||||
// ... additional LDAP attributes
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security & Configuration
|
|
||||||
|
|
||||||
### Security Rules
|
|
||||||
|
|
||||||
**MANDATORY - These rules have no exceptions:**
|
|
||||||
|
|
||||||
1. **Never log credentials**: API keys, passwords, tokens, and secrets must never appear in logs
|
|
||||||
2. **Never hardcode secrets**: All URLs, credentials, and sensitive data must come from configuration
|
|
||||||
3. **Use KeytarSecureStorageService**: All credentials must be stored in the system keychain
|
|
||||||
4. **Validate external data**: Sanitize all data received from directory services
|
|
||||||
5. **LDAP injection prevention**: Be cautious with user-provided LDAP filters
|
|
||||||
|
|
||||||
### Secure Storage Keys
|
|
||||||
|
|
||||||
The following are stored in the system keychain (not plain JSON):
|
|
||||||
|
|
||||||
- `ldapPassword` - LDAP bind password
|
|
||||||
- `gsuitePrivateKey` - Google Workspace private key
|
|
||||||
- `entraKey` - Microsoft Entra ID client secret
|
|
||||||
- `oktaToken` - Okta API token
|
|
||||||
- `oneLoginClientSecret` - OneLogin client secret
|
|
||||||
- User/group delta tokens
|
|
||||||
- Sync hashes
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
| Variable | Required | Description | Example |
|
|
||||||
| ------------------------------------------ | -------- | ---------------------------------------- | -------------------- |
|
|
||||||
| `BITWARDENCLI_CONNECTOR_APPDATA_DIR` | No | CLI app data directory override | `/custom/path` |
|
|
||||||
| `BITWARDEN_CONNECTOR_APPDATA_DIR` | No | GUI app data directory override | `/custom/path` |
|
|
||||||
| `BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS` | No | Store secrets in plain text (debug only) | `true` |
|
|
||||||
| `BITWARDENCLI_CONNECTOR_DEBUG` | No | Enable debug logging | `true` |
|
|
||||||
| `BW_CLIENTID` | No | CLI login client ID | `organization.xxxxx` |
|
|
||||||
| `BW_CLIENTSECRET` | No | CLI login client secret | `xxxxx` |
|
|
||||||
| `BW_NOINTERACTION` | No | Disable interactive prompts | `true` |
|
|
||||||
| `BW_PRETTY` | No | Pretty-print JSON output | `true` |
|
|
||||||
| `BW_RAW` | No | Raw output (no formatting) | `true` |
|
|
||||||
| `BW_RESPONSE` | No | JSON response format | `true` |
|
|
||||||
| `BW_QUIET` | No | Suppress stdout | `true` |
|
|
||||||
|
|
||||||
### Authentication & Authorization
|
|
||||||
|
|
||||||
- **API Token Authentication**: Uses organization `clientId` + `clientSecret`
|
|
||||||
- **Token Storage**: Access tokens and refresh tokens stored securely via Keytar
|
|
||||||
- **Token Refresh**: Automatic refresh when access token expires
|
|
||||||
- **Auth Service**: `src/services/auth.service.ts` handles the authentication flow
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
### Test Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── services/
|
|
||||||
│ ├── sync.service.spec.ts # Unit tests (colocated)
|
|
||||||
│ ├── sync.service.integration.spec.ts # Integration tests
|
|
||||||
│ └── directory-services/
|
|
||||||
│ ├── ldap-directory.service.spec.ts
|
|
||||||
│ └── ldap-directory.service.integration.spec.ts
|
|
||||||
utils/
|
|
||||||
└── openldap/
|
|
||||||
├── config-fixtures.ts # Test configuration helpers
|
|
||||||
├── user-fixtures.ts # Expected user data
|
|
||||||
├── group-fixtures.ts # Expected group data
|
|
||||||
├── certs/ # TLS certificates
|
|
||||||
└── docker-compose.yml # LDAP container config
|
|
||||||
```
|
|
||||||
|
|
||||||
### Writing Tests
|
|
||||||
|
|
||||||
**Unit Test Template**:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { mock, MockProxy } from "jest-mock-extended";
|
|
||||||
|
|
||||||
describe("ServiceName", () => {
|
|
||||||
let logService: MockProxy<LogService>;
|
|
||||||
let stateService: MockProxy<StateService>;
|
|
||||||
let service: ServiceUnderTest;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
logService = mock();
|
|
||||||
stateService = mock();
|
|
||||||
service = new ServiceUnderTest(logService, stateService);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should do something", async () => {
|
|
||||||
// Arrange
|
|
||||||
stateService.getSomeValue.mockResolvedValue(expectedValue);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const result = await service.doSomething();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result).toEqual(expectedResult);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Integration Test Template** (see `ldap-directory.service.integration.spec.ts`):
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Requires Docker containers running
|
|
||||||
// npm run test:integration:setup
|
|
||||||
|
|
||||||
describe("ldapDirectoryService", () => {
|
|
||||||
let stateService: MockProxy<StateService>;
|
|
||||||
let directoryService: LdapDirectoryService;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
stateService = mock();
|
|
||||||
stateService.getDirectoryType.mockResolvedValue(DirectoryType.Ldap);
|
|
||||||
stateService.getDirectory
|
|
||||||
.calledWith(DirectoryType.Ldap)
|
|
||||||
.mockResolvedValue(getLdapConfiguration());
|
|
||||||
});
|
|
||||||
|
|
||||||
it("syncs users and groups", async () => {
|
|
||||||
const result = await directoryService.getEntries(true, true);
|
|
||||||
expect(result).toEqual([groupFixtures, userFixtures]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running Tests
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm test # All unit tests (excludes integration)
|
|
||||||
npm test -- path/to/file.spec.ts # Single test file
|
|
||||||
npm run test:watch # Watch mode
|
|
||||||
|
|
||||||
# Integration tests
|
|
||||||
npm run test:integration:setup # Start Docker containers
|
|
||||||
npm run test:integration # Run integration tests
|
|
||||||
npm run test:integration:watch # Watch mode for integration
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Environment
|
|
||||||
|
|
||||||
- **Mocking**: `jest-mock-extended` with `mock<Type>()` for type-safe mocks
|
|
||||||
- **Alternative**: `@fluffy-spoon/substitute` available for some tests
|
|
||||||
- **Integration**: Docker containers for LDAP (OpenLDAP)
|
|
||||||
- **Fixtures**: Located in `utils/openldap/`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Code Style & Standards
|
|
||||||
|
|
||||||
### Formatting
|
|
||||||
|
|
||||||
- **Prettier**: Auto-formatting enforced via pre-commit hooks
|
|
||||||
- **Config**: `.prettierrc` in project root
|
|
||||||
|
|
||||||
### Naming Conventions
|
|
||||||
|
|
||||||
- `camelCase` for: variables, functions, method names
|
|
||||||
- `PascalCase` for: classes, interfaces, types, enums
|
|
||||||
- `SCREAMING_SNAKE_CASE` for: constants (rare in this codebase)
|
|
||||||
|
|
||||||
### Imports
|
|
||||||
|
|
||||||
**Path Aliases:**
|
|
||||||
|
|
||||||
- `@/` maps to project root
|
|
||||||
- Example: `import { SyncService } from "@/src/services/sync.service"`
|
|
||||||
|
|
||||||
**Import Order (ESLint enforced):**
|
|
||||||
|
|
||||||
1. External packages (node_modules)
|
|
||||||
2. jslib imports (`@/jslib/...`)
|
|
||||||
3. Project imports (`@/src/...`)
|
|
||||||
4. Alphabetized within each group with newlines between groups
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// External
|
|
||||||
import { mock, MockProxy } from "jest-mock-extended";
|
|
||||||
|
|
||||||
// jslib
|
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
|
||||||
|
|
||||||
// Project
|
|
||||||
import { DirectoryType } from "@/src/enums/directoryType";
|
|
||||||
import { SyncService } from "@/src/services/sync.service";
|
|
||||||
```
|
|
||||||
|
|
||||||
### Comments
|
|
||||||
|
|
||||||
- Avoid unnecessary comments; code should be self-documenting
|
|
||||||
- Use JSDoc only for public APIs that need documentation
|
|
||||||
- Inline comments for complex logic only
|
|
||||||
|
|
||||||
### Pre-commit Hooks
|
|
||||||
|
|
||||||
- **Husky**: Runs `lint-staged` on commit
|
|
||||||
- **lint-staged**: Runs Prettier on all files, ESLint on TypeScript files
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run lint # Check ESLint + Prettier
|
|
||||||
npm run lint:fix # Auto-fix ESLint issues
|
|
||||||
npm run prettier # Auto-format with Prettier
|
|
||||||
npm run test:types # TypeScript type checking
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Anti-Patterns
|
|
||||||
|
|
||||||
### DO
|
|
||||||
|
|
||||||
- ✅ Use `KeytarSecureStorageService` for all credential storage
|
|
||||||
- ✅ Implement `IDirectoryService` interface for new directory providers
|
|
||||||
- ✅ Use the factory pattern via `DirectoryFactoryService`
|
|
||||||
- ✅ Write unit tests with `jest-mock-extended` mocks
|
|
||||||
- ✅ Handle errors with state rollback (delta tokens)
|
|
||||||
- ✅ Use path aliases (`@/src/...`) for imports
|
|
||||||
- ✅ Validate data from external directory services
|
|
||||||
- ✅ Use `force` and `test` parameters consistently in sync methods
|
|
||||||
|
|
||||||
### DON'T
|
|
||||||
|
|
||||||
- ❌ Log credentials, API keys, or tokens
|
|
||||||
- ❌ Hardcode URLs, secrets, or configuration values
|
|
||||||
- ❌ Store sensitive data in LowDB (JSON) - use Keytar
|
|
||||||
- ❌ Skip input validation for LDAP filters (injection risk)
|
|
||||||
- ❌ Use `any` types without explicit justification
|
|
||||||
- ❌ Add new code to `jslib/` (legacy, read-only)
|
|
||||||
- ❌ Ignore delta token rollback on sync failure
|
|
||||||
- ❌ Bypass `overwriteExisting` validation for batch imports (>2000 entries)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
### Building
|
|
||||||
|
|
||||||
**Desktop GUI (Electron):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build # Build main + renderer
|
|
||||||
npm run build:dist # Full distribution build
|
|
||||||
npm run dist:win # Windows installer
|
|
||||||
npm run dist:mac # macOS installer
|
|
||||||
npm run dist:lin # Linux packages (AppImage, RPM)
|
|
||||||
```
|
|
||||||
|
|
||||||
**CLI Tool:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build:cli:prod # Production build
|
|
||||||
npm run dist:cli:win # Windows executable
|
|
||||||
npm run dist:cli:mac # macOS executable
|
|
||||||
npm run dist:cli:lin # Linux executable
|
|
||||||
```
|
|
||||||
|
|
||||||
### Versioning
|
|
||||||
|
|
||||||
Follow semantic versioning: `MAJOR.MINOR.PATCH`
|
|
||||||
|
|
||||||
- Version format: `YYYY.MM.PATCH` (e.g., `2025.12.0`)
|
|
||||||
- Managed in `package.json`
|
|
||||||
|
|
||||||
### Publishing
|
|
||||||
|
|
||||||
- **CI/CD**: GitHub Actions workflows in `.github/workflows/`
|
|
||||||
- **build.yml**: Multi-platform builds with code signing
|
|
||||||
- **release.yml**: Version bumping and publishing
|
|
||||||
- **Code Signing**: Azure Key Vault (Windows), App Store Connect (macOS)
|
|
||||||
- **Auto-update**: Electron Updater for GUI application
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
#### LDAP Connection Failures
|
|
||||||
|
|
||||||
**Problem**: Cannot connect to LDAP server, timeout or connection refused
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
|
|
||||||
1. Verify hostname and port are correct
|
|
||||||
2. Check SSL/TLS settings match server configuration
|
|
||||||
3. For StartTLS, ensure SSL is enabled and use the non-secure port (389)
|
|
||||||
4. For LDAPS, use port 636 and provide CA certificate path
|
|
||||||
|
|
||||||
#### Keytar/Native Module Issues
|
|
||||||
|
|
||||||
**Problem**: `Error: Module did not self-register` or keytar-related crashes
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run rebuild # Rebuild native modules for current Electron version
|
|
||||||
npm run reset # Full reset of keytar module
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Sync Hash Mismatch
|
|
||||||
|
|
||||||
**Problem**: Sync runs but no changes appear in Bitwarden
|
|
||||||
|
|
||||||
**Solution**: The sync service skips if the hash matches the previous sync. Use force sync:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bwdc sync --force # CLI
|
|
||||||
# Or clear cache
|
|
||||||
bwdc clear-cache
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Large Import Failures
|
|
||||||
|
|
||||||
**Problem**: Sync fails for organizations with >2000 users/groups
|
|
||||||
|
|
||||||
**Solution**: Enable `largeImport` in sync settings. Note: `overwriteExisting` is incompatible with batch mode.
|
|
||||||
|
|
||||||
### Debug Tips
|
|
||||||
|
|
||||||
- Enable debug logging: `BITWARDENCLI_CONNECTOR_DEBUG=true`
|
|
||||||
- View data file location: `bwdc data-file`
|
|
||||||
- Test sync without making changes: `bwdc test`
|
|
||||||
- Check last sync times: `bwdc last-sync users` / `bwdc last-sync groups`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
### Official Documentation
|
|
||||||
|
|
||||||
- [Directory Sync CLI Documentation](https://bitwarden.com/help/directory-sync-cli/)
|
|
||||||
- [Directory Connector Help](https://bitwarden.com/help/directory-sync/)
|
|
||||||
|
|
||||||
### Internal Documentation
|
|
||||||
|
|
||||||
- [Bitwarden Contributing Guidelines](https://contributing.bitwarden.com/contributing/)
|
|
||||||
- [Code Style Guide](https://contributing.bitwarden.com/contributing/code-style/)
|
|
||||||
|
|
||||||
### Tools & Libraries
|
|
||||||
|
|
||||||
- [ldapts](https://github.com/ldapts/ldapts) - LDAP client for Node.js
|
|
||||||
- [Keytar](https://github.com/atom/node-keytar) - Native keychain access
|
|
||||||
- [Commander.js](https://github.com/tj/commander.js) - CLI framework
|
|
||||||
- [LowDB](https://github.com/typicode/lowdb) - JSON database
|
|
||||||
- [Microsoft Graph Client](https://github.com/microsoftgraph/msgraph-sdk-javascript) - Entra ID API
|
|
||||||
- [Google APIs](https://github.com/googleapis/google-api-nodejs-client) - GSuite API
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
---
|
|
||||||
description: "Provides a brief explanation of the code attached, including key components, notable patterns, and a code walkthrough."
|
|
||||||
---
|
|
||||||
|
|
||||||
# Code Explainer
|
|
||||||
|
|
||||||
Provide a brief explanation of the code attached. I'm trying to better understand it.
|
|
||||||
|
|
||||||
## Key Components
|
|
||||||
|
|
||||||
- Main classes/functions and their roles
|
|
||||||
- Important dependencies
|
|
||||||
- Critical flows
|
|
||||||
|
|
||||||
## Notable Patterns
|
|
||||||
|
|
||||||
- Design patterns used
|
|
||||||
- Architecture decisions
|
|
||||||
- Important abstractions
|
|
||||||
|
|
||||||
## Code Walkthrough
|
|
||||||
|
|
||||||
- How it works
|
|
||||||
- Key decision points
|
|
||||||
- Important considerations
|
|
||||||
|
|
||||||
## Gotchas & Tips
|
|
||||||
|
|
||||||
- Edge cases to watch for
|
|
||||||
- Performance considerations
|
|
||||||
10
.eslintignore
Normal file
10
.eslintignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
dist
|
||||||
|
build
|
||||||
|
build-cli
|
||||||
|
webpack.cli.js
|
||||||
|
webpack.main.js
|
||||||
|
webpack.renderer.js
|
||||||
|
|
||||||
|
**/node_modules
|
||||||
|
|
||||||
|
**/jest.config.js
|
||||||
95
.eslintrc.json
Normal file
95
.eslintrc.json
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.js"],
|
||||||
|
"plugins": ["@typescript-eslint", "rxjs", "rxjs-angular", "import"],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"project": ["./tsconfig.eslint.json"],
|
||||||
|
"sourceType": "module",
|
||||||
|
"ecmaVersion": 2020
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:import/recommended",
|
||||||
|
"plugin:import/typescript",
|
||||||
|
"prettier",
|
||||||
|
"plugin:rxjs/recommended"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"import/parsers": {
|
||||||
|
"@typescript-eslint/parser": [".ts"]
|
||||||
|
},
|
||||||
|
"import/resolver": {
|
||||||
|
"typescript": {
|
||||||
|
"alwaysTryTypes": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/explicit-member-accessibility": [
|
||||||
|
"error",
|
||||||
|
{ "accessibility": "no-public" }
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-explicit-any": "off", // TODO: This should be re-enabled
|
||||||
|
"@typescript-eslint/no-misused-promises": ["error", { "checksVoidReturn": false }],
|
||||||
|
"@typescript-eslint/no-this-alias": ["error", { "allowedNames": ["self"] }],
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
|
||||||
|
"no-console": "error",
|
||||||
|
"import/no-unresolved": "off", // TODO: Look into turning off once each package is an actual package.
|
||||||
|
"import/order": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"alphabetize": {
|
||||||
|
"order": "asc"
|
||||||
|
},
|
||||||
|
"newlines-between": "always",
|
||||||
|
"pathGroups": [
|
||||||
|
{
|
||||||
|
"pattern": "@/jslib/**/*",
|
||||||
|
"group": "external",
|
||||||
|
"position": "after"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "@/src/**/*",
|
||||||
|
"group": "parent",
|
||||||
|
"position": "before"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pathGroupsExcludedImportTypes": ["builtin"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rxjs-angular/prefer-takeuntil": "error",
|
||||||
|
"rxjs/no-exposed-subjects": ["error", { "allowProtected": true }],
|
||||||
|
"no-restricted-syntax": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"message": "Calling `svgIcon` directly is not allowed",
|
||||||
|
"selector": "CallExpression[callee.name='svgIcon']"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "Accessing FormGroup using `get` is not allowed, use `.value` instead",
|
||||||
|
"selector": "ChainExpression[expression.object.callee.property.name='get'][expression.property.name='value']"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"curly": ["error", "all"],
|
||||||
|
"import/namespace": ["off"], // This doesn't resolve namespace imports correctly, but TS will throw for this anyway
|
||||||
|
"no-restricted-imports": ["error", { "patterns": ["src/**/*"] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.html"],
|
||||||
|
"parser": "@angular-eslint/template-parser",
|
||||||
|
"plugins": ["@angular-eslint/template"],
|
||||||
|
"rules": {
|
||||||
|
"@angular-eslint/template/button-has-type": "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
5
.github/CODEOWNERS
vendored
5
.github/CODEOWNERS
vendored
@@ -12,8 +12,3 @@
|
|||||||
**/*.dockerignore @bitwarden/team-appsec @bitwarden/dept-bre
|
**/*.dockerignore @bitwarden/team-appsec @bitwarden/dept-bre
|
||||||
**/entrypoint.sh @bitwarden/team-appsec @bitwarden/dept-bre
|
**/entrypoint.sh @bitwarden/team-appsec @bitwarden/dept-bre
|
||||||
**/docker-compose.yml @bitwarden/team-appsec @bitwarden/dept-bre
|
**/docker-compose.yml @bitwarden/team-appsec @bitwarden/dept-bre
|
||||||
|
|
||||||
# Claude related files
|
|
||||||
.claude/ @bitwarden/team-ai-sme
|
|
||||||
.github/workflows/respond.yml @bitwarden/team-ai-sme
|
|
||||||
.github/workflows/review-code.yml @bitwarden/team-ai-sme
|
|
||||||
|
|||||||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
14
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,14 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
contact_links:
|
|
||||||
- name: Feature Requests
|
|
||||||
url: https://community.bitwarden.com/c/feature-requests/
|
|
||||||
about: Request new features using the Community Forums. Please search existing feature requests before making a new one.
|
|
||||||
- name: Bitwarden Community Forums
|
|
||||||
url: https://community.bitwarden.com
|
|
||||||
about: Please visit the community forums for general community discussion, support and the development roadmap.
|
|
||||||
- name: Customer Support
|
|
||||||
url: https://bitwarden.com/contact/
|
|
||||||
about: Please contact our customer support for account issues and general customer support.
|
|
||||||
- name: Security Issues
|
|
||||||
url: https://hackerone.com/bitwarden
|
|
||||||
about: We use HackerOne to manage security disclosures.
|
|
||||||
111
.github/ISSUE_TEMPLATE/issue.yml
vendored
111
.github/ISSUE_TEMPLATE/issue.yml
vendored
@@ -1,111 +0,0 @@
|
|||||||
name: Directory Connector Bug Report
|
|
||||||
description: File a bug report
|
|
||||||
title: "[DC] "
|
|
||||||
labels: ["bug"]
|
|
||||||
type: bug
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Thanks for taking the time to fill out this bug report!
|
|
||||||
|
|
||||||
Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests.
|
|
||||||
- type: textarea
|
|
||||||
id: reproduce
|
|
||||||
attributes:
|
|
||||||
label: Steps To Reproduce
|
|
||||||
description: How can we reproduce the behavior.
|
|
||||||
value: |
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. Click on '...'
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: expected
|
|
||||||
attributes:
|
|
||||||
label: Expected Result
|
|
||||||
description: A clear and concise description of what you expected to happen.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: actual
|
|
||||||
attributes:
|
|
||||||
label: Actual Result
|
|
||||||
description: A clear and concise description of what is happening.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: screenshots
|
|
||||||
attributes:
|
|
||||||
label: Screenshots or Videos
|
|
||||||
description: If applicable, add screenshots and/or a short video to help explain your problem.
|
|
||||||
- type: textarea
|
|
||||||
id: additional-context
|
|
||||||
attributes:
|
|
||||||
label: Additional Context
|
|
||||||
description: Add any other context about the problem here.
|
|
||||||
- type: dropdown
|
|
||||||
id: os
|
|
||||||
attributes:
|
|
||||||
label: Operating System
|
|
||||||
description: What operating system(s) are you seeing the problem on?
|
|
||||||
multiple: true
|
|
||||||
options:
|
|
||||||
- Windows
|
|
||||||
- macOS
|
|
||||||
- Linux
|
|
||||||
- Other operating system (please specify in "Additional Context" section)
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: os-version
|
|
||||||
attributes:
|
|
||||||
label: Operating System Version
|
|
||||||
description: What version of the operating system(s) are you seeing the problem on?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: directories
|
|
||||||
attributes:
|
|
||||||
label: Directory Service
|
|
||||||
description: What directory service(s) are you seeing the problem on?
|
|
||||||
multiple: true
|
|
||||||
options:
|
|
||||||
- LDAP - Active Directory
|
|
||||||
- Another LDAP implementation (please specify in "Additional Context" section)
|
|
||||||
- Microsoft Entra ID
|
|
||||||
- Google Workspace
|
|
||||||
- Okta Universal Directory
|
|
||||||
- OneLogin
|
|
||||||
- Other directory service (please specify in "Additional Context" section)
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: application-type
|
|
||||||
attributes:
|
|
||||||
label: Application Type
|
|
||||||
description: Which Directory Connector application(s) are you seeing the problem on?
|
|
||||||
multiple: true
|
|
||||||
options:
|
|
||||||
- GUI (the desktop application)
|
|
||||||
- CLI (the bwdc command line application)
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: version
|
|
||||||
attributes:
|
|
||||||
label: Build Version
|
|
||||||
description: What version of our software are you running?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: checkboxes
|
|
||||||
id: issue-tracking-info
|
|
||||||
attributes:
|
|
||||||
label: Issue Tracking Info
|
|
||||||
description: |
|
|
||||||
Make sure to acknowledge the following before submitting your report!
|
|
||||||
options:
|
|
||||||
- label: I understand that work is tracked outside of Github. A PR will be linked to this issue should one be opened to address it, but Bitwarden doesn't use fields like "assigned", "milestone", or "project" to track progress.
|
|
||||||
required: true
|
|
||||||
11
.github/renovate.json5
vendored
11
.github/renovate.json5
vendored
@@ -8,6 +8,12 @@
|
|||||||
matchManagers: ["github-actions"],
|
matchManagers: ["github-actions"],
|
||||||
matchUpdateTypes: ["minor", "patch"],
|
matchUpdateTypes: ["minor", "patch"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
groupName: "Google Libraries",
|
||||||
|
matchPackagePatterns: ["google-auth-library", "googleapis"],
|
||||||
|
matchManagers: ["npm"],
|
||||||
|
groupSlug: "google-libraries",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
ignoreDeps: [
|
ignoreDeps: [
|
||||||
// yao-pkg is used to create a single executable application bundle for the CLI.
|
// yao-pkg is used to create a single executable application bundle for the CLI.
|
||||||
@@ -15,10 +21,5 @@
|
|||||||
// This must be manually vetted by our appsec team before upgrading.
|
// This must be manually vetted by our appsec team before upgrading.
|
||||||
// It is excluded from renovate to avoid accidentally upgrading to a non-vetted version.
|
// It is excluded from renovate to avoid accidentally upgrading to a non-vetted version.
|
||||||
"@yao-pkg/pkg",
|
"@yao-pkg/pkg",
|
||||||
// googleapis uses ESM after 149.0.0 so we are not upgrading it until we have ESM support.
|
|
||||||
// They release new versions every couple of weeks so ignoring it at the dependency dashboard
|
|
||||||
// level is not sufficient.
|
|
||||||
// FIXME: remove and upgrade when we have ESM support.
|
|
||||||
"googleapis",
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
56
.github/workflows/build.yml
vendored
56
.github/workflows/build.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -51,12 +51,12 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
@@ -111,7 +111,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload Linux Zip to GitHub
|
- name: Upload Linux Zip to GitHub
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
|
name: bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
|
||||||
path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
|
path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
|
||||||
@@ -120,7 +120,7 @@ jobs:
|
|||||||
|
|
||||||
macos-cli:
|
macos-cli:
|
||||||
name: Build Mac CLI
|
name: Build Mac CLI
|
||||||
runs-on: macos-15-intel
|
runs-on: macos-13
|
||||||
needs: setup
|
needs: setup
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -129,12 +129,12 @@ jobs:
|
|||||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
@@ -182,7 +182,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload Mac Zip to GitHub
|
- name: Upload Mac Zip to GitHub
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
|
name: bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
|
||||||
path: ./dist-cli/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
|
path: ./dist-cli/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
|
||||||
@@ -200,7 +200,7 @@ jobs:
|
|||||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -209,7 +209,7 @@ jobs:
|
|||||||
choco install checksum --no-progress
|
choco install checksum --no-progress
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
@@ -258,7 +258,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
- name: Upload Windows Zip to GitHub
|
- name: Upload Windows Zip to GitHub
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
|
name: bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
|
||||||
path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
|
path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
|
||||||
@@ -279,12 +279,12 @@ jobs:
|
|||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
@@ -338,28 +338,28 @@ jobs:
|
|||||||
SIGNING_CERT_NAME: ${{ steps.retrieve-secrets.outputs.code-signing-cert-name }}
|
SIGNING_CERT_NAME: ${{ steps.retrieve-secrets.outputs.code-signing-cert-name }}
|
||||||
|
|
||||||
- name: Upload Portable Executable to GitHub
|
- name: Upload Portable Executable to GitHub
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
|
name: Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
|
||||||
path: ./dist/Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
|
path: ./dist/Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Installer Executable to GitHub
|
- name: Upload Installer Executable to GitHub
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
|
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
|
||||||
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
|
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Installer Executable Blockmap to GitHub
|
- name: Upload Installer Executable Blockmap to GitHub
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
|
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
|
||||||
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
|
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload latest auto-update artifact
|
- name: Upload latest auto-update artifact
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: latest.yml
|
name: latest.yml
|
||||||
path: ./dist/latest.yml
|
path: ./dist/latest.yml
|
||||||
@@ -379,12 +379,12 @@ jobs:
|
|||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
@@ -411,14 +411,14 @@ jobs:
|
|||||||
run: npm run dist:lin
|
run: npm run dist:lin
|
||||||
|
|
||||||
- name: Upload AppImage
|
- name: Upload AppImage
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
|
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
|
||||||
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
|
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload latest auto-update artifact
|
- name: Upload latest auto-update artifact
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: latest-linux.yml
|
name: latest-linux.yml
|
||||||
path: ./dist/latest-linux.yml
|
path: ./dist/latest-linux.yml
|
||||||
@@ -427,7 +427,7 @@ jobs:
|
|||||||
|
|
||||||
macos-gui:
|
macos-gui:
|
||||||
name: Build MacOS GUI
|
name: Build MacOS GUI
|
||||||
runs-on: macos-15-intel
|
runs-on: macos-13
|
||||||
needs: setup
|
needs: setup
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -439,12 +439,12 @@ jobs:
|
|||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
@@ -542,28 +542,28 @@ jobs:
|
|||||||
CSC_FOR_PULL_REQUEST: true
|
CSC_FOR_PULL_REQUEST: true
|
||||||
|
|
||||||
- name: Upload .zip artifact
|
- name: Upload .zip artifact
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
|
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
|
||||||
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
|
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload .dmg artifact
|
- name: Upload .dmg artifact
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
|
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
|
||||||
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
|
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload .dmg Blockmap artifact
|
- name: Upload .dmg Blockmap artifact
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
|
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
|
||||||
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
|
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload latest auto-update artifact
|
- name: Upload latest auto-update artifact
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: latest-mac.yml
|
name: latest-mac.yml
|
||||||
path: ./dist/latest-mac.yml
|
path: ./dist/latest-mac.yml
|
||||||
|
|||||||
111
.github/workflows/integration-test.yml
vendored
111
.github/workflows/integration-test.yml
vendored
@@ -2,36 +2,25 @@ name: Integration Testing
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
# Integration tests are slow, so only run them if relevant files have changed.
|
|
||||||
# This is done at the workflow level and at the job level.
|
|
||||||
# Make sure these triggers stay consistent with the 'changed-files' job.
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'main'
|
- "main"
|
||||||
- 'rc'
|
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/integration-test.yml" # this file
|
- ".github/workflows/integration-test.yml" # this file
|
||||||
- "docker-compose.yml" # any change to Docker configuration
|
- "src/services/ldap-directory.service*" # we only have integration for LDAP testing at the moment
|
||||||
- "package.json" # dependencies
|
- "./openldap/**/*" # any change to test fixtures
|
||||||
- "utils/**" # any change to test fixtures
|
- "./docker-compose.yml" # any change to Docker configuration
|
||||||
- "src/services/sync.service.ts" # core sync service used by all directory services
|
- "./package.json" # dependencies
|
||||||
- "src/services/directory-services/ldap-directory.service*" # LDAP directory service
|
|
||||||
- "src/services/directory-services/gsuite-directory.service*" # Google Workspace directory service
|
|
||||||
# Add directory services here as we add test coverage
|
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/integration-test.yml" # this file
|
- ".github/workflows/integration-test.yml" # this file
|
||||||
- "docker-compose.yml" # any change to Docker configuration
|
- "src/services/ldap-directory.service*" # we only have integration for LDAP testing at the moment
|
||||||
- "package.json" # dependencies
|
- "./openldap/**/*" # any change to test fixtures
|
||||||
- "utils/**" # any change to test fixtures
|
- "./docker-compose.yml" # any change to Docker configuration
|
||||||
- "src/services/sync.service.ts" # core sync service used by all directory services
|
- "./package.json" # dependencies
|
||||||
- "src/services/directory-services/ldap-directory.service*" # LDAP directory service
|
|
||||||
- "src/services/directory-services/gsuite-directory.service*" # Google Workspace directory service
|
|
||||||
# Add directory services here as we add test coverage
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
checks: write # required by dorny/test-reporter to upload its results
|
checks: write # required by dorny/test-reporter to upload its results
|
||||||
id-token: write # required to use OIDC to login to Azure Key Vault
|
|
||||||
jobs:
|
jobs:
|
||||||
testing:
|
testing:
|
||||||
name: Run tests
|
name: Run tests
|
||||||
@@ -40,7 +29,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -52,7 +41,7 @@ jobs:
|
|||||||
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
|
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
@@ -61,86 +50,28 @@ jobs:
|
|||||||
- name: Install Node dependencies
|
- name: Install Node dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
# Get secrets from Azure Key Vault
|
- name: Install mkcert
|
||||||
- name: Azure Login
|
|
||||||
uses: bitwarden/gh-actions/azure-login@main
|
|
||||||
with:
|
|
||||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
|
||||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
|
||||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
|
||||||
|
|
||||||
- name: Get KV Secrets
|
|
||||||
id: get-kv-secrets
|
|
||||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
|
||||||
with:
|
|
||||||
keyvault: gh-directory-connector
|
|
||||||
secrets: "GOOGLE-ADMIN-USER,GOOGLE-CLIENT-EMAIL,GOOGLE-DOMAIN,GOOGLE-PRIVATE-KEY"
|
|
||||||
|
|
||||||
- name: Azure Logout
|
|
||||||
uses: bitwarden/gh-actions/azure-logout@main
|
|
||||||
|
|
||||||
# Only run relevant tests depending on what files have changed.
|
|
||||||
# This should be kept consistent with the workflow level triggers.
|
|
||||||
# Note: docker-compose.yml is only used for ldap for now
|
|
||||||
- name: Get changed files
|
|
||||||
id: changed-files
|
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
|
||||||
with:
|
|
||||||
list-files: shell
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# Add directory services here as we add test coverage
|
|
||||||
filters: |
|
|
||||||
common:
|
|
||||||
- '.github/workflows/integration-test.yml'
|
|
||||||
- 'utils/**'
|
|
||||||
- 'package.json'
|
|
||||||
- 'src/services/sync.service.ts'
|
|
||||||
ldap:
|
|
||||||
- 'docker-compose.yml'
|
|
||||||
- 'src/services/directory-services/ldap-directory.service*'
|
|
||||||
google:
|
|
||||||
- 'src/services/directory-services/gsuite-directory.service*'
|
|
||||||
|
|
||||||
# LDAP
|
|
||||||
- name: Setup LDAP integration tests
|
|
||||||
if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.ldap == 'true'
|
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get -y install mkcert
|
sudo apt-get -y install mkcert
|
||||||
npm run test:integration:setup
|
|
||||||
|
|
||||||
- name: Run LDAP integration tests
|
- name: Setup integration tests
|
||||||
if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.ldap == 'true'
|
run: npm run test:integration:setup
|
||||||
env:
|
|
||||||
JEST_JUNIT_UNIQUE_OUTPUT_NAME: "true" # avoids junit outputs from clashing
|
|
||||||
run: npx jest ldap-directory.service.integration.spec.ts --coverage --coverageDirectory=coverage-ldap
|
|
||||||
|
|
||||||
# Google Workspace
|
- name: Run integration tests
|
||||||
- name: Run Google Workspace integration tests
|
run: npm run test:integration --coverage
|
||||||
if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.google == 'true'
|
|
||||||
env:
|
|
||||||
GOOGLE_DOMAIN: ${{ steps.get-kv-secrets.outputs.GOOGLE-DOMAIN }}
|
|
||||||
GOOGLE_ADMIN_USER: ${{ steps.get-kv-secrets.outputs.GOOGLE-ADMIN-USER }}
|
|
||||||
GOOGLE_CLIENT_EMAIL: ${{ steps.get-kv-secrets.outputs.GOOGLE-CLIENT-EMAIL }}
|
|
||||||
GOOGLE_PRIVATE_KEY: ${{ steps.get-kv-secrets.outputs.GOOGLE-PRIVATE-KEY }}
|
|
||||||
JEST_JUNIT_UNIQUE_OUTPUT_NAME: "true" # avoids junit outputs from clashing
|
|
||||||
run: |
|
|
||||||
npx jest gsuite-directory.service.integration.spec.ts --coverage --coverageDirectory=coverage-google
|
|
||||||
|
|
||||||
- name: Report test results
|
- name: Report test results
|
||||||
id: report
|
uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1
|
||||||
uses: dorny/test-reporter@fe45e9537387dac839af0d33ba56eed8e24189e8 # v2.3.0
|
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
||||||
# This will skip the job if it's a pull request from a fork, because that won't have permission to upload test results.
|
|
||||||
# PRs from the repository and all other events are OK.
|
|
||||||
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && !cancelled()
|
|
||||||
with:
|
with:
|
||||||
name: Test Results
|
name: Test Results
|
||||||
path: "junit.xml*"
|
path: "junit.xml"
|
||||||
reporter: jest-junit
|
reporter: jest-junit
|
||||||
fail-on-error: true
|
fail-on-error: true
|
||||||
|
|
||||||
- name: Upload coverage to codecov.io
|
- name: Upload coverage to codecov.io
|
||||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
uses: codecov/codecov-action@5a605bd92782ce0810fa3b8acc235c921b497052 # v5.2.0
|
||||||
|
|
||||||
- name: Upload results to codecov.io
|
- name: Upload results to codecov.io
|
||||||
uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1.2.1
|
uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
|||||||
release_version: ${{ steps.version.outputs.version }}
|
release_version: ${{ steps.version.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||||
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
|
uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0
|
||||||
env:
|
env:
|
||||||
PKG_VERSION: ${{ needs.setup.outputs.release_version }}
|
PKG_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
28
.github/workflows/respond.yml
vendored
28
.github/workflows/respond.yml
vendored
@@ -1,28 +0,0 @@
|
|||||||
name: Respond
|
|
||||||
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
pull_request_review_comment:
|
|
||||||
types: [created]
|
|
||||||
issues:
|
|
||||||
types: [opened, assigned]
|
|
||||||
pull_request_review:
|
|
||||||
types: [submitted]
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
respond:
|
|
||||||
name: Respond
|
|
||||||
uses: bitwarden/gh-actions/.github/workflows/_respond.yml@main
|
|
||||||
secrets:
|
|
||||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
|
||||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: write
|
|
||||||
id-token: write
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
21
.github/workflows/review-code.yml
vendored
21
.github/workflows/review-code.yml
vendored
@@ -1,21 +0,0 @@
|
|||||||
name: Code Review
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, reopened]
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
review:
|
|
||||||
name: Review
|
|
||||||
uses: bitwarden/gh-actions/.github/workflows/_review-code.yml@main
|
|
||||||
secrets:
|
|
||||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
|
||||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
pull-requests: write
|
|
||||||
14
.github/workflows/test.yml
vendored
14
.github/workflows/test.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
|
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
@@ -53,10 +53,8 @@ jobs:
|
|||||||
run: npm run test --coverage
|
run: npm run test --coverage
|
||||||
|
|
||||||
- name: Report test results
|
- name: Report test results
|
||||||
uses: dorny/test-reporter@fe45e9537387dac839af0d33ba56eed8e24189e8 # v2.3.0
|
uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1
|
||||||
# This will skip the job if it's a pull request from a fork, because that won't have permission to upload test results.
|
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
||||||
# PRs from the repository and all other events are OK.
|
|
||||||
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && !cancelled()
|
|
||||||
with:
|
with:
|
||||||
name: Test Results
|
name: Test Results
|
||||||
path: "junit.xml"
|
path: "junit.xml"
|
||||||
@@ -64,7 +62,7 @@ jobs:
|
|||||||
fail-on-error: true
|
fail-on-error: true
|
||||||
|
|
||||||
- name: Upload coverage to codecov.io
|
- name: Upload coverage to codecov.io
|
||||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
uses: codecov/codecov-action@5a605bd92782ce0810fa3b8acc235c921b497052 # v5.2.0
|
||||||
|
|
||||||
- name: Upload results to codecov.io
|
- name: Upload results to codecov.io
|
||||||
uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1.2.1
|
uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2
|
||||||
|
|||||||
5
.github/workflows/version-bump.yml
vendored
5
.github/workflows/version-bump.yml
vendored
@@ -42,15 +42,14 @@ jobs:
|
|||||||
uses: bitwarden/gh-actions/azure-logout@main
|
uses: bitwarden/gh-actions/azure-logout@main
|
||||||
|
|
||||||
- name: Generate GH App token
|
- name: Generate GH App token
|
||||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
||||||
id: app-token
|
id: app-token
|
||||||
with:
|
with:
|
||||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||||
permission-contents: write
|
|
||||||
|
|
||||||
- name: Checkout Branch
|
- name: Checkout Branch
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.app-token.outputs.token }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -2,9 +2,6 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Environment variables used for tests
|
|
||||||
.env
|
|
||||||
|
|
||||||
# IDEs and editors
|
# IDEs and editors
|
||||||
.idea/
|
.idea/
|
||||||
.project
|
.project
|
||||||
@@ -33,8 +30,8 @@ build-cli
|
|||||||
.angular/cache
|
.angular/cache
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
coverage*
|
coverage
|
||||||
junit.xml*
|
junit.xml
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
*.crx
|
*.crx
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
# ESM Migration Plan
|
|
||||||
|
|
||||||
## Migration Status: Partial Success
|
|
||||||
|
|
||||||
The ESM migration has been **partially completed**. The source code is now ESM-compatible with `"type": "module"` in package.json, and webpack outputs CommonJS bundles (`.cjs`) for Node.js compatibility.
|
|
||||||
|
|
||||||
### What Works
|
|
||||||
|
|
||||||
- ✅ CLI build (`bwdc.cjs`) - builds and runs successfully
|
|
||||||
- ✅ Electron main process (`main.cjs`) - builds successfully
|
|
||||||
- ✅ All 130 tests pass
|
|
||||||
- ✅ Source code uses ESM syntax (import/export)
|
|
||||||
|
|
||||||
### What Doesn't Work
|
|
||||||
|
|
||||||
- ❌ Electron renderer build - **pre-existing type errors in jslib** (not caused by this migration)
|
|
||||||
|
|
||||||
The renderer build was failing with 37 TypeScript errors in `jslib/` **before** the ESM migration began. These are ArrayBuffer/SharedArrayBuffer type compatibility issues in the jslib submodule that need to be addressed separately.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Changes Made
|
|
||||||
|
|
||||||
### 1. package.json
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "module",
|
|
||||||
"main": "main.cjs"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. tsconfig.json
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"module": "ES2020",
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"noEmitOnError": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Webpack Configurations
|
|
||||||
|
|
||||||
**CLI (webpack.cli.cjs)**
|
|
||||||
|
|
||||||
- Output changed to `.cjs` extension
|
|
||||||
- Added `transpileOnly: true` to ts-loader for faster builds
|
|
||||||
|
|
||||||
**Main (webpack.main.cjs)**
|
|
||||||
|
|
||||||
- Output changed to `.cjs` extension
|
|
||||||
- Added `transpileOnly: true` to ts-loader
|
|
||||||
|
|
||||||
**Renderer (webpack.renderer.cjs)**
|
|
||||||
|
|
||||||
- Created separate `tsconfig.renderer.json` to isolate Angular compilation
|
|
||||||
- Removed ESM output experiments (not compatible with Angular's webpack plugin)
|
|
||||||
|
|
||||||
### 4. src-cli/package.json
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "module",
|
|
||||||
"bin": {
|
|
||||||
"bwdc": "../build-cli/bwdc.cjs"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. New File: tsconfig.renderer.json
|
|
||||||
|
|
||||||
Dedicated TypeScript config for Angular renderer to isolate from jslib type issues.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture Decision
|
|
||||||
|
|
||||||
### Why CJS Output Instead of ESM Output?
|
|
||||||
|
|
||||||
The migration uses a **hybrid approach**:
|
|
||||||
|
|
||||||
- **Source code**: ESM syntax (`import`/`export`)
|
|
||||||
- **Build output**: CommonJS (`.cjs` files)
|
|
||||||
|
|
||||||
This approach was chosen because:
|
|
||||||
|
|
||||||
1. **lowdb v1 incompatibility**: The legacy lowdb v1 used in jslib doesn't work properly with ESM output due to lodash interop issues
|
|
||||||
|
|
||||||
2. **Native module compatibility**: keytar and other native modules work better with CJS
|
|
||||||
|
|
||||||
3. **Electron compatibility**: Electron's main process ESM support is still maturing
|
|
||||||
|
|
||||||
4. **jslib constraints**: The jslib submodule is read-only and contains CJS-only patterns
|
|
||||||
|
|
||||||
The webpack bundler transpiles ESM source to CJS output, giving us modern syntax in the codebase while maintaining runtime compatibility.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Blocking Issues for Full ESM
|
|
||||||
|
|
||||||
### 1. jslib Submodule (Read-Only)
|
|
||||||
|
|
||||||
The jslib folder contains:
|
|
||||||
|
|
||||||
- `lowdb` v1.0.0 usage (CJS-only, v7 is ESM but has breaking API changes)
|
|
||||||
- `node-fetch` v2.7.0 usage (CJS-only, v3 is ESM-only)
|
|
||||||
- Pre-existing TypeScript errors (ArrayBuffer type mismatches)
|
|
||||||
|
|
||||||
### 2. Angular Webpack Plugin
|
|
||||||
|
|
||||||
The `@ngtools/webpack` plugin does its own TypeScript compilation and doesn't support `transpileOnly` mode, so it surfaces type errors from jslib.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Future Work
|
|
||||||
|
|
||||||
To complete full ESM migration:
|
|
||||||
|
|
||||||
1. **Update jslib submodule** - Fix type errors, upgrade to ESM-compatible dependencies
|
|
||||||
2. **Upgrade lowdb** - From v1 to v7 (requires rewriting storage layer)
|
|
||||||
3. **Remove node-fetch** - Use native `fetch` (Node 18+) or upgrade to v3
|
|
||||||
4. **Enable ESM output** - Once dependencies are updated, change webpack output to ESM
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing the Migration
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build CLI
|
|
||||||
npm run build:cli
|
|
||||||
node ./build-cli/bwdc.cjs --help
|
|
||||||
|
|
||||||
# Build Electron main
|
|
||||||
npm run build:main
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
npm test
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files Changed
|
|
||||||
|
|
||||||
| File | Change |
|
|
||||||
| ------------------------ | ---------------------------------------------------- |
|
|
||||||
| `package.json` | Added `"type": "module"`, changed main to `main.cjs` |
|
|
||||||
| `tsconfig.json` | Added `skipLibCheck`, `noEmitOnError` |
|
|
||||||
| `tsconfig.renderer.json` | New file for Angular compilation |
|
|
||||||
| `webpack.cli.cjs` | Output to `.cjs`, added `transpileOnly` |
|
|
||||||
| `webpack.main.cjs` | Output to `.cjs`, added `transpileOnly` |
|
|
||||||
| `webpack.renderer.cjs` | Use separate tsconfig |
|
|
||||||
| `src-cli/package.json` | Added `"type": "module"`, updated bin path |
|
|
||||||
@@ -11,8 +11,8 @@ services:
|
|||||||
- LDAP_TLS_KEY_FILE=/certs/openldap-key.pem
|
- LDAP_TLS_KEY_FILE=/certs/openldap-key.pem
|
||||||
- LDAP_TLS_CA_FILE=/certs/rootCA.pem
|
- LDAP_TLS_CA_FILE=/certs/rootCA.pem
|
||||||
volumes:
|
volumes:
|
||||||
- "./utils/openldap/ldifs:/ldifs"
|
- "./openldap/ldifs:/ldifs"
|
||||||
- "./utils/openldap/certs:/certs"
|
- "./openldap/certs:/certs"
|
||||||
ports:
|
ports:
|
||||||
- "1389:1389"
|
- "1389:1389"
|
||||||
- "1636:1636"
|
- "1636:1636"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
},
|
},
|
||||||
"productName": "Bitwarden Directory Connector",
|
"productName": "Bitwarden Directory Connector",
|
||||||
"appId": "com.bitwarden.directory-connector",
|
"appId": "com.bitwarden.directory-connector",
|
||||||
"copyright": "Copyright © 2015-2026 Bitwarden Inc.",
|
"copyright": "Copyright © 2015-2022 Bitwarden Inc.",
|
||||||
"directories": {
|
"directories": {
|
||||||
"buildResources": "resources",
|
"buildResources": "resources",
|
||||||
"output": "dist",
|
"output": "dist",
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
import eslint from "@eslint/js";
|
|
||||||
import tsParser from "@typescript-eslint/parser";
|
|
||||||
import tsPlugin from "@typescript-eslint/eslint-plugin";
|
|
||||||
import prettierConfig from "eslint-config-prettier";
|
|
||||||
import importPlugin from "eslint-plugin-import";
|
|
||||||
import rxjsX from "eslint-plugin-rxjs-x";
|
|
||||||
import rxjsAngularX from "eslint-plugin-rxjs-angular-x";
|
|
||||||
import angularEslint from "@angular-eslint/eslint-plugin-template";
|
|
||||||
import angularParser from "@angular-eslint/template-parser";
|
|
||||||
import globals from "globals";
|
|
||||||
|
|
||||||
export default [
|
|
||||||
// Global ignores (replaces .eslintignore)
|
|
||||||
{
|
|
||||||
ignores: [
|
|
||||||
"dist/**",
|
|
||||||
"dist-cli/**",
|
|
||||||
"build/**",
|
|
||||||
"build-cli/**",
|
|
||||||
"coverage/**",
|
|
||||||
"**/*.cjs",
|
|
||||||
"eslint.config.mjs",
|
|
||||||
"scripts/**/*.js",
|
|
||||||
"**/node_modules/**",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
// Base config for all JavaScript/TypeScript files
|
|
||||||
{
|
|
||||||
files: ["**/*.ts", "**/*.js"],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
sourceType: "module",
|
|
||||||
parser: tsParser,
|
|
||||||
parserOptions: {
|
|
||||||
project: ["./tsconfig.eslint.json"],
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
...globals.browser,
|
|
||||||
...globals.node,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
"@typescript-eslint": tsPlugin,
|
|
||||||
import: importPlugin,
|
|
||||||
"rxjs-x": rxjsX,
|
|
||||||
"rxjs-angular-x": rxjsAngularX,
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
"import/parsers": {
|
|
||||||
"@typescript-eslint/parser": [".ts"],
|
|
||||||
},
|
|
||||||
"import/resolver": {
|
|
||||||
typescript: {
|
|
||||||
alwaysTryTypes: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
// ESLint recommended rules
|
|
||||||
...eslint.configs.recommended.rules,
|
|
||||||
|
|
||||||
// TypeScript ESLint recommended rules
|
|
||||||
...tsPlugin.configs.recommended.rules,
|
|
||||||
|
|
||||||
// Import plugin recommended rules
|
|
||||||
...importPlugin.flatConfigs.recommended.rules,
|
|
||||||
|
|
||||||
// RxJS recommended rules
|
|
||||||
...rxjsX.configs.recommended.rules,
|
|
||||||
|
|
||||||
// Custom project rules
|
|
||||||
"@typescript-eslint/explicit-member-accessibility": ["error", { accessibility: "no-public" }],
|
|
||||||
"@typescript-eslint/no-explicit-any": "off", // TODO: This should be re-enabled
|
|
||||||
"@typescript-eslint/no-misused-promises": ["error", { checksVoidReturn: false }],
|
|
||||||
"@typescript-eslint/no-this-alias": ["error", { allowedNames: ["self"] }],
|
|
||||||
"@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
|
|
||||||
"no-console": "error",
|
|
||||||
"import/no-unresolved": "off", // TODO: Look into turning on once each package is an actual package.
|
|
||||||
"import/order": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
alphabetize: {
|
|
||||||
order: "asc",
|
|
||||||
},
|
|
||||||
"newlines-between": "always",
|
|
||||||
pathGroups: [
|
|
||||||
{
|
|
||||||
pattern: "@/jslib/**/*",
|
|
||||||
group: "external",
|
|
||||||
position: "after",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: "@/src/**/*",
|
|
||||||
group: "parent",
|
|
||||||
position: "before",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
pathGroupsExcludedImportTypes: ["builtin"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"rxjs-angular-x/prefer-takeuntil": "error",
|
|
||||||
"rxjs-x/no-exposed-subjects": ["error", { allowProtected: true }],
|
|
||||||
"no-restricted-syntax": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
message: "Calling `svgIcon` directly is not allowed",
|
|
||||||
selector: "CallExpression[callee.name='svgIcon']",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "Accessing FormGroup using `get` is not allowed, use `.value` instead",
|
|
||||||
selector:
|
|
||||||
"ChainExpression[expression.object.callee.property.name='get'][expression.property.name='value']",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
curly: ["error", "all"],
|
|
||||||
"import/namespace": ["off"], // This doesn't resolve namespace imports correctly, but TS will throw for this anyway
|
|
||||||
"no-restricted-imports": ["error", { patterns: ["src/**/*"] }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Jest test files (includes any test-related files)
|
|
||||||
{
|
|
||||||
files: ["**/*.spec.ts", "**/test.setup.ts", "**/spec/**/*.ts", "**/utils/**/*fixtures*.ts"],
|
|
||||||
languageOptions: {
|
|
||||||
globals: {
|
|
||||||
...globals.jest,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Angular HTML templates
|
|
||||||
{
|
|
||||||
files: ["**/*.html"],
|
|
||||||
languageOptions: {
|
|
||||||
parser: angularParser,
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
"@angular-eslint/template": angularEslint,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"@angular-eslint/template/button-has-type": "error",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Prettier config (must be last to override other configs)
|
|
||||||
prettierConfig,
|
|
||||||
];
|
|
||||||
@@ -24,20 +24,14 @@ module.exports = {
|
|||||||
|
|
||||||
roots: ["<rootDir>"],
|
roots: ["<rootDir>"],
|
||||||
modulePaths: [compilerOptions.baseUrl],
|
modulePaths: [compilerOptions.baseUrl],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: "<rootDir>/" }),
|
||||||
...pathsToModuleNameMapper(compilerOptions.paths, { prefix: "<rootDir>/" }),
|
|
||||||
// ESM compatibility: mock import.meta.url for tests
|
|
||||||
"^(\\.{1,2}/.*)\\.js$": "$1",
|
|
||||||
},
|
|
||||||
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
||||||
|
|
||||||
// Workaround for a memory leak that crashes tests in CI:
|
// Workaround for a memory leak that crashes tests in CI:
|
||||||
// https://github.com/facebook/jest/issues/9430#issuecomment-1149882002
|
// https://github.com/facebook/jest/issues/9430#issuecomment-1149882002
|
||||||
// Also anecdotally improves performance when run locally
|
// Also anecdotally improves performance when run locally
|
||||||
maxWorkers: 3,
|
maxWorkers: 3,
|
||||||
|
|
||||||
// ESM support
|
|
||||||
extensionsToTreatAsEsm: [".ts"],
|
|
||||||
|
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.tsx?$": [
|
"^.+\\.tsx?$": [
|
||||||
"jest-preset-angular",
|
"jest-preset-angular",
|
||||||
@@ -50,8 +44,6 @@ module.exports = {
|
|||||||
// Makes tests run faster and reduces size/rate of leak, but loses typechecking on test code
|
// Makes tests run faster and reduces size/rate of leak, but loses typechecking on test code
|
||||||
// See https://bitwarden.atlassian.net/browse/EC-497 for more info
|
// See https://bitwarden.atlassian.net/browse/EC-497 for more info
|
||||||
isolatedModules: true,
|
isolatedModules: true,
|
||||||
// ESM support
|
|
||||||
useESM: true,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { InjectOptions, Injector, ProviderToken } from "@angular/core";
|
import { InjectFlags, InjectOptions, Injector, ProviderToken } from "@angular/core";
|
||||||
|
|
||||||
export class ModalInjector implements Injector {
|
export class ModalInjector implements Injector {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -12,7 +12,8 @@ export class ModalInjector implements Injector {
|
|||||||
options: InjectOptions & { optional?: false },
|
options: InjectOptions & { optional?: false },
|
||||||
): T;
|
): T;
|
||||||
get<T>(token: ProviderToken<T>, notFoundValue: null, options: InjectOptions): T;
|
get<T>(token: ProviderToken<T>, notFoundValue: null, options: InjectOptions): T;
|
||||||
get<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions): T;
|
get<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions | InjectFlags): T;
|
||||||
|
get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
|
||||||
get(token: any, notFoundValue?: any): any;
|
get(token: any, notFoundValue?: any): any;
|
||||||
get(token: any, notFoundValue?: any, flags?: any): any {
|
get(token: any, notFoundValue?: any, flags?: any): any {
|
||||||
return this._additionalTokens.get(token) ?? this._parentInjector.get<any>(token, notFoundValue);
|
return this._additionalTokens.get(token) ?? this._parentInjector.get<any>(token, notFoundValue);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { lastValueFrom, Observable, Subject } from "rxjs";
|
import { Observable, Subject } from "rxjs";
|
||||||
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
export class ModalRef {
|
export class ModalRef {
|
||||||
onCreated: Observable<HTMLElement>; // Modal added to the DOM.
|
onCreated: Observable<HTMLElement>; // Modal added to the DOM.
|
||||||
@@ -44,6 +45,6 @@ export class ModalRef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onClosedPromise(): Promise<any> {
|
onClosedPromise(): Promise<any> {
|
||||||
return lastValueFrom(this.onClosed);
|
return this.onClosed.pipe(first()).toPromise();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Directive, ElementRef, Input, NgZone } from "@angular/core";
|
import { Directive, ElementRef, Input, NgZone } from "@angular/core";
|
||||||
import { take } from "rxjs";
|
import { take } from "rxjs/operators";
|
||||||
|
|
||||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
import { Utils } from "@/jslib/common/src/misc/utils";
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
Type,
|
Type,
|
||||||
ViewContainerRef,
|
ViewContainerRef,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { first, firstValueFrom } from "rxjs";
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
import { DynamicModalComponent } from "../components/modal/dynamic-modal.component";
|
import { DynamicModalComponent } from "../components/modal/dynamic-modal.component";
|
||||||
import { ModalInjector } from "../components/modal/modal-injector";
|
import { ModalInjector } from "../components/modal/modal-injector";
|
||||||
@@ -58,7 +58,7 @@ export class ModalService {
|
|||||||
|
|
||||||
viewContainerRef.insert(modalComponentRef.hostView);
|
viewContainerRef.insert(modalComponentRef.hostView);
|
||||||
|
|
||||||
await firstValueFrom(modalRef.onCreated);
|
await modalRef.onCreated.pipe(first()).toPromise();
|
||||||
|
|
||||||
return [modalRef, modalComponentRef.instance.componentRef.instance];
|
return [modalRef, modalComponentRef.instance.componentRef.instance];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,15 @@ declare let console: any;
|
|||||||
export function interceptConsole(interceptions: any): object {
|
export function interceptConsole(interceptions: any): object {
|
||||||
console = {
|
console = {
|
||||||
log: function () {
|
log: function () {
|
||||||
|
// eslint-disable-next-line
|
||||||
interceptions.log = arguments;
|
interceptions.log = arguments;
|
||||||
},
|
},
|
||||||
warn: function () {
|
warn: function () {
|
||||||
|
// eslint-disable-next-line
|
||||||
interceptions.warn = arguments;
|
interceptions.warn = arguments;
|
||||||
},
|
},
|
||||||
error: function () {
|
error: function () {
|
||||||
|
// eslint-disable-next-line
|
||||||
interceptions.error = arguments;
|
interceptions.error = arguments;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
/* eslint-disable no-useless-escape */
|
/* eslint-disable no-useless-escape */
|
||||||
import * as url from "url";
|
|
||||||
|
|
||||||
import { I18nService } from "../abstractions/i18n.service";
|
import { I18nService } from "../abstractions/i18n.service";
|
||||||
|
|
||||||
import * as tldjs from "tldjs";
|
import * as tldjs from "tldjs";
|
||||||
|
|
||||||
const nodeURL = typeof window === "undefined" ? url : null;
|
const nodeURL = typeof window === "undefined" ? require("url") : null;
|
||||||
|
|
||||||
export class Utils {
|
export class Utils {
|
||||||
static inited = false;
|
static inited = false;
|
||||||
@@ -249,7 +247,7 @@ export class Utils {
|
|||||||
const urlDomain =
|
const urlDomain =
|
||||||
tldjs != null && tldjs.getDomain != null ? tldjs.getDomain(url.hostname) : null;
|
tldjs != null && tldjs.getDomain != null ? tldjs.getDomain(url.hostname) : null;
|
||||||
return urlDomain != null ? urlDomain : url.hostname;
|
return urlDomain != null ? urlDomain : url.hostname;
|
||||||
} catch {
|
} catch (e) {
|
||||||
// Invalid domain, try another approach below.
|
// Invalid domain, try another approach below.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -397,7 +395,7 @@ export class Utils {
|
|||||||
anchor.href = uriString;
|
anchor.href = uriString;
|
||||||
return anchor as any;
|
return anchor as any;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (e) {
|
||||||
// Ignore error
|
// Ignore error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export class EncString {
|
|||||||
try {
|
try {
|
||||||
this.encryptionType = parseInt(headerPieces[0], null);
|
this.encryptionType = parseInt(headerPieces[0], null);
|
||||||
encPieces = headerPieces[1].split("|");
|
encPieces = headerPieces[1].split("|");
|
||||||
} catch {
|
} catch (e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -114,7 +114,7 @@ export class EncString {
|
|||||||
key = await cryptoService.getOrgKey(orgId);
|
key = await cryptoService.getOrgKey(orgId);
|
||||||
}
|
}
|
||||||
this.decryptedValue = await cryptoService.decryptToUtf8(this, key);
|
this.decryptedValue = await cryptoService.decryptToUtf8(this, key);
|
||||||
} catch {
|
} catch (e) {
|
||||||
this.decryptedValue = "[error: cannot decrypt]";
|
this.decryptedValue = "[error: cannot decrypt]";
|
||||||
}
|
}
|
||||||
return this.decryptedValue;
|
return this.decryptedValue;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ClientType } from "../../../enums/clientType";
|
import { ClientType } from "../../../enums/clientType";
|
||||||
|
import { Utils } from "../../../misc/utils";
|
||||||
import { CaptchaProtectedRequest } from "../captchaProtectedRequest";
|
import { CaptchaProtectedRequest } from "../captchaProtectedRequest";
|
||||||
import { DeviceRequest } from "../deviceRequest";
|
import { DeviceRequest } from "../deviceRequest";
|
||||||
|
|
||||||
@@ -29,4 +30,5 @@ export class PasswordTokenRequest extends TokenRequest implements CaptchaProtect
|
|||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export abstract class TokenRequest {
|
|||||||
this.device = device != null ? device : null;
|
this.device = device != null ? device : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
alterIdentityTokenHeaders(headers: Headers) {
|
alterIdentityTokenHeaders(headers: Headers) {
|
||||||
// Implemented in subclass if required
|
// Implemented in subclass if required
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,11 +335,9 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async clearStoredKey(keySuffix: KeySuffixOptions) {
|
async clearStoredKey(keySuffix: KeySuffixOptions) {
|
||||||
if (keySuffix === KeySuffixOptions.Auto) {
|
keySuffix === KeySuffixOptions.Auto
|
||||||
await this.stateService.setCryptoMasterKeyAuto(null);
|
? await this.stateService.setCryptoMasterKeyAuto(null)
|
||||||
} else {
|
: await this.stateService.setCryptoMasterKeyBiometric(null);
|
||||||
await this.stateService.setCryptoMasterKeyBiometric(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearKeyHash(userId?: string): Promise<any> {
|
async clearKeyHash(userId?: string): Promise<any> {
|
||||||
@@ -719,7 +717,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
|
|
||||||
const privateKey = await this.decryptToBytes(new EncString(encPrivateKey), encKey);
|
const privateKey = await this.decryptToBytes(new EncString(encPrivateKey), encKey);
|
||||||
await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
|
await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
|
||||||
} catch {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ const partialKeys = {
|
|||||||
export class StateService<
|
export class StateService<
|
||||||
TGlobalState extends GlobalState = GlobalState,
|
TGlobalState extends GlobalState = GlobalState,
|
||||||
TAccount extends Account = Account,
|
TAccount extends Account = Account,
|
||||||
> implements StateServiceAbstraction<TAccount> {
|
> implements StateServiceAbstraction<TAccount>
|
||||||
|
{
|
||||||
protected accountsSubject = new BehaviorSubject<{ [userId: string]: TAccount }>({});
|
protected accountsSubject = new BehaviorSubject<{ [userId: string]: TAccount }>({});
|
||||||
accounts$ = this.accountsSubject.asObservable();
|
accounts$ = this.accountsSubject.asObservable();
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
import {
|
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, nativeImage, Tray } from "electron";
|
||||||
app,
|
|
||||||
BrowserWindow,
|
|
||||||
Menu,
|
|
||||||
MenuItemConstructorOptions,
|
|
||||||
NativeImage,
|
|
||||||
nativeImage,
|
|
||||||
Tray,
|
|
||||||
} from "electron";
|
|
||||||
|
|
||||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||||
@@ -20,8 +12,8 @@ export class TrayMain {
|
|||||||
|
|
||||||
private appName: string;
|
private appName: string;
|
||||||
private tray: Tray;
|
private tray: Tray;
|
||||||
private icon: string | NativeImage;
|
private icon: string | Electron.NativeImage;
|
||||||
private pressedIcon: NativeImage;
|
private pressedIcon: Electron.NativeImage;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private windowMain: WindowMain,
|
private windowMain: WindowMain,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import * as url from "url";
|
import * as url from "url";
|
||||||
|
|
||||||
import { app, BrowserWindow, Rectangle, screen } from "electron";
|
import { app, BrowserWindow, screen } from "electron";
|
||||||
|
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||||
@@ -14,7 +14,7 @@ export class WindowMain {
|
|||||||
win: BrowserWindow;
|
win: BrowserWindow;
|
||||||
isQuitting = false;
|
isQuitting = false;
|
||||||
|
|
||||||
private windowStateChangeTimer: ReturnType<typeof setTimeout>;
|
private windowStateChangeTimer: NodeJS.Timeout;
|
||||||
private windowStates: { [key: string]: any } = {};
|
private windowStates: { [key: string]: any } = {};
|
||||||
private enableAlwaysOnTop = false;
|
private enableAlwaysOnTop = false;
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@ export class WindowMain {
|
|||||||
app.quit();
|
app.quit();
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
// eslint-disable-next-line
|
||||||
app.on("second-instance", (event, argv, workingDirectory) => {
|
app.on("second-instance", (event, argv, workingDirectory) => {
|
||||||
// Someone tried to run a second instance, we should focus our window.
|
// Someone tried to run a second instance, we should focus our window.
|
||||||
if (this.win != null) {
|
if (this.win != null) {
|
||||||
@@ -240,7 +241,7 @@ export class WindowMain {
|
|||||||
const state = await this.stateService.getWindow();
|
const state = await this.stateService.getWindow();
|
||||||
|
|
||||||
const isValid = state != null && (this.stateHasBounds(state) || state.isMaximized);
|
const isValid = state != null && (this.stateHasBounds(state) || state.isMaximized);
|
||||||
let displayBounds: Rectangle = null;
|
let displayBounds: Electron.Rectangle = null;
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
state.width = defaultWidth;
|
state.width = defaultWidth;
|
||||||
state.height = defaultHeight;
|
state.height = defaultHeight;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { GroupEntry } from "@/src/models/groupEntry";
|
import { GroupEntry } from "../src/models/groupEntry";
|
||||||
|
|
||||||
// These must match the ldap server seed data in directory.ldif
|
// These must match the ldap server seed data in directory.ldif
|
||||||
const data: Jsonify<GroupEntry>[] = [
|
const data: Jsonify<GroupEntry>[] = [
|
||||||
10
openldap/mkcert.sh
Executable file
10
openldap/mkcert.sh
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
if ! [ -x "$(command -v mkcert)" ]; then
|
||||||
|
echo 'Error: mkcert is not installed. Install mkcert first and then re-run this script.'
|
||||||
|
echo 'e.g. brew install mkcert'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkcert -install
|
||||||
|
mkdir -p ./openldap/certs
|
||||||
|
cp "$(mkcert -CAROOT)/rootCA.pem" ./openldap/certs/rootCA.pem
|
||||||
|
mkcert -key-file ./openldap/certs/openldap-key.pem -cert-file ./openldap/certs/openldap.pem localhost openldap
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { UserEntry } from "@/src/models/userEntry";
|
import { UserEntry } from "../src/models/userEntry";
|
||||||
|
|
||||||
// These must match the ldap server seed data in directory.ldif
|
// These must match the ldap server seed data in directory.ldif
|
||||||
const data: Jsonify<UserEntry>[] = [
|
const data: Jsonify<UserEntry>[] = [
|
||||||
11792
package-lock.json
generated
11792
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
96
package.json
96
package.json
@@ -2,8 +2,7 @@
|
|||||||
"name": "@bitwarden/directory-connector",
|
"name": "@bitwarden/directory-connector",
|
||||||
"productName": "Bitwarden Directory Connector",
|
"productName": "Bitwarden Directory Connector",
|
||||||
"description": "Sync your user directory to your Bitwarden organization.",
|
"description": "Sync your user directory to your Bitwarden organization.",
|
||||||
"version": "2025.12.0",
|
"version": "2025.9.0",
|
||||||
"type": "module",
|
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"bitwarden",
|
"bitwarden",
|
||||||
"password",
|
"password",
|
||||||
@@ -17,7 +16,7 @@
|
|||||||
"url": "https://github.com/bitwarden/directory-connector"
|
"url": "https://github.com/bitwarden/directory-connector"
|
||||||
},
|
},
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"main": "main.cjs",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"sub:init": "git submodule update --init --recursive",
|
"sub:init": "git submodule update --init --recursive",
|
||||||
"sub:update": "git submodule update --remote",
|
"sub:update": "git submodule update --remote",
|
||||||
@@ -32,14 +31,14 @@
|
|||||||
"lint": "eslint . && prettier --check .",
|
"lint": "eslint . && prettier --check .",
|
||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"build": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\"",
|
"build": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\"",
|
||||||
"build:main": "webpack --config webpack.main.cjs",
|
"build:main": "webpack --config webpack.main.js",
|
||||||
"build:renderer": "webpack --config webpack.renderer.cjs",
|
"build:renderer": "webpack --config webpack.renderer.js",
|
||||||
"build:renderer:watch": "webpack --config webpack.renderer.cjs --watch",
|
"build:renderer:watch": "webpack --config webpack.renderer.js --watch",
|
||||||
"build:dist": "npm run reset && npm run rebuild && npm run build",
|
"build:dist": "npm run reset && npm run rebuild && npm run build",
|
||||||
"build:cli": "webpack --config webpack.cli.cjs",
|
"build:cli": "webpack --config webpack.cli.js",
|
||||||
"build:cli:watch": "webpack --config webpack.cli.cjs --watch",
|
"build:cli:watch": "webpack --config webpack.cli.js --watch",
|
||||||
"build:cli:prod": "cross-env NODE_ENV=production webpack --config webpack.cli.cjs",
|
"build:cli:prod": "cross-env NODE_ENV=production webpack --config webpack.cli.js",
|
||||||
"build:cli:prod:watch": "cross-env NODE_ENV=production webpack --config webpack.cli.cjs --watch",
|
"build:cli:prod:watch": "cross-env NODE_ENV=production webpack --config webpack.cli.js --watch",
|
||||||
"electron": "npm run build:main && concurrently -k -n Main,Rend -c yellow,cyan \"electron --inspect=5858 ./build --watch\" \"npm run build:renderer:watch\"",
|
"electron": "npm run build:main && concurrently -k -n Main,Rend -c yellow,cyan \"electron --inspect=5858 ./build --watch\" \"npm run build:renderer:watch\"",
|
||||||
"electron:ignore": "npm run build:main && concurrently -k -n Main,Rend -c yellow,cyan \"electron --inspect=5858 --ignore-certificate-errors ./build --watch\" \"npm run build:renderer:watch\"",
|
"electron:ignore": "npm run build:main && concurrently -k -n Main,Rend -c yellow,cyan \"electron --inspect=5858 --ignore-certificate-errors ./build --watch\" \"npm run build:renderer:watch\"",
|
||||||
"clean:dist": "rimraf --glob ./dist/*",
|
"clean:dist": "rimraf --glob ./dist/*",
|
||||||
@@ -50,7 +49,7 @@
|
|||||||
"pack:win:ci": "npm run clean:dist && electron-builder --win --x64 --ia32 -p never",
|
"pack:win:ci": "npm run clean:dist && electron-builder --win --x64 --ia32 -p never",
|
||||||
"pack:cli": "npm run pack:cli:win | npm run pack:cli:mac | npm run pack:cli:lin",
|
"pack:cli": "npm run pack:cli:win | npm run pack:cli:mac | npm run pack:cli:lin",
|
||||||
"pack:cli:win": "pkg ./src-cli --targets win-x64 --output ./dist-cli/windows/bwdc.exe",
|
"pack:cli:win": "pkg ./src-cli --targets win-x64 --output ./dist-cli/windows/bwdc.exe",
|
||||||
"pack:cli:mac": "pkg ./src-cli --targets macos-x64 --output ./dist-cli/macos/bwdc",
|
"pack:cli:mac": "pkg ./src-cli --options experimental-vm-modules --targets macos-x64 --output ./dist-cli/macos/bwdc",
|
||||||
"pack:cli:lin": "pkg ./src-cli --targets linux-x64 --output ./dist-cli/linux/bwdc",
|
"pack:cli:lin": "pkg ./src-cli --targets linux-x64 --output ./dist-cli/linux/bwdc",
|
||||||
"dist:lin": "npm run build:dist && npm run pack:lin",
|
"dist:lin": "npm run build:dist && npm run pack:lin",
|
||||||
"dist:mac": "npm run build:dist && npm run pack:mac",
|
"dist:mac": "npm run build:dist && npm run pack:mac",
|
||||||
@@ -70,30 +69,29 @@
|
|||||||
"test:watch:all": "jest --watchAll --testPathIgnorePatterns=.integration.spec.ts",
|
"test:watch:all": "jest --watchAll --testPathIgnorePatterns=.integration.spec.ts",
|
||||||
"test:integration": "jest .integration.spec.ts",
|
"test:integration": "jest .integration.spec.ts",
|
||||||
"test:integration:watch": "jest .integration.spec.ts --watch",
|
"test:integration:watch": "jest .integration.spec.ts --watch",
|
||||||
"test:integration:setup": "sh ./utils/openldap/mkcert.sh && docker compose up -d",
|
"test:integration:setup": "sh ./openldap/mkcert.sh && docker compose up -d",
|
||||||
"test:types": "npx tsc --noEmit"
|
"test:types": "npx tsc --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "20.3.3",
|
"@angular-devkit/build-angular": "19.2.15",
|
||||||
"@angular-eslint/eslint-plugin-template": "20.7.0",
|
"@angular-eslint/eslint-plugin-template": "19.8.0",
|
||||||
"@angular-eslint/template-parser": "20.7.0",
|
"@angular-eslint/template-parser": "19.8.0",
|
||||||
"@angular/compiler-cli": "20.3.15",
|
"@angular/compiler-cli": "19.2.14",
|
||||||
"@electron/notarize": "2.5.0",
|
"@electron/notarize": "2.5.0",
|
||||||
"@electron/rebuild": "4.0.1",
|
"@electron/rebuild": "4.0.1",
|
||||||
"@fluffy-spoon/substitute": "1.208.0",
|
"@fluffy-spoon/substitute": "1.208.0",
|
||||||
"@microsoft/microsoft-graph-types": "2.43.1",
|
"@microsoft/microsoft-graph-types": "2.40.0",
|
||||||
"@ngtools/webpack": "20.3.3",
|
"@ngtools/webpack": "19.2.14",
|
||||||
"@types/inquirer": "8.2.10",
|
"@types/inquirer": "8.2.10",
|
||||||
"@types/jest": "29.5.14",
|
"@types/jest": "29.5.14",
|
||||||
"@types/lowdb": "1.0.15",
|
"@types/lowdb": "1.0.15",
|
||||||
"@types/node": "22.19.2",
|
"@types/node": "22.18.1",
|
||||||
"@types/node-fetch": "2.6.12",
|
"@types/node-fetch": "2.6.12",
|
||||||
"@types/node-forge": "1.3.11",
|
"@types/node-forge": "1.3.11",
|
||||||
"@types/proper-lockfile": "4.1.4",
|
"@types/proper-lockfile": "4.1.4",
|
||||||
"@types/semver": "7.7.1",
|
|
||||||
"@types/tldjs": "2.3.4",
|
"@types/tldjs": "2.3.4",
|
||||||
"@typescript-eslint/eslint-plugin": "8.50.0",
|
"@typescript-eslint/eslint-plugin": "8.43.0",
|
||||||
"@typescript-eslint/parser": "8.50.0",
|
"@typescript-eslint/parser": "8.43.0",
|
||||||
"@yao-pkg/pkg": "5.16.1",
|
"@yao-pkg/pkg": "5.16.1",
|
||||||
"clean-webpack-plugin": "4.0.0",
|
"clean-webpack-plugin": "4.0.0",
|
||||||
"concurrently": "9.2.0",
|
"concurrently": "9.2.0",
|
||||||
@@ -101,73 +99,75 @@
|
|||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"css-loader": "7.1.2",
|
"css-loader": "7.1.2",
|
||||||
"dotenv": "17.2.0",
|
"dotenv": "17.2.0",
|
||||||
"electron": "39.2.1",
|
"electron": "38.1.0",
|
||||||
"electron-builder": "24.13.3",
|
"electron-builder": "24.13.3",
|
||||||
"electron-log": "5.4.1",
|
"electron-log": "5.4.1",
|
||||||
"electron-reload": "2.0.0-alpha.1",
|
"electron-reload": "2.0.0-alpha.1",
|
||||||
"electron-store": "8.2.0",
|
"electron-store": "8.2.0",
|
||||||
"electron-updater": "6.6.2",
|
"electron-updater": "6.6.2",
|
||||||
"eslint": "9.39.1",
|
"eslint": "8.57.1",
|
||||||
"eslint-config-prettier": "10.1.5",
|
"eslint-config-prettier": "10.1.5",
|
||||||
"eslint-import-resolver-typescript": "4.4.4",
|
"eslint-import-resolver-typescript": "4.4.4",
|
||||||
"eslint-plugin-import": "2.32.0",
|
"eslint-plugin-import": "2.32.0",
|
||||||
"eslint-plugin-rxjs-angular-x": "0.1.0",
|
"eslint-plugin-rxjs": "5.0.3",
|
||||||
"eslint-plugin-rxjs-x": "0.8.3",
|
"eslint-plugin-rxjs-angular": "2.0.1",
|
||||||
"form-data": "4.0.4",
|
"form-data": "4.0.4",
|
||||||
"glob": "13.0.0",
|
|
||||||
"html-loader": "5.1.0",
|
"html-loader": "5.1.0",
|
||||||
"html-webpack-plugin": "5.6.3",
|
"html-webpack-plugin": "5.6.3",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-junit": "16.0.0",
|
"jest-junit": "16.0.0",
|
||||||
"jest-mock-extended": "4.0.0",
|
"jest-mock-extended": "3.0.7",
|
||||||
"jest-preset-angular": "14.6.0",
|
"jest-preset-angular": "14.6.0",
|
||||||
"lint-staged": "16.2.6",
|
"lint-staged": "16.1.2",
|
||||||
"mini-css-extract-plugin": "2.9.2",
|
"mini-css-extract-plugin": "2.9.2",
|
||||||
"minimatch": "5.1.2",
|
"node-abi": "3.77.0",
|
||||||
"node-forge": "1.3.2",
|
"node-forge": "1.3.1",
|
||||||
"node-loader": "2.1.0",
|
"node-loader": "2.1.0",
|
||||||
"prettier": "3.7.4",
|
"prettier": "3.6.2",
|
||||||
"rimraf": "6.1.0",
|
"rimraf": "6.0.1",
|
||||||
"rxjs": "7.8.2",
|
"rxjs": "7.8.2",
|
||||||
"sass": "1.97.1",
|
"sass": "1.92.1",
|
||||||
"sass-loader": "16.0.5",
|
"sass-loader": "16.0.5",
|
||||||
"ts-jest": "29.4.1",
|
"ts-jest": "29.4.1",
|
||||||
"ts-loader": "9.5.2",
|
"ts-loader": "9.5.2",
|
||||||
"tsconfig-paths-webpack-plugin": "4.2.0",
|
"tsconfig-paths-webpack-plugin": "4.2.0",
|
||||||
"type-fest": "5.3.0",
|
"type-fest": "4.41.0",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"webpack": "5.104.1",
|
"webpack": "5.101.0",
|
||||||
"webpack-cli": "6.0.1",
|
"webpack-cli": "6.0.1",
|
||||||
"webpack-merge": "6.0.1",
|
"webpack-merge": "6.0.1",
|
||||||
"webpack-node-externals": "3.0.0",
|
"webpack-node-externals": "3.0.0",
|
||||||
"zone.js": "0.15.1"
|
"zone.js": "0.15.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "20.3.15",
|
"@angular/animations": "19.2.14",
|
||||||
"@angular/cdk": "20.2.14",
|
"@angular/cdk": "19.2.14",
|
||||||
"@angular/cli": "20.3.3",
|
"@angular/cli": "19.2.14",
|
||||||
"@angular/common": "20.3.15",
|
"@angular/common": "19.2.14",
|
||||||
"@angular/compiler": "20.3.15",
|
"@angular/compiler": "19.2.14",
|
||||||
"@angular/core": "20.3.15",
|
"@angular/core": "19.2.14",
|
||||||
"@angular/forms": "20.3.15",
|
"@angular/forms": "19.2.14",
|
||||||
"@angular/platform-browser": "20.3.15",
|
"@angular/platform-browser": "19.2.14",
|
||||||
"@angular/platform-browser-dynamic": "20.3.15",
|
"@angular/platform-browser-dynamic": "19.2.14",
|
||||||
"@angular/router": "20.3.15",
|
"@angular/router": "19.2.14",
|
||||||
"@microsoft/microsoft-graph-client": "3.0.7",
|
"@microsoft/microsoft-graph-client": "3.0.7",
|
||||||
"big-integer": "1.6.52",
|
"big-integer": "1.6.52",
|
||||||
"bootstrap": "5.3.7",
|
"bootstrap": "5.3.7",
|
||||||
"browser-hrtime": "1.1.8",
|
"browser-hrtime": "1.1.8",
|
||||||
"chalk": "4.1.2",
|
"chalk": "4.1.2",
|
||||||
"commander": "14.0.0",
|
"commander": "14.0.0",
|
||||||
|
"core-js": "3.44.0",
|
||||||
"form-data": "4.0.4",
|
"form-data": "4.0.4",
|
||||||
"googleapis": "149.0.0",
|
"google-auth-library": "10.3.0",
|
||||||
|
"googleapis": "153.0.0",
|
||||||
|
"googleapis-common": "8.0.0",
|
||||||
"https-proxy-agent": "7.0.6",
|
"https-proxy-agent": "7.0.6",
|
||||||
"inquirer": "8.2.6",
|
"inquirer": "8.2.6",
|
||||||
"keytar": "7.9.0",
|
"keytar": "7.9.0",
|
||||||
"ldapts": "8.0.1",
|
"ldapts": "8.0.1",
|
||||||
"lowdb": "1.0.0",
|
"lowdb": "1.0.0",
|
||||||
"ngx-toastr": "19.1.0",
|
"ngx-toastr": "19.0.0",
|
||||||
"node-fetch": "2.7.0",
|
"node-fetch": "2.7.0",
|
||||||
"parse5": "8.0.0",
|
"parse5": "8.0.0",
|
||||||
"proper-lockfile": "4.1.2",
|
"proper-lockfile": "4.1.2",
|
||||||
|
|||||||
@@ -3,17 +3,16 @@
|
|||||||
"productName": "Bitwarden Directory Connector",
|
"productName": "Bitwarden Directory Connector",
|
||||||
"description": "Sync your user directory to your Bitwarden organization.",
|
"description": "Sync your user directory to your Bitwarden organization.",
|
||||||
"version": "2.9.5",
|
"version": "2.9.5",
|
||||||
"type": "module",
|
|
||||||
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
||||||
"homepage": "https://bitwarden.com",
|
"homepage": "https://bitwarden.com",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"main": "main.mjs",
|
"main": "main.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/bitwarden/directory-connector"
|
"url": "https://github.com/bitwarden/directory-connector"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"bwdc": "../build-cli/bwdc.cjs"
|
"bwdc": "../build-cli/bwdc.js"
|
||||||
},
|
},
|
||||||
"pkg": {
|
"pkg": {
|
||||||
"assets": "../build-cli/**/*"
|
"assets": "../build-cli/**/*"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DirectoryType } from "@/src/enums/directoryType";
|
import { DirectoryType } from "@/src/enums/directoryType";
|
||||||
import { IDirectoryService } from "@/src/services/directory-services/directory.service";
|
import { IDirectoryService } from "@/src/services/directory.service";
|
||||||
|
|
||||||
export abstract class DirectoryFactoryService {
|
export abstract class DirectoryFactoryService {
|
||||||
abstract createService(type: DirectoryType): IDirectoryService;
|
abstract createService(type: DirectoryType): IDirectoryService;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { EnvironmentComponent } from "./environment.component";
|
|||||||
// The only subscription in this component is closed from a child component, confusing eslint.
|
// The only subscription in this component is closed from a child component, confusing eslint.
|
||||||
// https://github.com/cartant/eslint-plugin-rxjs-angular/blob/main/docs/rules/prefer-takeuntil.md
|
// https://github.com/cartant/eslint-plugin-rxjs-angular/blob/main/docs/rules/prefer-takeuntil.md
|
||||||
//
|
//
|
||||||
// eslint-disable-next-line rxjs-angular-x/prefer-takeuntil
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
export class ApiKeyComponent {
|
export class ApiKeyComponent {
|
||||||
@ViewChild("environment", { read: ViewContainerRef, static: true })
|
@ViewChild("environment", { read: ViewContainerRef, static: true })
|
||||||
environmentModal: ViewContainerRef;
|
environmentModal: ViewContainerRef;
|
||||||
@@ -100,7 +100,7 @@ export class ApiKeyComponent {
|
|||||||
this.environmentModal,
|
this.environmentModal,
|
||||||
);
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line rxjs-angular-x/prefer-takeuntil
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
childComponent.onSaved.pipe(takeUntil(modalRef.onClosed)).subscribe(() => {
|
childComponent.onSaved.pipe(takeUntil(modalRef.onClosed)).subscribe(() => {
|
||||||
modalRef.close();
|
modalRef.close();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// core-js is required for bwdc cli which appears to require these pollyfills for dynamic imports
|
||||||
|
// see https://github.com/bitwarden/directory-connector/issues/878
|
||||||
|
import "core-js/stable";
|
||||||
import "zone.js";
|
import "zone.js";
|
||||||
|
|
||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
|||||||
|
|
||||||
import { isDev } from "@/jslib/electron/src/utils";
|
import { isDev } from "@/jslib/electron/src/utils";
|
||||||
|
|
||||||
import "../scss/styles.scss";
|
// tslint:disable-next-line
|
||||||
|
require("../scss/styles.scss");
|
||||||
|
|
||||||
import { AppModule } from "./app.module";
|
import { AppModule } from "./app.module";
|
||||||
|
|
||||||
|
|||||||
@@ -768,8 +768,5 @@
|
|||||||
},
|
},
|
||||||
"launchWebVault": {
|
"launchWebVault": {
|
||||||
"message": "Launch Web Vault"
|
"message": "Launch Web Vault"
|
||||||
},
|
|
||||||
"authenticationFailed": {
|
|
||||||
"message": "Authentication failed"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { MenuMain } from "./menu.main";
|
|||||||
const SyncCheckInterval = 60 * 1000; // 1 minute
|
const SyncCheckInterval = 60 * 1000; // 1 minute
|
||||||
|
|
||||||
export class MessagingMain {
|
export class MessagingMain {
|
||||||
private syncTimeout: ReturnType<typeof setTimeout>;
|
private syncTimeout: NodeJS.Timeout;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private windowMain: WindowMain,
|
private windowMain: WindowMain,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { GetUniqueString } from "@/jslib/common/spec/utils";
|
|||||||
|
|
||||||
import { UserEntry } from "@/src/models/userEntry";
|
import { UserEntry } from "@/src/models/userEntry";
|
||||||
|
|
||||||
import { groupSimulator, userSimulator } from "../../utils/request-builder-helper";
|
|
||||||
import { RequestBuilderOptions } from "../abstractions/request-builder.service";
|
import { RequestBuilderOptions } from "../abstractions/request-builder.service";
|
||||||
|
import { groupSimulator, userSimulator } from "../utils/request-builder-helper";
|
||||||
|
|
||||||
import { BatchRequestBuilder } from "./batch-request-builder";
|
import { BatchRequestBuilder } from "./batch-request-builder";
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { DirectoryFactoryService } from "../abstractions/directory-factory.servi
|
|||||||
import { StateService } from "../abstractions/state.service";
|
import { StateService } from "../abstractions/state.service";
|
||||||
import { DirectoryType } from "../enums/directoryType";
|
import { DirectoryType } from "../enums/directoryType";
|
||||||
|
|
||||||
import { EntraIdDirectoryService } from "./directory-services/entra-id-directory.service";
|
import { EntraIdDirectoryService } from "./entra-id-directory.service";
|
||||||
import { GSuiteDirectoryService } from "./directory-services/gsuite-directory.service";
|
import { GSuiteDirectoryService } from "./gsuite-directory.service";
|
||||||
import { LdapDirectoryService } from "./directory-services/ldap-directory.service";
|
import { LdapDirectoryService } from "./ldap-directory.service";
|
||||||
import { OktaDirectoryService } from "./directory-services/okta-directory.service";
|
import { OktaDirectoryService } from "./okta-directory.service";
|
||||||
import { OneLoginDirectoryService } from "./directory-services/onelogin-directory.service";
|
import { OneLoginDirectoryService } from "./onelogin-directory.service";
|
||||||
|
|
||||||
export class DefaultDirectoryFactoryService implements DirectoryFactoryService {
|
export class DefaultDirectoryFactoryService implements DirectoryFactoryService {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
import { config as dotenvConfig } from "dotenv";
|
|
||||||
import { mock, MockProxy } from "jest-mock-extended";
|
|
||||||
|
|
||||||
import { I18nService } from "../../../jslib/common/src/abstractions/i18n.service";
|
|
||||||
import { LogService } from "../../../jslib/common/src/abstractions/log.service";
|
|
||||||
import {
|
|
||||||
getGSuiteConfiguration,
|
|
||||||
getSyncConfiguration,
|
|
||||||
} from "../../../utils/google-workspace/config-fixtures";
|
|
||||||
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 { GSuiteDirectoryService } from "./gsuite-directory.service";
|
|
||||||
|
|
||||||
// These tests integrate with a test Google Workspace instance.
|
|
||||||
// Credentials are located in the shared Bitwarden collection for Directory Connector testing.
|
|
||||||
// Place the .env file attachment in the utils folder.
|
|
||||||
|
|
||||||
// Load .env variables
|
|
||||||
dotenvConfig({ path: "utils/.env" });
|
|
||||||
|
|
||||||
// These filters target integration test data.
|
|
||||||
// These should return data that matches the user and group fixtures exactly.
|
|
||||||
// There may be additional data present if not used.
|
|
||||||
const INTEGRATION_USER_FILTER = "|orgUnitPath='/Integration testing'";
|
|
||||||
const INTEGRATION_GROUP_FILTER = "|name:Integration*";
|
|
||||||
|
|
||||||
// These tests are slow!
|
|
||||||
// Increase the default timeout from 5s to 15s
|
|
||||||
jest.setTimeout(15000);
|
|
||||||
|
|
||||||
describe("gsuiteDirectoryService", () => {
|
|
||||||
let logService: MockProxy<LogService>;
|
|
||||||
let i18nService: MockProxy<I18nService>;
|
|
||||||
let stateService: MockProxy<StateService>;
|
|
||||||
|
|
||||||
let directoryService: GSuiteDirectoryService;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
logService = mock();
|
|
||||||
i18nService = mock();
|
|
||||||
stateService = mock();
|
|
||||||
|
|
||||||
stateService.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);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("syncs without using filters (includes test data)", async () => {
|
|
||||||
const directoryConfig = getGSuiteConfiguration();
|
|
||||||
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig);
|
|
||||||
|
|
||||||
const syncConfig = getSyncConfiguration({
|
|
||||||
groups: true,
|
|
||||||
users: true,
|
|
||||||
});
|
|
||||||
stateService.getSync.mockResolvedValue(syncConfig);
|
|
||||||
|
|
||||||
const result = await directoryService.getEntries(true, true);
|
|
||||||
|
|
||||||
expect(result[0]).toEqual(expect.arrayContaining(groupFixtures));
|
|
||||||
expect(result[1]).toEqual(expect.arrayContaining(userFixtures));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("syncs using user and group filters (exact match for test data)", async () => {
|
|
||||||
const directoryConfig = getGSuiteConfiguration();
|
|
||||||
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig);
|
|
||||||
|
|
||||||
const syncConfig = getSyncConfiguration({
|
|
||||||
groups: true,
|
|
||||||
users: true,
|
|
||||||
userFilter: INTEGRATION_USER_FILTER,
|
|
||||||
groupFilter: INTEGRATION_GROUP_FILTER,
|
|
||||||
});
|
|
||||||
stateService.getSync.mockResolvedValue(syncConfig);
|
|
||||||
|
|
||||||
const result = await directoryService.getEntries(true, true);
|
|
||||||
|
|
||||||
expect(result).toEqual([groupFixtures, userFixtures]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { GroupEntry } from "../../models/groupEntry";
|
import { GroupEntry } from "../models/groupEntry";
|
||||||
import { UserEntry } from "../../models/userEntry";
|
import { UserEntry } from "../models/userEntry";
|
||||||
|
|
||||||
export interface IDirectoryService {
|
export interface IDirectoryService {
|
||||||
getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>;
|
getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>;
|
||||||
@@ -7,14 +7,14 @@ import * as graphType from "@microsoft/microsoft-graph-types";
|
|||||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||||
|
|
||||||
import { StateService } from "../../abstractions/state.service";
|
import { StateService } from "../abstractions/state.service";
|
||||||
import { DirectoryType } from "../../enums/directoryType";
|
import { DirectoryType } from "../enums/directoryType";
|
||||||
import { EntraIdConfiguration } from "../../models/entraIdConfiguration";
|
import { EntraIdConfiguration } from "../models/entraIdConfiguration";
|
||||||
import { GroupEntry } from "../../models/groupEntry";
|
import { GroupEntry } from "../models/groupEntry";
|
||||||
import { SyncConfiguration } from "../../models/syncConfiguration";
|
import { SyncConfiguration } from "../models/syncConfiguration";
|
||||||
import { UserEntry } from "../../models/userEntry";
|
import { UserEntry } from "../models/userEntry";
|
||||||
import { BaseDirectoryService } from "../baseDirectory.service";
|
|
||||||
|
|
||||||
|
import { BaseDirectoryService } from "./baseDirectory.service";
|
||||||
import { IDirectoryService } from "./directory.service";
|
import { IDirectoryService } from "./directory.service";
|
||||||
|
|
||||||
const EntraIdPublicIdentityAuthority = "login.microsoftonline.com";
|
const EntraIdPublicIdentityAuthority = "login.microsoftonline.com";
|
||||||
@@ -132,7 +132,7 @@ export class EntraIdDirectoryService extends BaseDirectoryService implements IDi
|
|||||||
}
|
}
|
||||||
|
|
||||||
const setFilter = this.createCustomUserSet(this.syncConfig.userFilter);
|
const setFilter = this.createCustomUserSet(this.syncConfig.userFilter);
|
||||||
|
// eslint-disable-next-line
|
||||||
while (true) {
|
while (true) {
|
||||||
const users: graphType.User[] = res.value;
|
const users: graphType.User[] = res.value;
|
||||||
if (users != null) {
|
if (users != null) {
|
||||||
@@ -211,7 +211,7 @@ export class EntraIdDirectoryService extends BaseDirectoryService implements IDi
|
|||||||
let auMembers = await this.client
|
let auMembers = await this.client
|
||||||
.api(`${this.getGraphApiEndpoint()}/v1.0/directory/administrativeUnits/${p}/members`)
|
.api(`${this.getGraphApiEndpoint()}/v1.0/directory/administrativeUnits/${p}/members`)
|
||||||
.get();
|
.get();
|
||||||
|
// eslint-disable-next-line
|
||||||
while (true) {
|
while (true) {
|
||||||
for (const auMember of auMembers.value) {
|
for (const auMember of auMembers.value) {
|
||||||
const groupId = auMember.id;
|
const groupId = auMember.id;
|
||||||
@@ -328,7 +328,7 @@ export class EntraIdDirectoryService extends BaseDirectoryService implements IDi
|
|||||||
const entries: GroupEntry[] = [];
|
const entries: GroupEntry[] = [];
|
||||||
const groupsReq = this.client.api("/groups");
|
const groupsReq = this.client.api("/groups");
|
||||||
let res = await groupsReq.get();
|
let res = await groupsReq.get();
|
||||||
|
// eslint-disable-next-line
|
||||||
while (true) {
|
while (true) {
|
||||||
const groups: graphType.Group[] = res.value;
|
const groups: graphType.Group[] = res.value;
|
||||||
if (groups != null) {
|
if (groups != null) {
|
||||||
@@ -421,7 +421,7 @@ export class EntraIdDirectoryService extends BaseDirectoryService implements IDi
|
|||||||
|
|
||||||
const memReq = this.client.api("/groups/" + group.id + "/members");
|
const memReq = this.client.api("/groups/" + group.id + "/members");
|
||||||
let memRes = await memReq.get();
|
let memRes = await memReq.get();
|
||||||
|
// eslint-disable-next-line
|
||||||
while (true) {
|
while (true) {
|
||||||
const members: any = memRes.value;
|
const members: any = memRes.value;
|
||||||
if (members != null) {
|
if (members != null) {
|
||||||
@@ -4,14 +4,14 @@ import { admin_directory_v1, google } from "googleapis";
|
|||||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||||
|
|
||||||
import { StateService } from "../../abstractions/state.service";
|
import { StateService } from "../abstractions/state.service";
|
||||||
import { DirectoryType } from "../../enums/directoryType";
|
import { DirectoryType } from "../enums/directoryType";
|
||||||
import { GroupEntry } from "../../models/groupEntry";
|
import { GroupEntry } from "../models/groupEntry";
|
||||||
import { GSuiteConfiguration } from "../../models/gsuiteConfiguration";
|
import { GSuiteConfiguration } from "../models/gsuiteConfiguration";
|
||||||
import { SyncConfiguration } from "../../models/syncConfiguration";
|
import { SyncConfiguration } from "../models/syncConfiguration";
|
||||||
import { UserEntry } from "../../models/userEntry";
|
import { UserEntry } from "../models/userEntry";
|
||||||
import { BaseDirectoryService } from "../baseDirectory.service";
|
|
||||||
|
|
||||||
|
import { BaseDirectoryService } from "./baseDirectory.service";
|
||||||
import { IDirectoryService } from "./directory.service";
|
import { IDirectoryService } from "./directory.service";
|
||||||
|
|
||||||
export class GSuiteDirectoryService extends BaseDirectoryService implements IDirectoryService {
|
export class GSuiteDirectoryService extends BaseDirectoryService implements IDirectoryService {
|
||||||
@@ -71,7 +71,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
|
|||||||
let nextPageToken: string = null;
|
let nextPageToken: string = null;
|
||||||
|
|
||||||
const filter = this.createCustomSet(this.syncConfig.userFilter);
|
const filter = this.createCustomSet(this.syncConfig.userFilter);
|
||||||
|
// eslint-disable-next-line
|
||||||
while (true) {
|
while (true) {
|
||||||
this.logService.info("Querying users - nextPageToken:" + nextPageToken);
|
this.logService.info("Querying users - nextPageToken:" + nextPageToken);
|
||||||
const p = Object.assign({ query: query, pageToken: nextPageToken }, this.authParams);
|
const p = Object.assign({ query: query, pageToken: nextPageToken }, this.authParams);
|
||||||
@@ -99,7 +99,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
|
|||||||
}
|
}
|
||||||
|
|
||||||
nextPageToken = null;
|
nextPageToken = null;
|
||||||
|
// eslint-disable-next-line
|
||||||
while (true) {
|
while (true) {
|
||||||
this.logService.info("Querying deleted users - nextPageToken:" + nextPageToken);
|
this.logService.info("Querying deleted users - nextPageToken:" + nextPageToken);
|
||||||
const p = Object.assign(
|
const p = Object.assign(
|
||||||
@@ -154,6 +154,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
|
|||||||
const query = this.createDirectoryQuery(this.syncConfig.groupFilter);
|
const query = this.createDirectoryQuery(this.syncConfig.groupFilter);
|
||||||
let nextPageToken: string = null;
|
let nextPageToken: string = null;
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
while (true) {
|
while (true) {
|
||||||
this.logService.info("Querying groups - nextPageToken:" + nextPageToken);
|
this.logService.info("Querying groups - nextPageToken:" + nextPageToken);
|
||||||
let p = null;
|
let p = null;
|
||||||
@@ -193,6 +194,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
|
|||||||
entry.externalId = group.id;
|
entry.externalId = group.id;
|
||||||
entry.name = group.name;
|
entry.name = group.name;
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
while (true) {
|
while (true) {
|
||||||
const p = Object.assign({ groupKey: group.id, pageToken: nextPageToken }, this.authParams);
|
const p = Object.assign({ groupKey: group.id, pageToken: nextPageToken }, this.authParams);
|
||||||
const memRes = await this.service.members.list(p);
|
const memRes = await this.service.members.list(p);
|
||||||
@@ -251,15 +253,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
await this.client.authorize();
|
||||||
await this.client.authorize();
|
|
||||||
} catch (error) {
|
|
||||||
// Catch and rethrow this to sanitize any sensitive info (e.g. private key) in the error message
|
|
||||||
this.logService.error(
|
|
||||||
`Google Workspace authentication failed: ${error?.name || "Unknown error"}`,
|
|
||||||
);
|
|
||||||
throw new Error(this.i18nService.t("authenticationFailed"));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.authParams = {
|
this.authParams = {
|
||||||
auth: this.client,
|
auth: this.client,
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
import { I18nService } from "../../../jslib/common/src/abstractions/i18n.service";
|
import { I18nService } from "../../jslib/common/src/abstractions/i18n.service";
|
||||||
import { LogService } from "../../../jslib/common/src/abstractions/log.service";
|
import { LogService } from "../../jslib/common/src/abstractions/log.service";
|
||||||
import {
|
import { groupFixtures } from "../../openldap/group-fixtures";
|
||||||
getLdapConfiguration,
|
import { userFixtures } from "../../openldap/user-fixtures";
|
||||||
getSyncConfiguration,
|
import { DirectoryType } from "../enums/directoryType";
|
||||||
} from "../../../utils/openldap/config-fixtures";
|
import { getLdapConfiguration, getSyncConfiguration } from "../utils/test-fixtures";
|
||||||
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 { LdapDirectoryService } from "./ldap-directory.service";
|
import { LdapDirectoryService } from "./ldap-directory.service";
|
||||||
|
import { StateService } from "./state.service";
|
||||||
|
|
||||||
// These tests integrate with the OpenLDAP docker image and seed data located in the openldap folder.
|
// These tests integrate with the OpenLDAP docker image and seed data located in the openldap folder.
|
||||||
// To run theses tests:
|
// To run theses tests:
|
||||||
@@ -55,7 +52,7 @@ describe("ldapDirectoryService", () => {
|
|||||||
getLdapConfiguration({
|
getLdapConfiguration({
|
||||||
ssl: true,
|
ssl: true,
|
||||||
startTls: true,
|
startTls: true,
|
||||||
tlsCaPath: "./utils/openldap/certs/rootCA.pem",
|
tlsCaPath: "./openldap/certs/rootCA.pem",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
stateService.getSync.mockResolvedValue(getSyncConfiguration({ groups: true, users: true }));
|
stateService.getSync.mockResolvedValue(getSyncConfiguration({ groups: true, users: true }));
|
||||||
@@ -70,7 +67,7 @@ describe("ldapDirectoryService", () => {
|
|||||||
getLdapConfiguration({
|
getLdapConfiguration({
|
||||||
port: 1636,
|
port: 1636,
|
||||||
ssl: true,
|
ssl: true,
|
||||||
sslCaPath: "./utils/openldap/certs/rootCA.pem",
|
sslCaPath: "./openldap/certs/rootCA.pem",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
stateService.getSync.mockResolvedValue(getSyncConfiguration({ groups: true, users: true }));
|
stateService.getSync.mockResolvedValue(getSyncConfiguration({ groups: true, users: true }));
|
||||||
@@ -7,12 +7,12 @@ import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
|||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
import { Utils } from "@/jslib/common/src/misc/utils";
|
||||||
|
|
||||||
import { StateService } from "../../abstractions/state.service";
|
import { StateService } from "../abstractions/state.service";
|
||||||
import { DirectoryType } from "../../enums/directoryType";
|
import { DirectoryType } from "../enums/directoryType";
|
||||||
import { GroupEntry } from "../../models/groupEntry";
|
import { GroupEntry } from "../models/groupEntry";
|
||||||
import { LdapConfiguration } from "../../models/ldapConfiguration";
|
import { LdapConfiguration } from "../models/ldapConfiguration";
|
||||||
import { SyncConfiguration } from "../../models/syncConfiguration";
|
import { SyncConfiguration } from "../models/syncConfiguration";
|
||||||
import { UserEntry } from "../../models/userEntry";
|
import { UserEntry } from "../models/userEntry";
|
||||||
|
|
||||||
import { IDirectoryService } from "./directory.service";
|
import { IDirectoryService } from "./directory.service";
|
||||||
|
|
||||||
@@ -3,14 +3,14 @@ import * as https from "https";
|
|||||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||||
|
|
||||||
import { StateService } from "../../abstractions/state.service";
|
import { StateService } from "../abstractions/state.service";
|
||||||
import { DirectoryType } from "../../enums/directoryType";
|
import { DirectoryType } from "../enums/directoryType";
|
||||||
import { GroupEntry } from "../../models/groupEntry";
|
import { GroupEntry } from "../models/groupEntry";
|
||||||
import { OktaConfiguration } from "../../models/oktaConfiguration";
|
import { OktaConfiguration } from "../models/oktaConfiguration";
|
||||||
import { SyncConfiguration } from "../../models/syncConfiguration";
|
import { SyncConfiguration } from "../models/syncConfiguration";
|
||||||
import { UserEntry } from "../../models/userEntry";
|
import { UserEntry } from "../models/userEntry";
|
||||||
import { BaseDirectoryService } from "../baseDirectory.service";
|
|
||||||
|
|
||||||
|
import { BaseDirectoryService } from "./baseDirectory.service";
|
||||||
import { IDirectoryService } from "./directory.service";
|
import { IDirectoryService } from "./directory.service";
|
||||||
|
|
||||||
const DelayBetweenBuildGroupCallsInMilliseconds = 500;
|
const DelayBetweenBuildGroupCallsInMilliseconds = 500;
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||||
|
|
||||||
import { StateService } from "../../abstractions/state.service";
|
import { StateService } from "../abstractions/state.service";
|
||||||
import { DirectoryType } from "../../enums/directoryType";
|
import { DirectoryType } from "../enums/directoryType";
|
||||||
import { GroupEntry } from "../../models/groupEntry";
|
import { GroupEntry } from "../models/groupEntry";
|
||||||
import { OneLoginConfiguration } from "../../models/oneLoginConfiguration";
|
import { OneLoginConfiguration } from "../models/oneLoginConfiguration";
|
||||||
import { SyncConfiguration } from "../../models/syncConfiguration";
|
import { SyncConfiguration } from "../models/syncConfiguration";
|
||||||
import { UserEntry } from "../../models/userEntry";
|
import { UserEntry } from "../models/userEntry";
|
||||||
import { BaseDirectoryService } from "../baseDirectory.service";
|
|
||||||
|
|
||||||
|
import { BaseDirectoryService } from "./baseDirectory.service";
|
||||||
import { IDirectoryService } from "./directory.service";
|
import { IDirectoryService } from "./directory.service";
|
||||||
|
|
||||||
// Basic email validation: something@something.something
|
// Basic email validation: something@something.something
|
||||||
@@ -2,8 +2,8 @@ import { GetUniqueString } from "@/jslib/common/spec/utils";
|
|||||||
|
|
||||||
import { UserEntry } from "@/src/models/userEntry";
|
import { UserEntry } from "@/src/models/userEntry";
|
||||||
|
|
||||||
import { groupSimulator, userSimulator } from "../../utils/request-builder-helper";
|
|
||||||
import { RequestBuilderOptions } from "../abstractions/request-builder.service";
|
import { RequestBuilderOptions } from "../abstractions/request-builder.service";
|
||||||
|
import { groupSimulator, userSimulator } from "../utils/request-builder-helper";
|
||||||
|
|
||||||
import { SingleRequestBuilder } from "./single-request-builder";
|
import { SingleRequestBuilder } from "./single-request-builder";
|
||||||
|
|
||||||
|
|||||||
@@ -7,20 +7,19 @@ import { EnvironmentService } from "@/jslib/common/src/services/environment.serv
|
|||||||
|
|
||||||
import { I18nService } from "../../jslib/common/src/abstractions/i18n.service";
|
import { I18nService } from "../../jslib/common/src/abstractions/i18n.service";
|
||||||
import { LogService } from "../../jslib/common/src/abstractions/log.service";
|
import { LogService } from "../../jslib/common/src/abstractions/log.service";
|
||||||
import { getLdapConfiguration, getSyncConfiguration } from "../../utils/openldap/config-fixtures";
|
import { groupFixtures } from "../../openldap/group-fixtures";
|
||||||
|
import { userFixtures } from "../../openldap/user-fixtures";
|
||||||
import { DirectoryFactoryService } from "../abstractions/directory-factory.service";
|
import { DirectoryFactoryService } from "../abstractions/directory-factory.service";
|
||||||
import { DirectoryType } from "../enums/directoryType";
|
import { DirectoryType } from "../enums/directoryType";
|
||||||
|
import { getLdapConfiguration, getSyncConfiguration } from "../utils/test-fixtures";
|
||||||
|
|
||||||
import { BatchRequestBuilder } from "./batch-request-builder";
|
import { BatchRequestBuilder } from "./batch-request-builder";
|
||||||
import { LdapDirectoryService } from "./directory-services/ldap-directory.service";
|
import { LdapDirectoryService } from "./ldap-directory.service";
|
||||||
import { SingleRequestBuilder } from "./single-request-builder";
|
import { SingleRequestBuilder } from "./single-request-builder";
|
||||||
import { StateService } from "./state.service";
|
import { StateService } from "./state.service";
|
||||||
import { SyncService } from "./sync.service";
|
import { SyncService } from "./sync.service";
|
||||||
import * as constants from "./sync.service";
|
import * as constants from "./sync.service";
|
||||||
|
|
||||||
import { groupFixtures } from "@/utils/openldap/group-fixtures";
|
|
||||||
import { userFixtures } from "@/utils/openldap/user-fixtures";
|
|
||||||
|
|
||||||
describe("SyncService", () => {
|
describe("SyncService", () => {
|
||||||
let logService: MockProxy<LogService>;
|
let logService: MockProxy<LogService>;
|
||||||
let i18nService: MockProxy<I18nService>;
|
let i18nService: MockProxy<I18nService>;
|
||||||
@@ -116,7 +115,6 @@ describe("SyncService", () => {
|
|||||||
stateService.getLastSyncHash.mockResolvedValue("unique hash");
|
stateService.getLastSyncHash.mockResolvedValue("unique hash");
|
||||||
|
|
||||||
// @ts-expect-error This is a workaround to make the batchsize smaller to trigger the batching logic since its a const.
|
// @ts-expect-error This is a workaround to make the batchsize smaller to trigger the batching logic since its a const.
|
||||||
// eslint-disable-next-line no-import-assign
|
|
||||||
constants.batchSize = 4;
|
constants.batchSize = 4;
|
||||||
|
|
||||||
const syncResult = await syncService.sync(false, false);
|
const syncResult = await syncService.sync(false, false);
|
||||||
@@ -131,7 +129,6 @@ describe("SyncService", () => {
|
|||||||
expect(apiService.postPublicImportDirectory).toHaveBeenCalledTimes(7);
|
expect(apiService.postPublicImportDirectory).toHaveBeenCalledTimes(7);
|
||||||
|
|
||||||
// @ts-expect-error Reset batch size to original state.
|
// @ts-expect-error Reset batch size to original state.
|
||||||
// eslint-disable-next-line no-import-assign
|
|
||||||
constants.batchSize = originalBatchSize;
|
constants.batchSize = originalBatchSize;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,20 +6,20 @@ import { MessagingService } from "@/jslib/common/src/abstractions/messaging.serv
|
|||||||
import { OrganizationImportRequest } from "@/jslib/common/src/models/request/organizationImportRequest";
|
import { OrganizationImportRequest } from "@/jslib/common/src/models/request/organizationImportRequest";
|
||||||
import { ApiService } from "@/jslib/common/src/services/api.service";
|
import { ApiService } from "@/jslib/common/src/services/api.service";
|
||||||
|
|
||||||
import { getSyncConfiguration } from "../../utils/openldap/config-fixtures";
|
|
||||||
import { DirectoryFactoryService } from "../abstractions/directory-factory.service";
|
import { DirectoryFactoryService } from "../abstractions/directory-factory.service";
|
||||||
import { DirectoryType } from "../enums/directoryType";
|
import { DirectoryType } from "../enums/directoryType";
|
||||||
|
import { getSyncConfiguration } from "../utils/test-fixtures";
|
||||||
|
|
||||||
import { BatchRequestBuilder } from "./batch-request-builder";
|
import { BatchRequestBuilder } from "./batch-request-builder";
|
||||||
import { LdapDirectoryService } from "./directory-services/ldap-directory.service";
|
|
||||||
import { I18nService } from "./i18n.service";
|
import { I18nService } from "./i18n.service";
|
||||||
|
import { LdapDirectoryService } from "./ldap-directory.service";
|
||||||
import { SingleRequestBuilder } from "./single-request-builder";
|
import { SingleRequestBuilder } from "./single-request-builder";
|
||||||
import { StateService } from "./state.service";
|
import { StateService } from "./state.service";
|
||||||
import { SyncService } from "./sync.service";
|
import { SyncService } from "./sync.service";
|
||||||
import * as constants from "./sync.service";
|
import * as constants from "./sync.service";
|
||||||
|
|
||||||
import { groupFixtures } from "@/utils/openldap/group-fixtures";
|
import { groupFixtures } from "@/openldap/group-fixtures";
|
||||||
import { userFixtures } from "@/utils/openldap/user-fixtures";
|
import { userFixtures } from "@/openldap/user-fixtures";
|
||||||
|
|
||||||
describe("SyncService", () => {
|
describe("SyncService", () => {
|
||||||
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
|
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
|
||||||
@@ -97,7 +97,6 @@ describe("SyncService", () => {
|
|||||||
stateService.getLastSyncHash.mockResolvedValue("unique hash");
|
stateService.getLastSyncHash.mockResolvedValue("unique hash");
|
||||||
|
|
||||||
// @ts-expect-error This is a workaround to make the batchsize smaller to trigger the batching logic since its a const.
|
// @ts-expect-error This is a workaround to make the batchsize smaller to trigger the batching logic since its a const.
|
||||||
// eslint-disable-next-line no-import-assign
|
|
||||||
constants.batchSize = 4;
|
constants.batchSize = 4;
|
||||||
|
|
||||||
const mockRequests = new Array(6).fill({
|
const mockRequests = new Array(6).fill({
|
||||||
@@ -120,7 +119,6 @@ describe("SyncService", () => {
|
|||||||
expect(apiService.postPublicImportDirectory).toHaveBeenCalledWith(mockRequests[5]);
|
expect(apiService.postPublicImportDirectory).toHaveBeenCalledWith(mockRequests[5]);
|
||||||
|
|
||||||
// @ts-expect-error Reset batch size back to original value.
|
// @ts-expect-error Reset batch size back to original value.
|
||||||
// eslint-disable-next-line no-import-assign
|
|
||||||
constants.batchSize = originalBatchSize;
|
constants.batchSize = originalBatchSize;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { GetUniqueString } from "@/jslib/common/spec/utils";
|
import { GetUniqueString } from "@/jslib/common/spec/utils";
|
||||||
|
|
||||||
import { GroupEntry } from "../src/models/groupEntry";
|
import { GroupEntry } from "../models/groupEntry";
|
||||||
import { UserEntry } from "../src/models/userEntry";
|
import { UserEntry } from "../models/userEntry";
|
||||||
|
|
||||||
export function userSimulator(userCount: number): UserEntry[] {
|
export function userSimulator(userCount: number): UserEntry[] {
|
||||||
const users: UserEntry[] = [];
|
const users: UserEntry[] = [];
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { LdapConfiguration } from "../../src/models/ldapConfiguration";
|
import { LdapConfiguration } from "../models/ldapConfiguration";
|
||||||
import { SyncConfiguration } from "../../src/models/syncConfiguration";
|
import { SyncConfiguration } from "../models/syncConfiguration";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns a basic ldap configuration without TLS/SSL enabled. Can be overridden by passing in a partial configuration.
|
* @returns a basic ldap configuration without TLS/SSL enabled. Can be overridden by passing in a partial configuration.
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
"pretty": true,
|
"pretty": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"target": "ES2020",
|
"target": "ES2016",
|
||||||
"module": "ES2020",
|
"module": "ES2020",
|
||||||
"lib": ["es5", "es6", "es7", "dom"],
|
"lib": ["es5", "es6", "es7", "dom"],
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
@@ -18,8 +18,6 @@
|
|||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"skipLibCheck": true,
|
|
||||||
"noEmitOnError": false,
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"tldjs": ["./jslib/common/src/misc/tldjs.noop"],
|
"tldjs": ["./jslib/common/src/misc/tldjs.noop"],
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"]
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"angularCompilerOptions": {
|
|
||||||
"strictTemplates": true,
|
|
||||||
"preserveWhitespaces": true
|
|
||||||
},
|
|
||||||
"compilerOptions": {
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"noEmitOnError": false
|
|
||||||
},
|
|
||||||
"include": ["src/app"],
|
|
||||||
"exclude": ["jslib", "**/*.spec.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
GOOGLE_DOMAIN=
|
|
||||||
GOOGLE_ADMIN_USER=
|
|
||||||
GOOGLE_CLIENT_EMAIL=
|
|
||||||
GOOGLE_PRIVATE_KEY=
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { GSuiteConfiguration } from "../../src/models/gsuiteConfiguration";
|
|
||||||
import { SyncConfiguration } from "../../src/models/syncConfiguration";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns a basic GSuite configuration. Can be overridden by passing in a partial configuration.
|
|
||||||
*/
|
|
||||||
export const getGSuiteConfiguration = (
|
|
||||||
config?: Partial<GSuiteConfiguration>,
|
|
||||||
): GSuiteConfiguration => {
|
|
||||||
const adminUser = process.env.GOOGLE_ADMIN_USER;
|
|
||||||
const clientEmail = process.env.GOOGLE_CLIENT_EMAIL;
|
|
||||||
const privateKey = process.env.GOOGLE_PRIVATE_KEY;
|
|
||||||
const domain = process.env.GOOGLE_DOMAIN;
|
|
||||||
|
|
||||||
if (!adminUser || !clientEmail || !privateKey || !domain) {
|
|
||||||
throw new Error("Google Workspace integration test credentials not configured.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
// TODO
|
|
||||||
adminUser,
|
|
||||||
clientEmail,
|
|
||||||
privateKey,
|
|
||||||
domain: domain,
|
|
||||||
customer: "",
|
|
||||||
...(config ?? {}),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns a basic Google Workspace sync configuration. Can be overridden by passing in a partial configuration.
|
|
||||||
*/
|
|
||||||
export const getSyncConfiguration = (config?: Partial<SyncConfiguration>): SyncConfiguration => ({
|
|
||||||
users: false,
|
|
||||||
groups: false,
|
|
||||||
interval: 5,
|
|
||||||
userFilter: "",
|
|
||||||
groupFilter: "",
|
|
||||||
removeDisabled: false,
|
|
||||||
overwriteExisting: false,
|
|
||||||
largeImport: false,
|
|
||||||
// Ldap properties - not optional for some reason
|
|
||||||
groupObjectClass: "",
|
|
||||||
userObjectClass: "",
|
|
||||||
groupPath: null,
|
|
||||||
userPath: null,
|
|
||||||
groupNameAttribute: "",
|
|
||||||
userEmailAttribute: "",
|
|
||||||
memberAttribute: "",
|
|
||||||
useEmailPrefixSuffix: false,
|
|
||||||
emailPrefixAttribute: "",
|
|
||||||
emailSuffix: null,
|
|
||||||
creationDateAttribute: "",
|
|
||||||
revisionDateAttribute: "",
|
|
||||||
...(config ?? {}),
|
|
||||||
});
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { Jsonify } from "type-fest";
|
|
||||||
|
|
||||||
import { GroupEntry } from "../../src/models/groupEntry";
|
|
||||||
|
|
||||||
// These must match the Google Workspace seed data
|
|
||||||
|
|
||||||
const data: Jsonify<GroupEntry>[] = [
|
|
||||||
{
|
|
||||||
externalId: "0319y80a3anpxhj",
|
|
||||||
groupMemberReferenceIds: [],
|
|
||||||
name: "Integration Test Group A",
|
|
||||||
referenceId: "0319y80a3anpxhj",
|
|
||||||
userMemberExternalIds: ["111605910541641314041", "111147009830456099026"],
|
|
||||||
users: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
externalId: "02afmg28317uyub",
|
|
||||||
groupMemberReferenceIds: [],
|
|
||||||
name: "Integration Test Group B",
|
|
||||||
referenceId: "02afmg28317uyub",
|
|
||||||
userMemberExternalIds: ["111147009830456099026", "100150970267699397306"],
|
|
||||||
users: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const groupFixtures = data.map((g) => GroupEntry.fromJSON(g));
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { Jsonify } from "type-fest";
|
|
||||||
|
|
||||||
import { UserEntry } from "../../src/models/userEntry";
|
|
||||||
|
|
||||||
// These must match the Google Workspace seed data
|
|
||||||
|
|
||||||
const data: Jsonify<UserEntry>[] = [
|
|
||||||
// In Group A
|
|
||||||
{
|
|
||||||
deleted: false,
|
|
||||||
disabled: false,
|
|
||||||
email: "testuser1@bwrox.dev",
|
|
||||||
externalId: "111605910541641314041",
|
|
||||||
referenceId: "111605910541641314041",
|
|
||||||
},
|
|
||||||
// In Groups A + B
|
|
||||||
{
|
|
||||||
deleted: false,
|
|
||||||
disabled: false,
|
|
||||||
email: "testuser2@bwrox.dev",
|
|
||||||
externalId: "111147009830456099026",
|
|
||||||
referenceId: "111147009830456099026",
|
|
||||||
},
|
|
||||||
// In Group B
|
|
||||||
{
|
|
||||||
deleted: false,
|
|
||||||
disabled: false,
|
|
||||||
email: "testuser3@bwrox.dev",
|
|
||||||
externalId: "100150970267699397306",
|
|
||||||
referenceId: "100150970267699397306",
|
|
||||||
},
|
|
||||||
// Not in a group
|
|
||||||
{
|
|
||||||
deleted: false,
|
|
||||||
disabled: false,
|
|
||||||
email: "testuser4@bwrox.dev",
|
|
||||||
externalId: "113764752650306721470",
|
|
||||||
referenceId: "113764752650306721470",
|
|
||||||
},
|
|
||||||
// Disabled user
|
|
||||||
{
|
|
||||||
deleted: false,
|
|
||||||
disabled: true,
|
|
||||||
email: "testuser5@bwrox.dev",
|
|
||||||
externalId: "110381976819725658200",
|
|
||||||
referenceId: "110381976819725658200",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const userFixtures = data.map((g) => UserEntry.fromJSON(g));
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
if ! [ -x "$(command -v mkcert)" ]; then
|
|
||||||
echo 'Error: mkcert is not installed. Install mkcert first and then re-run this script.'
|
|
||||||
echo 'e.g. brew install mkcert'
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkcert -install
|
|
||||||
mkdir -p ./utils/openldap/certs
|
|
||||||
cp "$(mkcert -CAROOT)/rootCA.pem" ./utils/openldap/certs/rootCA.pem
|
|
||||||
mkcert -key-file ./utils/openldap/certs/openldap-key.pem -cert-file ./utils/openldap/certs/openldap.pem localhost openldap
|
|
||||||
@@ -14,12 +14,7 @@ const ENV = (process.env.ENV = process.env.NODE_ENV);
|
|||||||
const moduleRules = [
|
const moduleRules = [
|
||||||
{
|
{
|
||||||
test: /\.ts$/,
|
test: /\.ts$/,
|
||||||
use: {
|
use: "ts-loader",
|
||||||
loader: "ts-loader",
|
|
||||||
options: {
|
|
||||||
transpileOnly: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
exclude: path.resolve(__dirname, "node_modules"),
|
exclude: path.resolve(__dirname, "node_modules"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -67,7 +62,7 @@ const config = {
|
|||||||
modules: [path.resolve("node_modules")],
|
modules: [path.resolve("node_modules")],
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: "[name].cjs",
|
filename: "[name].js",
|
||||||
path: path.resolve(__dirname, "build-cli"),
|
path: path.resolve(__dirname, "build-cli"),
|
||||||
},
|
},
|
||||||
module: { rules: moduleRules },
|
module: { rules: moduleRules },
|
||||||
@@ -10,12 +10,7 @@ const common = {
|
|||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.tsx?$/,
|
test: /\.tsx?$/,
|
||||||
use: {
|
use: "ts-loader",
|
||||||
loader: "ts-loader",
|
|
||||||
options: {
|
|
||||||
transpileOnly: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
exclude: /node_modules\/(?!(@bitwarden)\/).*/,
|
exclude: /node_modules\/(?!(@bitwarden)\/).*/,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -62,9 +57,6 @@ const main = {
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
output: {
|
|
||||||
filename: "[name].cjs",
|
|
||||||
},
|
|
||||||
externals: {
|
externals: {
|
||||||
"electron-reload": "commonjs2 electron-reload",
|
"electron-reload": "commonjs2 electron-reload",
|
||||||
keytar: "commonjs2 keytar",
|
keytar: "commonjs2 keytar",
|
||||||
@@ -38,7 +38,7 @@ const common = {
|
|||||||
plugins: [],
|
plugins: [],
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: [".tsx", ".ts", ".js", ".json"],
|
extensions: [".tsx", ".ts", ".js", ".json"],
|
||||||
plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.renderer.json" })],
|
plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })],
|
||||||
symlinks: false,
|
symlinks: false,
|
||||||
modules: [path.resolve("node_modules")],
|
modules: [path.resolve("node_modules")],
|
||||||
},
|
},
|
||||||
@@ -113,7 +113,7 @@ const renderer = {
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new AngularWebpackPlugin({
|
new AngularWebpackPlugin({
|
||||||
tsConfigPath: "tsconfig.renderer.json",
|
tsConfigPath: "tsconfig.json",
|
||||||
entryModule: "src/app/app.module#AppModule",
|
entryModule: "src/app/app.module#AppModule",
|
||||||
sourceMap: true,
|
sourceMap: true,
|
||||||
}),
|
}),
|
||||||
Reference in New Issue
Block a user