1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-05 23:53:21 +00:00

Compare commits

...

28 Commits

Author SHA1 Message Date
Brandon
1c7205a9c6 arm => x64 2025-11-19 17:23:36 -05:00
renovate[bot]
f5abaf114a [deps]: Update actions/setup-node action to v6 (#908)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 15:22:43 +00:00
renovate[bot]
5792578946 [deps]: Update glob to v11.1.0 [SECURITY] (#923)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 09:48:22 -05:00
renovate[bot]
6b3b29a1a0 [deps]: Update angular-eslint monorepo to v20.6.0 (#911)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 14:06:00 +00:00
renovate[bot]
02809be178 [deps]: Update actions/upload-artifact action to v5 (#916)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 13:43:59 +00:00
Thomas Rittson
6abfdd8a88 Fix not reporting test results on push to main (#921)
Allow other events to report test results
2025-11-18 08:50:19 +10:00
Vincent Salucci
b95f57c4e7 chore: bump version to v2025.11.0 (#922) 2025-11-17 11:38:29 -06:00
renovate[bot]
9ecfc29ae4 [deps]: Update electron to v39 (#917)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-13 13:59:52 -05:00
Mick Letofsky
e32f29b8e7 [PM-27181] - Grant additional permissions for review code (#920) 2025-11-13 14:44:41 +01:00
brandonbiete
e333db372d [BRE-1302] Update runners to macos-15 (#918)
* [BRE-1302] Update runners to macos-15 and update architecture dependencies and targets to arm64

* [BRE-1302] Update macos-cli build job to macos-15 runner
2025-11-12 09:44:59 -05:00
Thomas Rittson
a44eb28be8 [PM-26672] Add Google Workspace integration tests to CI pipeline (#909)
- reorganize integration test files to allow for future additions
- add Google Workspace integration tests to the Github workflow
- refactor to run tests selective based on changed files and use
  Azure Key Vault
2025-11-12 06:03:37 +10:00
Thomas Rittson
ab436551de Remove unused dep: node-abi (#919) 2025-11-12 06:01:34 +10:00
renovate[bot]
10e17adfb2 [deps]: Update lint-staged to v16.2.6 (#897)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 15:24:06 -06:00
Mick Letofsky
c7db8376ec Implement reusable Claude code review workflow (#905)
* Implement reusable Claude code review workflow
2025-10-30 07:39:45 +01:00
renovate[bot]
bc996d680f [deps]: Update angular-eslint monorepo to v20.4.0 (#906)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 15:04:12 -04:00
Thomas Rittson
fe01b49df1 [PM-26671] Google workspace integration tests (#894)
Add tests for Google Workspace - not enabled in CI yet
2025-10-28 11:31:02 +10:00
renovate[bot]
daeb96713f [deps]: Update @microsoft/microsoft-graph-types to v2.43.1 (#895)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-22 16:38:53 -04:00
renovate[bot]
f6791dabef [deps]: Update electron to v38.3.0 (#896)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-22 15:51:18 -04:00
renovate[bot]
a3a5ed8531 [deps]: Update webpack to v5.102.1 (#900)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-22 15:47:11 -04:00
renovate[bot]
d3d62c30aa [deps]: Update node-abi to v3.78.0 (#898)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-22 15:04:21 -04:00
renovate[bot]
f81155b6b3 [deps]: Update glob to v11 (#902)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-22 13:58:57 -04:00
Vincent Salucci
57a3ef04cc chore: version bump to 2025.10.0 (#904) 2025-10-20 12:50:18 -05:00
renovate[bot]
4e21b28276 [deps]: Update typescript-eslint monorepo to v8.46.0 (#885)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-17 15:22:20 -04:00
renovate[bot]
1c2a0c677b [deps]: Update ngx-toastr to v19.1.0 (#883)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-17 15:07:36 -04:00
renovate[bot]
5666f09e89 [deps]: Update type-fest to v5 (#886)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-15 15:49:36 -05:00
Thomas Rittson
b13895bdd6 [PM-26669] Fix Google Workspace dynamic import error in CLI (#893)
* Revert "[PM-26454] Undo removal of core-js to fix dynamic import errors (#890)"

This reverts commit 7c27202dab.

This removes the core-js dependency again, because restoring it did not fix the bug.

* Downgrade googleapis to 149 to avoid ESM issue

* Exclude googleapis from updates
2025-10-09 07:10:03 +10:00
renovate[bot]
29fc4ad61e [deps]: Update sass to v1.93.2 (#884)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 15:11:07 +01:00
Brandon Treston
f722196149 upgrade Angular libs to v20 (#892) 2025-10-07 10:09:22 -04:00
46 changed files with 5397 additions and 5898 deletions

203
.claude/CLAUDE.md Normal file
View File

@@ -0,0 +1,203 @@
# Bitwarden Directory Connector
## Project Overview
Directory Connector is a TypeScript application that synchronizes users and groups from directory services to Bitwarden organizations. It provides both a desktop GUI (built with Angular and Electron) and a CLI tool (bwdc).
**Supported Directory Services:**
- LDAP (Lightweight Directory Access Protocol) - includes Active Directory and general LDAP servers
- Microsoft Entra ID (formerly Azure Active Directory)
- Google Workspace
- Okta
- OneLogin
**Technologies:**
- TypeScript
- Angular (GUI)
- Electron (Desktop wrapper)
- Node
- Jest for testing
## Code Architecture & Structure
### Directory Organization
```
src/
├── abstractions/ # Interface definitions (e.g., IDirectoryService)
├── services/ # Business logic implementations for directory services, sync, auth
├── models/ # Data models (UserEntry, GroupEntry, etc.)
├── commands/ # CLI command implementations
├── app/ # Angular GUI components
└── utils/ # Test utilities and fixtures
src-cli/ # CLI-specific code (imports common code from src/)
jslib/ # Legacy folder structure (mix of deprecated/unused and current code - new code should not be added here)
```
### Key Architectural Patterns
1. **Abstractions = Interfaces**: All interfaces are defined in `/abstractions`
2. **Services = Business Logic**: Implementations live in `/services`
3. **Directory Service Pattern**: Each directory provider implements `IDirectoryService` interface
4. **Separation of Concerns**: GUI (Angular app) and CLI (commands) share the same service layer
## Development Conventions
### Code Organization
**File Naming:**
- kebab-case for files: `ldap-directory.service.ts`
- Descriptive names that reflect purpose
**Class/Function Naming:**
- PascalCase for classes and interfaces
- camelCase for functions and variables
- Descriptive names that indicate purpose
**File Structure:**
- Keep files focused on single responsibility
- Create new service files for distinct directory integrations
- Separate models into individual files when complex
### TypeScript Conventions
**Import Patterns:**
- Use path aliases (`@/`) for project imports
- `@/` - project root
- `@/jslib/` - jslib folder
- ESLint enforces alphabetized import ordering with newlines between groups
**Type Safety:**
- Avoid `any` types - use proper typing or `unknown` with type guards
- Prefer interfaces for contracts, types for unions/intersections
- Use strict null checks - handle `null` and `undefined` explicitly
- Leverage TypeScript's type inference where appropriate
**Configuration:**
- Use configuration files or environment variables
- Never hardcode URLs or configuration values
## Security Best Practices
**Credential Handling:**
- Never log directory service credentials, API keys, or tokens
- Use secure storage mechanisms for sensitive data
- Credentials should never be hardcoded
- Store credentials encrypted, never in plain text
**Sensitive Data:**
- User and group data from directories should be handled securely
- Avoid exposing sensitive information in error messages
- Sanitize data before logging
- Be cautious with data persistence
**Input Validation:**
- Validate and sanitize data from external directory services
- Check for injection vulnerabilities (LDAP injection, etc.)
- Validate configuration inputs from users
**API Security:**
- Ensure authentication flows are implemented correctly
- Verify SSL/TLS is used for all external connections
- Check for secure token storage and refresh mechanisms
## Error Handling
**Best Practices:**
1. **Try-catch for async operations** - Always wrap external API calls
2. **Meaningful error messages** - Provide context for debugging
3. **Error propagation** - Don't swallow errors silently
4. **User-facing errors** - Separate user messages from developer logs
## Performance Best Practices
**Large Dataset Handling:**
- Use pagination for large user/group lists
- Avoid loading entire datasets into memory at once
- Consider streaming or batch processing for large operations
**API Rate Limiting:**
- Respect rate limits for Microsoft Graph API, Google Admin SDK, etc.
- Consider batching large API calls where necessary
**Memory Management:**
- Close connections and clean up resources
- Remove event listeners when components are destroyed
- Be cautious with caching large datasets
## Testing
**Framework:**
- Jest with jest-preset-angular
- jest-mock-extended for type-safe mocks with `mock<Type>()`
**Test Organization:**
- Tests colocated with source files
- `*.spec.ts` - Unit tests for individual components/services
- `*.integration.spec.ts` - Integration tests against live directory services
- Test helpers located in `utils/` directory
**Test Naming:**
- Descriptive, human-readable test names
- Example: `'should return empty array when no users exist in directory'`
**Test Coverage:**
- New features must include tests
- Bug fixes should include regression tests
- Changes to core sync logic or directory specific logic require integration tests
**Testing Approach:**
- **Unit tests**: Mock external API calls using jest-mock-extended
- **Integration tests**: Use live directory services (Docker containers or configured cloud services)
- Focus on critical paths (authentication, sync, data transformation)
- Test error scenarios and edge cases (empty results, malformed data, connection failures), not just happy paths
## Directory Service Patterns
### IDirectoryService Interface
All directory services implement this core interface with methods:
- `getUsers()` - Retrieve users from directory and transform them into standard objects
- `getGroups()` - Retrieve groups from directory and transform them into standard objects
- Connection and authentication handling
### Service-Specific Implementations
Each directory service has unique authentication and query patterns:
- **LDAP**: Direct LDAP queries, bind authentication
- **Microsoft Entra ID**: Microsoft Graph API, OAuth tokens
- **Google Workspace**: Google Admin SDK, service account credentials
- **Okta/OneLogin**: REST APIs with API tokens
## References
- [Architectural Decision Records (ADRs)](https://contributing.bitwarden.com/architecture/adr/)
- [Contributing Guidelines](https://contributing.bitwarden.com/contributing/)
- [Code Style](https://contributing.bitwarden.com/contributing/code-style/)
- [Security Whitepaper](https://bitwarden.com/help/bitwarden-security-white-paper/)
- [Security Definitions](https://contributing.bitwarden.com/architecture/security/definitions)

View File

@@ -0,0 +1,27 @@
Please review this pull request with a focus on:
- Code quality and best practices
- Potential bugs or issues
- Security implications
- Performance considerations
Note: The PR branch is already checked out in the current working directory.
Provide a comprehensive review including:
- Summary of changes since last review
- Critical issues found (be thorough)
- Suggested improvements (be thorough)
- Good practices observed (be concise - list only the most notable items without elaboration)
- Action items for the author
- Leverage collapsible <details> sections where appropriate for lengthy explanations or code
snippets to enhance human readability
When reviewing subsequent commits:
- Track status of previously identified issues (fixed/unfixed/reopened)
- Identify NEW problems introduced since last review
- Note if fixes introduced new issues
IMPORTANT: Be comprehensive about issues and improvements. For good practices, be brief - just note
what was done well without explaining why or praising excessively.

5
.github/CODEOWNERS vendored
View File

@@ -12,3 +12,8 @@
**/*.dockerignore @bitwarden/team-appsec @bitwarden/dept-bre
**/entrypoint.sh @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

View File

@@ -8,12 +8,6 @@
matchManagers: ["github-actions"],
matchUpdateTypes: ["minor", "patch"],
},
{
groupName: "Google Libraries",
matchPackagePatterns: ["google-auth-library", "googleapis"],
matchManagers: ["npm"],
groupSlug: "google-libraries",
},
],
ignoreDeps: [
// yao-pkg is used to create a single executable application bundle for the CLI.
@@ -21,5 +15,10 @@
// 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.
"@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",
],
}

View File

@@ -56,7 +56,7 @@ jobs:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -111,7 +111,7 @@ jobs:
fi
- name: Upload Linux Zip to GitHub
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
@@ -120,7 +120,7 @@ jobs:
macos-cli:
name: Build Mac CLI
runs-on: macos-13
runs-on: macos-15
needs: setup
permissions:
contents: read
@@ -134,7 +134,7 @@ jobs:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -182,7 +182,7 @@ jobs:
fi
- name: Upload Mac Zip to GitHub
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
@@ -209,7 +209,7 @@ jobs:
choco install checksum --no-progress
- name: Set up Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -258,7 +258,7 @@ jobs:
}
- name: Upload Windows Zip to GitHub
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
@@ -284,7 +284,7 @@ jobs:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -338,28 +338,28 @@ jobs:
SIGNING_CERT_NAME: ${{ steps.retrieve-secrets.outputs.code-signing-cert-name }}
- name: Upload Portable Executable to GitHub
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload Installer Executable to GitHub
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload Installer Executable Blockmap to GitHub
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: latest.yml
path: ./dist/latest.yml
@@ -384,7 +384,7 @@ jobs:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -411,14 +411,14 @@ jobs:
run: npm run dist:lin
- name: Upload AppImage
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: latest-linux.yml
path: ./dist/latest-linux.yml
@@ -427,7 +427,7 @@ jobs:
macos-gui:
name: Build MacOS GUI
runs-on: macos-13
runs-on: macos-15
needs: setup
permissions:
contents: read
@@ -444,7 +444,7 @@ jobs:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -542,28 +542,28 @@ jobs:
CSC_FOR_PULL_REQUEST: true
- name: Upload .zip artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
if-no-files-found: error
- name: Upload .dmg artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
if-no-files-found: error
- name: Upload .dmg Blockmap artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: latest-mac.yml
path: ./dist/latest-mac.yml

View File

@@ -2,25 +2,36 @@ name: Integration Testing
on:
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:
branches:
- "main"
- 'main'
- 'rc'
paths:
- ".github/workflows/integration-test.yml" # this file
- "src/services/ldap-directory.service*" # we only have integration for LDAP testing at the moment
- "./openldap/**/*" # any change to test fixtures
- "./docker-compose.yml" # any change to Docker configuration
- "./package.json" # dependencies
- "docker-compose.yml" # any change to Docker configuration
- "package.json" # dependencies
- "utils/**" # any change to test fixtures
- "src/services/sync.service.ts" # core sync service used by all directory services
- "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:
paths:
- ".github/workflows/integration-test.yml" # this file
- "src/services/ldap-directory.service*" # we only have integration for LDAP testing at the moment
- "./openldap/**/*" # any change to test fixtures
- "./docker-compose.yml" # any change to Docker configuration
- "./package.json" # dependencies
- "docker-compose.yml" # any change to Docker configuration
- "package.json" # dependencies
- "utils/**" # any change to test fixtures
- "src/services/sync.service.ts" # core sync service used by all directory services
- "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:
contents: read
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:
testing:
name: Run tests
@@ -41,7 +52,7 @@ jobs:
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
- name: Set up Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -50,23 +61,81 @@ jobs:
- name: Install Node dependencies
run: npm ci
- name: Install mkcert
# Get secrets from Azure Key Vault
- 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: |
sudo apt-get update
sudo apt-get -y install mkcert
npm run test:integration:setup
- name: Setup integration tests
run: npm run test:integration:setup
- name: Run LDAP integration tests
if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.ldap == 'true'
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
- name: Run integration tests
run: npm run test:integration --coverage
# Google Workspace
- name: Run Google Workspace integration tests
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
id: report
uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1
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:
name: Test Results
path: "junit.xml"
path: "junit.xml*"
reporter: jest-junit
fail-on-error: true

28
.github/workflows/respond.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
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 Normal file
View File

@@ -0,0 +1,21 @@
name: Code Review
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
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

View File

@@ -34,7 +34,7 @@ jobs:
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
- name: Set up Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -54,7 +54,9 @@ jobs:
- name: Report test results
uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1
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:
name: Test Results
path: "junit.xml"

7
.gitignore vendored
View File

@@ -2,6 +2,9 @@
.DS_Store
Thumbs.db
# Environment variables used for tests
.env
# IDEs and editors
.idea/
.project
@@ -30,8 +33,8 @@ build-cli
.angular/cache
# Testing
coverage
junit.xml
coverage*
junit.xml*
# Misc
*.crx

View File

@@ -11,8 +11,8 @@ services:
- LDAP_TLS_KEY_FILE=/certs/openldap-key.pem
- LDAP_TLS_CA_FILE=/certs/rootCA.pem
volumes:
- "./openldap/ldifs:/ldifs"
- "./openldap/certs:/certs"
- "./utils/openldap/ldifs:/ldifs"
- "./utils/openldap/certs:/certs"
ports:
- "1389:1389"
- "1636:1636"

View File

@@ -1,4 +1,4 @@
import { InjectFlags, InjectOptions, Injector, ProviderToken } from "@angular/core";
import { InjectOptions, Injector, ProviderToken } from "@angular/core";
export class ModalInjector implements Injector {
constructor(
@@ -12,8 +12,7 @@ export class ModalInjector implements Injector {
options: InjectOptions & { optional?: false },
): T;
get<T>(token: ProviderToken<T>, notFoundValue: null, 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<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions): T;
get(token: any, notFoundValue?: any): any;
get(token: any, notFoundValue?: any, flags?: any): any {
return this._additionalTokens.get(token) ?? this._parentInjector.get<any>(token, notFoundValue);

View File

@@ -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 ./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

10378
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@bitwarden/directory-connector",
"productName": "Bitwarden Directory Connector",
"description": "Sync your user directory to your Bitwarden organization.",
"version": "2025.9.0",
"version": "2025.11.0",
"keywords": [
"bitwarden",
"password",
@@ -69,19 +69,19 @@
"test:watch:all": "jest --watchAll --testPathIgnorePatterns=.integration.spec.ts",
"test:integration": "jest .integration.spec.ts",
"test:integration:watch": "jest .integration.spec.ts --watch",
"test:integration:setup": "sh ./openldap/mkcert.sh && docker compose up -d",
"test:integration:setup": "sh ./utils/openldap/mkcert.sh && docker compose up -d",
"test:types": "npx tsc --noEmit"
},
"devDependencies": {
"@angular-devkit/build-angular": "19.2.15",
"@angular-eslint/eslint-plugin-template": "19.8.0",
"@angular-eslint/template-parser": "19.8.0",
"@angular/compiler-cli": "19.2.14",
"@angular-devkit/build-angular": "20.3.3",
"@angular-eslint/eslint-plugin-template": "20.6.0",
"@angular-eslint/template-parser": "20.6.0",
"@angular/compiler-cli": "20.3.3",
"@electron/notarize": "2.5.0",
"@electron/rebuild": "4.0.1",
"@fluffy-spoon/substitute": "1.208.0",
"@microsoft/microsoft-graph-types": "2.40.0",
"@ngtools/webpack": "19.2.14",
"@microsoft/microsoft-graph-types": "2.43.1",
"@ngtools/webpack": "20.3.3",
"@types/inquirer": "8.2.10",
"@types/jest": "29.5.14",
"@types/lowdb": "1.0.15",
@@ -90,8 +90,8 @@
"@types/node-forge": "1.3.11",
"@types/proper-lockfile": "4.1.4",
"@types/tldjs": "2.3.4",
"@typescript-eslint/eslint-plugin": "8.43.0",
"@typescript-eslint/parser": "8.43.0",
"@typescript-eslint/eslint-plugin": "8.46.0",
"@typescript-eslint/parser": "8.46.0",
"@yao-pkg/pkg": "5.16.1",
"clean-webpack-plugin": "4.0.0",
"concurrently": "9.2.0",
@@ -99,7 +99,7 @@
"cross-env": "7.0.3",
"css-loader": "7.1.2",
"dotenv": "17.2.0",
"electron": "38.1.0",
"electron": "39.1.0",
"electron-builder": "24.13.3",
"electron-log": "5.4.1",
"electron-reload": "2.0.0-alpha.1",
@@ -112,6 +112,7 @@
"eslint-plugin-rxjs": "5.0.3",
"eslint-plugin-rxjs-angular": "2.0.1",
"form-data": "4.0.4",
"glob": "11.1.0",
"html-loader": "5.1.0",
"html-webpack-plugin": "5.6.3",
"husky": "9.1.7",
@@ -119,55 +120,52 @@
"jest-junit": "16.0.0",
"jest-mock-extended": "3.0.7",
"jest-preset-angular": "14.6.0",
"lint-staged": "16.1.2",
"lint-staged": "16.2.6",
"mini-css-extract-plugin": "2.9.2",
"node-abi": "3.77.0",
"minimatch": "5.1.2",
"node-forge": "1.3.1",
"node-loader": "2.1.0",
"prettier": "3.6.2",
"rimraf": "6.0.1",
"rxjs": "7.8.2",
"sass": "1.92.1",
"sass": "1.93.2",
"sass-loader": "16.0.5",
"ts-jest": "29.4.1",
"ts-loader": "9.5.2",
"tsconfig-paths-webpack-plugin": "4.2.0",
"type-fest": "4.41.0",
"type-fest": "5.0.1",
"typescript": "5.8.3",
"webpack": "5.101.0",
"webpack": "5.102.1",
"webpack-cli": "6.0.1",
"webpack-merge": "6.0.1",
"webpack-node-externals": "3.0.0",
"zone.js": "0.15.1"
},
"dependencies": {
"@angular/animations": "19.2.14",
"@angular/cdk": "19.2.14",
"@angular/cli": "19.2.14",
"@angular/common": "19.2.14",
"@angular/compiler": "19.2.14",
"@angular/core": "19.2.14",
"@angular/forms": "19.2.14",
"@angular/platform-browser": "19.2.14",
"@angular/platform-browser-dynamic": "19.2.14",
"@angular/router": "19.2.14",
"@angular/animations": "20.3.3",
"@angular/cdk": "20.2.7",
"@angular/cli": "20.3.3",
"@angular/common": "20.3.3",
"@angular/compiler": "20.3.3",
"@angular/core": "20.3.3",
"@angular/forms": "20.3.3",
"@angular/platform-browser": "20.3.3",
"@angular/platform-browser-dynamic": "20.3.3",
"@angular/router": "20.3.3",
"@microsoft/microsoft-graph-client": "3.0.7",
"big-integer": "1.6.52",
"bootstrap": "5.3.7",
"browser-hrtime": "1.1.8",
"chalk": "4.1.2",
"commander": "14.0.0",
"core-js": "3.44.0",
"form-data": "4.0.4",
"google-auth-library": "10.3.0",
"googleapis": "153.0.0",
"googleapis-common": "8.0.0",
"googleapis": "149.0.0",
"https-proxy-agent": "7.0.6",
"inquirer": "8.2.6",
"keytar": "7.9.0",
"ldapts": "8.0.1",
"lowdb": "1.0.0",
"ngx-toastr": "19.0.0",
"ngx-toastr": "19.1.0",
"node-fetch": "2.7.0",
"parse5": "8.0.0",
"proper-lockfile": "4.1.2",

View File

@@ -1,5 +1,5 @@
import { DirectoryType } from "@/src/enums/directoryType";
import { IDirectoryService } from "@/src/services/directory.service";
import { IDirectoryService } from "@/src/services/directory-services/directory.service";
export abstract class DirectoryFactoryService {
abstract createService(type: DirectoryType): IDirectoryService;

View File

@@ -1,6 +1,3 @@
// 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 { NgModule } from "@angular/core";

View File

@@ -768,5 +768,8 @@
},
"launchWebVault": {
"message": "Launch Web Vault"
},
"authenticationFailed": {
"message": "Authentication failed"
}
}

View File

@@ -2,8 +2,8 @@ import { GetUniqueString } from "@/jslib/common/spec/utils";
import { UserEntry } from "@/src/models/userEntry";
import { groupSimulator, userSimulator } from "../../utils/request-builder-helper";
import { RequestBuilderOptions } from "../abstractions/request-builder.service";
import { groupSimulator, userSimulator } from "../utils/request-builder-helper";
import { BatchRequestBuilder } from "./batch-request-builder";

View File

@@ -5,11 +5,11 @@ import { DirectoryFactoryService } from "../abstractions/directory-factory.servi
import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType";
import { EntraIdDirectoryService } from "./entra-id-directory.service";
import { GSuiteDirectoryService } from "./gsuite-directory.service";
import { LdapDirectoryService } from "./ldap-directory.service";
import { OktaDirectoryService } from "./okta-directory.service";
import { OneLoginDirectoryService } from "./onelogin-directory.service";
import { EntraIdDirectoryService } from "./directory-services/entra-id-directory.service";
import { GSuiteDirectoryService } from "./directory-services/gsuite-directory.service";
import { LdapDirectoryService } from "./directory-services/ldap-directory.service";
import { OktaDirectoryService } from "./directory-services/okta-directory.service";
import { OneLoginDirectoryService } from "./directory-services/onelogin-directory.service";
export class DefaultDirectoryFactoryService implements DirectoryFactoryService {
constructor(

View File

@@ -1,5 +1,5 @@
import { GroupEntry } from "../models/groupEntry";
import { UserEntry } from "../models/userEntry";
import { GroupEntry } from "../../models/groupEntry";
import { UserEntry } from "../../models/userEntry";
export interface IDirectoryService {
getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>;

View File

@@ -7,14 +7,14 @@ import * as graphType from "@microsoft/microsoft-graph-types";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType";
import { EntraIdConfiguration } from "../models/entraIdConfiguration";
import { GroupEntry } from "../models/groupEntry";
import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from "../models/userEntry";
import { StateService } from "../../abstractions/state.service";
import { DirectoryType } from "../../enums/directoryType";
import { EntraIdConfiguration } from "../../models/entraIdConfiguration";
import { GroupEntry } from "../../models/groupEntry";
import { SyncConfiguration } from "../../models/syncConfiguration";
import { UserEntry } from "../../models/userEntry";
import { BaseDirectoryService } from "../baseDirectory.service";
import { BaseDirectoryService } from "./baseDirectory.service";
import { IDirectoryService } from "./directory.service";
const EntraIdPublicIdentityAuthority = "login.microsoftonline.com";

View File

@@ -0,0 +1,85 @@
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]);
});
});

View File

@@ -4,14 +4,14 @@ import { admin_directory_v1, google } from "googleapis";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType";
import { GroupEntry } from "../models/groupEntry";
import { GSuiteConfiguration } from "../models/gsuiteConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from "../models/userEntry";
import { StateService } from "../../abstractions/state.service";
import { DirectoryType } from "../../enums/directoryType";
import { GroupEntry } from "../../models/groupEntry";
import { GSuiteConfiguration } from "../../models/gsuiteConfiguration";
import { SyncConfiguration } from "../../models/syncConfiguration";
import { UserEntry } from "../../models/userEntry";
import { BaseDirectoryService } from "../baseDirectory.service";
import { BaseDirectoryService } from "./baseDirectory.service";
import { IDirectoryService } from "./directory.service";
export class GSuiteDirectoryService extends BaseDirectoryService implements IDirectoryService {
@@ -253,7 +253,15 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
],
});
await this.client.authorize();
try {
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 = {
auth: this.client,

View File

@@ -1,14 +1,17 @@
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 { groupFixtures } from "../../openldap/group-fixtures";
import { userFixtures } from "../../openldap/user-fixtures";
import { DirectoryType } from "../enums/directoryType";
import { getLdapConfiguration, getSyncConfiguration } from "../utils/test-fixtures";
import { I18nService } from "../../../jslib/common/src/abstractions/i18n.service";
import { LogService } from "../../../jslib/common/src/abstractions/log.service";
import {
getLdapConfiguration,
getSyncConfiguration,
} from "../../../utils/openldap/config-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 { StateService } from "./state.service";
// These tests integrate with the OpenLDAP docker image and seed data located in the openldap folder.
// To run theses tests:
@@ -52,7 +55,7 @@ describe("ldapDirectoryService", () => {
getLdapConfiguration({
ssl: true,
startTls: true,
tlsCaPath: "./openldap/certs/rootCA.pem",
tlsCaPath: "./utils/openldap/certs/rootCA.pem",
}),
);
stateService.getSync.mockResolvedValue(getSyncConfiguration({ groups: true, users: true }));
@@ -67,7 +70,7 @@ describe("ldapDirectoryService", () => {
getLdapConfiguration({
port: 1636,
ssl: true,
sslCaPath: "./openldap/certs/rootCA.pem",
sslCaPath: "./utils/openldap/certs/rootCA.pem",
}),
);
stateService.getSync.mockResolvedValue(getSyncConfiguration({ groups: true, users: true }));

View File

@@ -7,12 +7,12 @@ import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { Utils } from "@/jslib/common/src/misc/utils";
import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType";
import { GroupEntry } from "../models/groupEntry";
import { LdapConfiguration } from "../models/ldapConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from "../models/userEntry";
import { StateService } from "../../abstractions/state.service";
import { DirectoryType } from "../../enums/directoryType";
import { GroupEntry } from "../../models/groupEntry";
import { LdapConfiguration } from "../../models/ldapConfiguration";
import { SyncConfiguration } from "../../models/syncConfiguration";
import { UserEntry } from "../../models/userEntry";
import { IDirectoryService } from "./directory.service";

View File

@@ -3,14 +3,14 @@ import * as https from "https";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType";
import { GroupEntry } from "../models/groupEntry";
import { OktaConfiguration } from "../models/oktaConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from "../models/userEntry";
import { StateService } from "../../abstractions/state.service";
import { DirectoryType } from "../../enums/directoryType";
import { GroupEntry } from "../../models/groupEntry";
import { OktaConfiguration } from "../../models/oktaConfiguration";
import { SyncConfiguration } from "../../models/syncConfiguration";
import { UserEntry } from "../../models/userEntry";
import { BaseDirectoryService } from "../baseDirectory.service";
import { BaseDirectoryService } from "./baseDirectory.service";
import { IDirectoryService } from "./directory.service";
const DelayBetweenBuildGroupCallsInMilliseconds = 500;

View File

@@ -1,14 +1,14 @@
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType";
import { GroupEntry } from "../models/groupEntry";
import { OneLoginConfiguration } from "../models/oneLoginConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from "../models/userEntry";
import { StateService } from "../../abstractions/state.service";
import { DirectoryType } from "../../enums/directoryType";
import { GroupEntry } from "../../models/groupEntry";
import { OneLoginConfiguration } from "../../models/oneLoginConfiguration";
import { SyncConfiguration } from "../../models/syncConfiguration";
import { UserEntry } from "../../models/userEntry";
import { BaseDirectoryService } from "../baseDirectory.service";
import { BaseDirectoryService } from "./baseDirectory.service";
import { IDirectoryService } from "./directory.service";
// Basic email validation: something@something.something

View File

@@ -2,8 +2,8 @@ import { GetUniqueString } from "@/jslib/common/spec/utils";
import { UserEntry } from "@/src/models/userEntry";
import { groupSimulator, userSimulator } from "../../utils/request-builder-helper";
import { RequestBuilderOptions } from "../abstractions/request-builder.service";
import { groupSimulator, userSimulator } from "../utils/request-builder-helper";
import { SingleRequestBuilder } from "./single-request-builder";

View File

@@ -7,19 +7,20 @@ import { EnvironmentService } from "@/jslib/common/src/services/environment.serv
import { I18nService } from "../../jslib/common/src/abstractions/i18n.service";
import { LogService } from "../../jslib/common/src/abstractions/log.service";
import { groupFixtures } from "../../openldap/group-fixtures";
import { userFixtures } from "../../openldap/user-fixtures";
import { getLdapConfiguration, getSyncConfiguration } from "../../utils/openldap/config-fixtures";
import { DirectoryFactoryService } from "../abstractions/directory-factory.service";
import { DirectoryType } from "../enums/directoryType";
import { getLdapConfiguration, getSyncConfiguration } from "../utils/test-fixtures";
import { BatchRequestBuilder } from "./batch-request-builder";
import { LdapDirectoryService } from "./ldap-directory.service";
import { LdapDirectoryService } from "./directory-services/ldap-directory.service";
import { SingleRequestBuilder } from "./single-request-builder";
import { StateService } from "./state.service";
import { SyncService } 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", () => {
let logService: MockProxy<LogService>;
let i18nService: MockProxy<I18nService>;

View File

@@ -6,20 +6,20 @@ import { MessagingService } from "@/jslib/common/src/abstractions/messaging.serv
import { OrganizationImportRequest } from "@/jslib/common/src/models/request/organizationImportRequest";
import { ApiService } from "@/jslib/common/src/services/api.service";
import { getSyncConfiguration } from "../../utils/openldap/config-fixtures";
import { DirectoryFactoryService } from "../abstractions/directory-factory.service";
import { DirectoryType } from "../enums/directoryType";
import { getSyncConfiguration } from "../utils/test-fixtures";
import { BatchRequestBuilder } from "./batch-request-builder";
import { LdapDirectoryService } from "./directory-services/ldap-directory.service";
import { I18nService } from "./i18n.service";
import { LdapDirectoryService } from "./ldap-directory.service";
import { SingleRequestBuilder } from "./single-request-builder";
import { StateService } from "./state.service";
import { SyncService } from "./sync.service";
import * as constants from "./sync.service";
import { groupFixtures } from "@/openldap/group-fixtures";
import { userFixtures } from "@/openldap/user-fixtures";
import { groupFixtures } from "@/utils/openldap/group-fixtures";
import { userFixtures } from "@/utils/openldap/user-fixtures";
describe("SyncService", () => {
let cryptoFunctionService: MockProxy<CryptoFunctionService>;

4
utils/.env.example Normal file
View File

@@ -0,0 +1,4 @@
GOOGLE_DOMAIN=
GOOGLE_ADMIN_USER=
GOOGLE_CLIENT_EMAIL=
GOOGLE_PRIVATE_KEY=

View File

@@ -0,0 +1,56 @@
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 ?? {}),
});

View File

@@ -0,0 +1,26 @@
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));

View File

@@ -0,0 +1,50 @@
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));

View File

@@ -1,5 +1,5 @@
import { LdapConfiguration } from "../models/ldapConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration";
import { LdapConfiguration } from "../../src/models/ldapConfiguration";
import { SyncConfiguration } from "../../src/models/syncConfiguration";
/**
* @returns a basic ldap configuration without TLS/SSL enabled. Can be overridden by passing in a partial configuration.

View File

@@ -1,6 +1,6 @@
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
const data: Jsonify<GroupEntry>[] = [

10
utils/openldap/mkcert.sh Executable file
View 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 ./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

View File

@@ -1,6 +1,6 @@
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
const data: Jsonify<UserEntry>[] = [

View File

@@ -1,7 +1,7 @@
import { GetUniqueString } from "@/jslib/common/spec/utils";
import { GroupEntry } from "../models/groupEntry";
import { UserEntry } from "../models/userEntry";
import { GroupEntry } from "../src/models/groupEntry";
import { UserEntry } from "../src/models/userEntry";
export function userSimulator(userCount: number): UserEntry[] {
const users: UserEntry[] = [];