1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-13 14:53:16 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Thomas Rittson
b1d7920d73 Enable experimental vm modules to see if it fixes google sync error 2025-10-07 20:26:32 +10:00
72 changed files with 6865 additions and 6518 deletions

View File

@@ -1,203 +0,0 @@
# 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

@@ -1,27 +0,0 @@
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.

10
.eslintignore Normal file
View 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
View 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
View File

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

View File

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

View File

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

View File

@@ -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",
], ],
} }

View File

@@ -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@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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

View File

@@ -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@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.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,81 +50,23 @@ 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@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"
reporter: jest-junit reporter: jest-junit
fail-on-error: true fail-on-error: true

View File

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

View File

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

View File

@@ -1,21 +0,0 @@
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

@@ -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@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.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'
@@ -54,9 +54,7 @@ jobs:
- name: Report test results - name: Report test results
uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1 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"

View File

@@ -47,10 +47,9 @@ jobs:
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
View File

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

View File

@@ -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"

View File

@@ -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,
];

View File

@@ -26,6 +26,7 @@ module.exports = {
modulePaths: [compilerOptions.baseUrl], modulePaths: [compilerOptions.baseUrl],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: "<rootDir>/" }), moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: "<rootDir>/" }),
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

View File

@@ -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);

View File

@@ -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();
} }
} }

View File

@@ -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";

View File

@@ -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];
} }

View File

@@ -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;
}, },
}; };

View File

@@ -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
} }

View File

@@ -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;

View File

@@ -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;
} }
} }

View File

@@ -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
} }

View File

@@ -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;
} }

View File

@@ -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();

View File

@@ -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,

View File

@@ -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;

View File

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

View File

@@ -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>[] = [

11942
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +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.11.0", "version": "2025.9.0",
"keywords": [ "keywords": [
"bitwarden", "bitwarden",
"password", "password",
@@ -31,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/*",
@@ -49,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",
@@ -69,19 +69,19 @@
"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.6.0", "@angular-eslint/eslint-plugin-template": "19.8.0",
"@angular-eslint/template-parser": "20.6.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",
@@ -89,10 +89,9 @@
"@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.48.0", "@typescript-eslint/eslint-plugin": "8.43.0",
"@typescript-eslint/parser": "8.48.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",
@@ -100,20 +99,19 @@
"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": "11.1.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",
@@ -121,52 +119,55 @@
"jest-junit": "16.0.0", "jest-junit": "16.0.0",
"jest-mock-extended": "3.0.7", "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.94.2", "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.102.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",

View File

@@ -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;

View File

@@ -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();
}); });

View File

@@ -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";

View File

@@ -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";

View File

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

View File

@@ -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,

View File

@@ -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";

View File

@@ -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(

View File

@@ -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]);
});
});

View File

@@ -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[]]>;

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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 }));

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 { 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";

View File

@@ -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;

View File

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

View File

@@ -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";

View File

@@ -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;
}); });
}); });

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 { 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;
}); });

View File

@@ -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[] = [];

View File

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

View File

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

View File

@@ -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 ?? {}),
});

View File

@@ -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));

View File

@@ -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));

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