mirror of
https://github.com/bitwarden/directory-connector
synced 2026-01-10 20:43:52 +00:00
Compare commits
1 Commits
gsuite-doc
...
renovate/n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d06f1c314b |
@@ -1,300 +0,0 @@
|
||||
# Google Workspace Directory Integration
|
||||
|
||||
This document provides technical documentation for the Google Workspace (formerly G Suite) directory integration in Bitwarden Directory Connector.
|
||||
|
||||
## Overview
|
||||
|
||||
The Google Workspace integration synchronizes users and groups from Google Workspace to Bitwarden organizations using the Google Admin SDK Directory API. The service uses a service account with domain-wide delegation to authenticate and access directory data.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Service Location
|
||||
|
||||
- **Implementation**: `src/services/directory-services/gsuite-directory.service.ts`
|
||||
- **Configuration Model**: `src/models/gsuiteConfiguration.ts`
|
||||
- **Integration Tests**: `src/services/directory-services/gsuite-directory.service.integration.spec.ts`
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
The Google Workspace integration uses **OAuth 2.0 with Service Accounts** and domain-wide delegation:
|
||||
|
||||
1. A service account is created in Google Cloud Console
|
||||
2. The service account is granted domain-wide delegation authority
|
||||
3. The service account is authorized for specific OAuth scopes in Google Workspace Admin Console
|
||||
4. The Directory Connector uses the service account's private key to generate JWT tokens
|
||||
5. JWT tokens are exchanged for access tokens to call the Admin SDK APIs
|
||||
|
||||
### Required OAuth Scopes
|
||||
|
||||
The service account must be granted the following OAuth 2.0 scopes:
|
||||
|
||||
```
|
||||
https://www.googleapis.com/auth/admin.directory.user.readonly
|
||||
https://www.googleapis.com/auth/admin.directory.group.readonly
|
||||
https://www.googleapis.com/auth/admin.directory.group.member.readonly
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Required Fields
|
||||
|
||||
| Field | Description |
|
||||
| ------------- | --------------------------------------------------------------------------------------- |
|
||||
| `clientEmail` | Service account email address (e.g., `service-account@project.iam.gserviceaccount.com`) |
|
||||
| `privateKey` | Service account private key in PEM format |
|
||||
| `adminUser` | Admin user email to impersonate for domain-wide delegation |
|
||||
| `domain` | Primary domain of the Google Workspace organization |
|
||||
|
||||
### Optional Fields
|
||||
|
||||
| Field | Description |
|
||||
| ---------- | ---------------------------------------------------------- |
|
||||
| `customer` | Customer ID for multi-domain organizations (rarely needed) |
|
||||
|
||||
### Example Configuration
|
||||
|
||||
```typescript
|
||||
{
|
||||
clientEmail: "directory-connector@my-project.iam.gserviceaccount.com",
|
||||
privateKey: "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
|
||||
adminUser: "admin@example.com",
|
||||
domain: "example.com",
|
||||
customer: "" // Usually not required
|
||||
}
|
||||
```
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### 1. Create a Service Account
|
||||
|
||||
1. Go to [Google Cloud Console](https://console.cloud.google.com)
|
||||
2. Create or select a project
|
||||
3. Navigate to **IAM & Admin** > **Service Accounts**
|
||||
4. Click **Create Service Account**
|
||||
5. Enter a name and description
|
||||
6. Click **Create and Continue**
|
||||
7. Skip granting roles (not needed for this use case)
|
||||
8. Click **Done**
|
||||
|
||||
### 2. Generate Service Account Key
|
||||
|
||||
1. Click on the newly created service account
|
||||
2. Navigate to the **Keys** tab
|
||||
3. Click **Add Key** > **Create new key**
|
||||
4. Select **JSON** format
|
||||
5. Click **Create** and download the key file
|
||||
6. Extract `client_email` and `private_key` from the JSON file
|
||||
|
||||
### 3. Enable Domain-Wide Delegation
|
||||
|
||||
1. In the service account details, click **Show Advanced Settings**
|
||||
2. Under **Domain-wide delegation**, click **Enable Google Workspace Domain-wide Delegation**
|
||||
3. Note the **Client ID** (numeric ID)
|
||||
|
||||
### 4. Authorize the Service Account in Google Workspace
|
||||
|
||||
1. Go to [Google Workspace Admin Console](https://admin.google.com)
|
||||
2. Navigate to **Security** > **API Controls** > **Domain-wide Delegation**
|
||||
3. Click **Add new**
|
||||
4. Enter the **Client ID** from step 3
|
||||
5. Enter the following OAuth scopes (comma-separated):
|
||||
```
|
||||
https://www.googleapis.com/auth/admin.directory.user.readonly,
|
||||
https://www.googleapis.com/auth/admin.directory.group.readonly,
|
||||
https://www.googleapis.com/auth/admin.directory.group.member.readonly
|
||||
```
|
||||
6. Click **Authorize**
|
||||
|
||||
### 5. Configure Directory Connector
|
||||
|
||||
Use the extracted values to configure the Directory Connector:
|
||||
|
||||
- **Client Email**: From `client_email` in the JSON key file
|
||||
- **Private Key**: From `private_key` in the JSON key file (keep the `\n` line breaks)
|
||||
- **Admin User**: Email of a super admin user in your Google Workspace domain
|
||||
- **Domain**: Your primary Google Workspace domain
|
||||
|
||||
## Sync Behavior
|
||||
|
||||
### User Synchronization
|
||||
|
||||
The service synchronizes the following user attributes:
|
||||
|
||||
| Google Workspace Field | Bitwarden Field | Notes |
|
||||
| ------------------------- | --------------------------- | ----------------------------------------- |
|
||||
| `id` | `referenceId`, `externalId` | User's unique Google ID |
|
||||
| `primaryEmail` | `email` | Normalized to lowercase |
|
||||
| `suspended` OR `archived` | `disabled` | User is disabled if suspended or archived |
|
||||
| Deleted status | `deleted` | Set to true for deleted users |
|
||||
|
||||
**Special Behavior:**
|
||||
|
||||
- The service queries both **active users** and **deleted users** separately
|
||||
- Suspended and archived users are included but marked as disabled
|
||||
- Deleted users are included with the `deleted` flag set to true
|
||||
|
||||
### Group Synchronization
|
||||
|
||||
The service synchronizes the following group attributes:
|
||||
|
||||
| Google Workspace Field | Bitwarden Field | Notes |
|
||||
| ----------------------- | --------------------------- | ------------------------ |
|
||||
| `id` | `referenceId`, `externalId` | Group's unique Google ID |
|
||||
| `name` | `name` | Group display name |
|
||||
| Members (type=USER) | `userMemberExternalIds` | Individual user members |
|
||||
| Members (type=GROUP) | `groupMemberReferenceIds` | Nested group members |
|
||||
| Members (type=CUSTOMER) | `userMemberExternalIds` | All domain users |
|
||||
|
||||
**Member Types:**
|
||||
|
||||
- **USER**: Individual user accounts (only ACTIVE status users are synced)
|
||||
- **GROUP**: Nested groups (allows group hierarchy)
|
||||
- **CUSTOMER**: Special member type that includes all users in the domain
|
||||
|
||||
### Filtering
|
||||
|
||||
#### User Filter Examples
|
||||
|
||||
```
|
||||
exclude:testuser1@bwrox.dev | testuser1@bwrox.dev # Exclude multiple users
|
||||
|orgUnitPath='/Integration testing' # Users in Integration testing Organizational unit (OU)
|
||||
exclude:testuser1@bwrox.dev | orgUnitPath='/Integration testing' # Combined filter: get users in OU excluding provided user
|
||||
|email:testuser* # Users with email starting with "testuser"
|
||||
```
|
||||
|
||||
#### Group Filter Examples
|
||||
|
||||
An important note for group filters is that it implicitly only syncs users that are in groups. For example, in the case of
|
||||
the integration test data, `admin@bwrox.dev` is not a member of any group. Therefore, the first example filter below will
|
||||
also implicitly exclude `admin@bwrox.dev`, who is not in any group. This is important because when it is paired with an
|
||||
empty user filter, this query may semantically be understood as "sync everyone not in Integration Test Group A," while in
|
||||
practice it means "Only sync members of groups not in integration Test Groups A."
|
||||
|
||||
```
|
||||
exclude:Integration Test Group A # Get all users in groups excluding the provided group.
|
||||
```
|
||||
|
||||
### User AND Group Filter Examples
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
**Filter Syntax:**
|
||||
|
||||
- Prefix with `|` for custom filters
|
||||
- Use `:` for pattern matching (supports `*` wildcard)
|
||||
- Combine multiple conditions with spaces (AND logic)
|
||||
|
||||
### Pagination
|
||||
|
||||
The service automatically handles pagination for all API calls:
|
||||
|
||||
- Users API (active and deleted)
|
||||
- Groups API
|
||||
- Group Members API
|
||||
|
||||
Each API call processes all pages using the `nextPageToken` mechanism until no more results are available.
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Errors
|
||||
|
||||
| Error | Cause | Resolution |
|
||||
| ---------------------- | ------------------------------------- | ---------------------------------------------------------- |
|
||||
| "dirConfigIncomplete" | Missing required configuration fields | Verify all required fields are provided |
|
||||
| "authenticationFailed" | Invalid credentials or unauthorized | Check service account key and domain-wide delegation setup |
|
||||
| API returns 401/403 | Missing OAuth scopes | Verify scopes are authorized in Admin Console |
|
||||
| API returns 404 | Invalid domain or customer ID | Check domain configuration |
|
||||
|
||||
### Security Considerations
|
||||
|
||||
The service implements the following security measures:
|
||||
|
||||
1. **Credential sanitization**: Error messages do not expose private keys or sensitive credentials
|
||||
2. **Secure authentication**: Uses OAuth 2.0 with JWT tokens, not API keys
|
||||
3. **Read-only access**: Only requires read-only scopes for directory data
|
||||
4. **No credential logging**: Service account credentials are not logged
|
||||
|
||||
## Testing
|
||||
|
||||
### Integration Tests
|
||||
|
||||
Integration tests are located in `src/services/directory-services/gsuite-directory.service.integration.spec.ts`.
|
||||
|
||||
**Test Coverage:**
|
||||
|
||||
- Basic sync (users and groups)
|
||||
- Sync with filters
|
||||
- Users-only sync
|
||||
- Groups-only sync
|
||||
- User filtering scenarios
|
||||
- Group filtering scenarios
|
||||
- Disabled users handling
|
||||
- Group membership scenarios
|
||||
- Error handling
|
||||
|
||||
**Running Integration Tests:**
|
||||
|
||||
Integration tests require live Google Workspace credentials:
|
||||
|
||||
1. Create a `.env` file in the `utils/` folder with:
|
||||
```
|
||||
GOOGLE_ADMIN_USER=admin@example.com
|
||||
GOOGLE_CLIENT_EMAIL=service-account@project.iam.gserviceaccount.com
|
||||
GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
|
||||
GOOGLE_DOMAIN=example.com
|
||||
```
|
||||
2. Run tests:
|
||||
|
||||
```bash
|
||||
# Run all integration tests (includes LDAP, Google Workspace, etc.)
|
||||
npm run test:integration
|
||||
|
||||
# Run only Google Workspace integration tests
|
||||
npx jest gsuite-directory.service.integration.spec.ts
|
||||
```
|
||||
|
||||
**Test Data:**
|
||||
|
||||
The integration tests expect specific test data in Google Workspace:
|
||||
|
||||
- **Users**: 5 test users in organizational unit `/Integration testing`
|
||||
- testuser1@bwrox.dev (in Group A)
|
||||
- testuser2@bwrox.dev (in Groups A & B)
|
||||
- testuser3@bwrox.dev (in Group B)
|
||||
- testuser4@bwrox.dev (no groups)
|
||||
- testuser5@bwrox.dev (disabled)
|
||||
|
||||
- **Groups**: 2 test groups with name pattern `Integration*`
|
||||
- Integration Test Group A
|
||||
- Integration Test Group B
|
||||
|
||||
## API Reference
|
||||
|
||||
### Google Admin SDK APIs Used
|
||||
|
||||
- **Users API**: `admin.users.list()`
|
||||
- [Documentation](https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/list)
|
||||
|
||||
- **Groups API**: `admin.groups.list()`
|
||||
- [Documentation](https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/list)
|
||||
|
||||
- **Members API**: `admin.members.list()`
|
||||
- [Documentation](https://developers.google.com/admin-sdk/directory/reference/rest/v1/members/list)
|
||||
|
||||
### Rate Limits
|
||||
|
||||
Google Workspace Directory API rate limits:
|
||||
|
||||
- Default: 2,400 queries per minute per user, per Google Cloud Project
|
||||
|
||||
The service does not implement rate limiting logic; it relies on API error responses.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Google Admin SDK Directory API Guide](https://developers.google.com/admin-sdk/directory/v1/guides)
|
||||
- [Service Account Authentication](https://developers.google.com/identity/protocols/oauth2/service-account)
|
||||
- [Domain-wide Delegation](https://support.google.com/a/answer/162106)
|
||||
- [Google Workspace Admin Console](https://admin.google.com)
|
||||
- [Bitwarden Directory Connector Documentation](https://bitwarden.com/help/directory-sync/)
|
||||
@@ -33,5 +33,5 @@ export function makeStaticByteArray(length: number, start = 0) {
|
||||
for (let i = 0; i < length; i++) {
|
||||
arr[i] = start + i;
|
||||
}
|
||||
return arr.buffer;
|
||||
return arr;
|
||||
}
|
||||
|
||||
@@ -26,4 +26,9 @@ export class NodeUtils {
|
||||
.on("error", (err) => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/31394257
|
||||
static bufferToArrayBuffer(buf: Buffer): ArrayBuffer {
|
||||
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export class Utils {
|
||||
Utils.global = Utils.isNode && !Utils.isBrowser ? global : window;
|
||||
}
|
||||
|
||||
static fromB64ToArray(str: string): Uint8Array<ArrayBuffer> {
|
||||
static fromB64ToArray(str: string): Uint8Array {
|
||||
if (Utils.isNode) {
|
||||
return new Uint8Array(Buffer.from(str, "base64"));
|
||||
} else {
|
||||
@@ -49,11 +49,11 @@ export class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
static fromUrlB64ToArray(str: string): Uint8Array<ArrayBuffer> {
|
||||
static fromUrlB64ToArray(str: string): Uint8Array {
|
||||
return Utils.fromB64ToArray(Utils.fromUrlB64ToB64(str));
|
||||
}
|
||||
|
||||
static fromHexToArray(str: string): Uint8Array<ArrayBuffer> {
|
||||
static fromHexToArray(str: string): Uint8Array {
|
||||
if (Utils.isNode) {
|
||||
return new Uint8Array(Buffer.from(str, "hex"));
|
||||
} else {
|
||||
@@ -65,7 +65,7 @@ export class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
static fromUtf8ToArray(str: string): Uint8Array<ArrayBuffer> {
|
||||
static fromUtf8ToArray(str: string): Uint8Array {
|
||||
if (Utils.isNode) {
|
||||
return new Uint8Array(Buffer.from(str, "utf8"));
|
||||
} else {
|
||||
@@ -78,7 +78,7 @@ export class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
static fromByteStringToArray(str: string): Uint8Array<ArrayBuffer> {
|
||||
static fromByteStringToArray(str: string): Uint8Array {
|
||||
const arr = new Uint8Array(str.length);
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
arr[i] = str.charCodeAt(i);
|
||||
@@ -99,8 +99,8 @@ export class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
static fromBufferToUrlB64(buffer: Uint8Array<ArrayBuffer>): string {
|
||||
return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer.buffer));
|
||||
static fromBufferToUrlB64(buffer: ArrayBuffer): string {
|
||||
return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer));
|
||||
}
|
||||
|
||||
static fromB64toUrlB64(b64Str: string) {
|
||||
|
||||
@@ -636,9 +636,9 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
|
||||
const encBytes = new Uint8Array(encBuf);
|
||||
const encType = encBytes[0];
|
||||
let ctBytes: Uint8Array<ArrayBuffer> = null;
|
||||
let ivBytes: Uint8Array<ArrayBuffer> = null;
|
||||
let macBytes: Uint8Array<ArrayBuffer> = null;
|
||||
let ctBytes: Uint8Array = null;
|
||||
let ivBytes: Uint8Array = null;
|
||||
let macBytes: Uint8Array = null;
|
||||
|
||||
switch (encType) {
|
||||
case EncryptionType.AesCbc128_HmacSha256_B64:
|
||||
|
||||
@@ -94,7 +94,7 @@ describe("NodeCrypto Function Service", () => {
|
||||
it("should fail with prk too small", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const f = cryptoFunctionService.hkdfExpand(
|
||||
Utils.fromB64ToArray(prk16Byte).buffer,
|
||||
Utils.fromB64ToArray(prk16Byte),
|
||||
"info",
|
||||
32,
|
||||
"sha256",
|
||||
@@ -105,7 +105,7 @@ describe("NodeCrypto Function Service", () => {
|
||||
it("should fail with outputByteSize is too large", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const f = cryptoFunctionService.hkdfExpand(
|
||||
Utils.fromB64ToArray(prk32Byte).buffer,
|
||||
Utils.fromB64ToArray(prk32Byte),
|
||||
"info",
|
||||
8161,
|
||||
"sha256",
|
||||
@@ -341,7 +341,7 @@ function testHkdf(
|
||||
utf8Key: string,
|
||||
unicodeKey: string,
|
||||
) {
|
||||
const ikm = Utils.fromB64ToArray("criAmKtfzxanbgea5/kelQ==").buffer;
|
||||
const ikm = Utils.fromB64ToArray("criAmKtfzxanbgea5/kelQ==");
|
||||
|
||||
const regularSalt = "salt";
|
||||
const utf8Salt = "üser_salt";
|
||||
@@ -393,7 +393,7 @@ function testHkdfExpand(
|
||||
it("should create valid " + algorithm + " " + outputByteSize + " byte okm", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const okm = await cryptoFunctionService.hkdfExpand(
|
||||
Utils.fromB64ToArray(b64prk).buffer,
|
||||
Utils.fromB64ToArray(b64prk),
|
||||
info,
|
||||
outputByteSize,
|
||||
algorithm,
|
||||
|
||||
138
package-lock.json
generated
138
package-lock.json
generated
@@ -55,7 +55,7 @@
|
||||
"@types/inquirer": "8.2.10",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/lowdb": "1.0.15",
|
||||
"@types/node": "22.19.2",
|
||||
"@types/node": "24.10.4",
|
||||
"@types/node-fetch": "2.6.12",
|
||||
"@types/node-forge": "1.3.11",
|
||||
"@types/proper-lockfile": "4.1.4",
|
||||
@@ -105,7 +105,7 @@
|
||||
"ts-loader": "9.5.2",
|
||||
"tsconfig-paths-webpack-plugin": "4.2.0",
|
||||
"type-fest": "5.3.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript": "5.8.3",
|
||||
"webpack": "5.104.1",
|
||||
"webpack-cli": "6.0.1",
|
||||
"webpack-merge": "6.0.1",
|
||||
@@ -600,7 +600,6 @@
|
||||
"integrity": "sha512-4JLXU0tD6OZNVqlwzm3HGEhAHufSiyv+skb7q0d2367VDMzrU1Q/ZeepvkcHH0rZie6uqEtTQQe0OEOOluH3Mg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.8",
|
||||
@@ -742,7 +741,6 @@
|
||||
"integrity": "sha512-CVskZnF38IIxVVlKWi1VCz7YH/gHMJu2IY9bD1AVoBBGIe0xA4FRXJkW2Y+EDs9vQqZTkZZljhK5gL65Ro1PeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@angular-eslint/bundled-angular-compiler": "20.7.0",
|
||||
"eslint-scope": "^9.0.0"
|
||||
@@ -772,7 +770,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.15.tgz",
|
||||
"integrity": "sha512-ikyKfhkxoqQA6JcBN0B9RaN6369sM1XYX81Id0lI58dmWCe7gYfrTp8ejqxxKftl514psQO3pkW8Gn1nJ131Gw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -957,7 +954,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.15.tgz",
|
||||
"integrity": "sha512-k4mCXWRFiOHK3bUKfWkRQQ8KBPxW8TAJuKLYCsSHPCpMz6u0eA1F0VlrnOkZVKWPI792fOaEAWH2Y4PTaXlUHw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -974,7 +970,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.15.tgz",
|
||||
"integrity": "sha512-lMicIAFAKZXa+BCZWs3soTjNQPZZXrF/WMVDinm8dQcggNarnDj4UmXgKSyXkkyqK5SLfnLsXVzrX6ndVT6z7A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -988,7 +983,6 @@
|
||||
"integrity": "sha512-8sJoxodxsfyZ8eJ5r6Bx7BCbazXYgsZ1+dE8t5u5rTQ6jNggwNtYEzkyReoD5xvP+MMtRkos3xpwq4rtFnpI6A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "7.28.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14",
|
||||
@@ -1021,7 +1015,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.15.tgz",
|
||||
"integrity": "sha512-NMbX71SlTZIY9+rh/SPhRYFJU0pMJYW7z/TBD4lqiO+b0DTOIg1k7Pg9ydJGqSjFO1Z4dQaA6TteNuF99TJCNw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -1065,7 +1058,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.15.tgz",
|
||||
"integrity": "sha512-TxRM/wTW/oGXv/3/Iohn58yWoiYXOaeEnxSasiGNS1qhbkcKtR70xzxW6NjChBUYAixz2ERkLURkpx3pI8Q6Dw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -1088,7 +1080,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.3.15.tgz",
|
||||
"integrity": "sha512-RizuRdBt0d6ongQ2y8cr8YsXFyjF8f91vFfpSNw+cFj+oiEmRC1txcWUlH5bPLD9qSDied8qazUi0Tb8VPQDGw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -1151,7 +1142,6 @@
|
||||
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@@ -3016,7 +3006,6 @@
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -4451,7 +4440,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.2.tgz",
|
||||
"integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@inquirer/checkbox": "^4.2.1",
|
||||
"@inquirer/confirm": "^5.1.14",
|
||||
@@ -4948,7 +4936,6 @@
|
||||
"integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/environment": "^29.7.0",
|
||||
"@jest/expect": "^29.7.0",
|
||||
@@ -7872,13 +7859,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz",
|
||||
"integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==",
|
||||
"version": "24.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz",
|
||||
"integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
@@ -8127,7 +8113,6 @@
|
||||
"integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.50.0",
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
@@ -8235,7 +8220,6 @@
|
||||
"integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
@@ -8311,7 +8295,6 @@
|
||||
"integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.50.0",
|
||||
@@ -9054,7 +9037,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -9156,7 +9138,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -9436,6 +9417,7 @@
|
||||
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"archiver-utils": "^2.1.0",
|
||||
"async": "^3.2.4",
|
||||
@@ -9455,6 +9437,7 @@
|
||||
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.1.4",
|
||||
"graceful-fs": "^4.2.0",
|
||||
@@ -9477,6 +9460,7 @@
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -9489,6 +9473,7 @@
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
@@ -9509,7 +9494,8 @@
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/archiver-utils/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
@@ -9517,6 +9503,7 @@
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
@@ -9530,6 +9517,7 @@
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
@@ -9545,7 +9533,8 @@
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/archiver-utils/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
@@ -9553,6 +9542,7 @@
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
@@ -10320,7 +10310,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -10819,7 +10808,6 @@
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
@@ -11202,6 +11190,7 @@
|
||||
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"buffer-crc32": "^0.2.13",
|
||||
"crc32-stream": "^4.0.2",
|
||||
@@ -11675,6 +11664,7 @@
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
},
|
||||
@@ -11688,6 +11678,7 @@
|
||||
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"crc-32": "^1.2.0",
|
||||
"readable-stream": "^3.4.0"
|
||||
@@ -12334,7 +12325,6 @@
|
||||
"integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "24.13.3",
|
||||
"builder-util": "24.13.1",
|
||||
@@ -12674,6 +12664,7 @@
|
||||
"integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "24.13.3",
|
||||
"archiver": "^5.3.1",
|
||||
@@ -12687,6 +12678,7 @@
|
||||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
@@ -12958,6 +12950,23 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/electron/node_modules/@types/node": {
|
||||
"version": "22.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz",
|
||||
"integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron/node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emittery": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
|
||||
@@ -13402,7 +13411,6 @@
|
||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -13589,7 +13597,6 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -14075,7 +14082,6 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
|
||||
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.0",
|
||||
@@ -15564,7 +15570,6 @@
|
||||
"integrity": "sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/html-minifier-terser": "^6.0.0",
|
||||
"html-minifier-terser": "^6.0.2",
|
||||
@@ -16975,7 +16980,6 @@
|
||||
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "^29.7.0",
|
||||
"@jest/types": "^29.6.3",
|
||||
@@ -17274,7 +17278,6 @@
|
||||
"integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/environment": "^29.7.0",
|
||||
"@jest/fake-timers": "^29.7.0",
|
||||
@@ -17841,7 +17844,6 @@
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
@@ -18161,6 +18163,7 @@
|
||||
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"readable-stream": "^2.0.5"
|
||||
},
|
||||
@@ -18173,7 +18176,8 @@
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lazystream/node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
@@ -18181,6 +18185,7 @@
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
@@ -18196,7 +18201,8 @@
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lazystream/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
@@ -18204,6 +18210,7 @@
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
@@ -18282,7 +18289,6 @@
|
||||
"integrity": "sha512-kdTwsyRuncDfjEs0DlRILWNvxhDG/Zij4YLO4TMJgDLW+8OzpfkdPnRgrsRuY1o+oaxJGWsps5f/RVBgGmmN0w==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"copy-anything": "^2.0.1",
|
||||
"parse-node-version": "^1.0.1",
|
||||
@@ -18648,7 +18654,6 @@
|
||||
"resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.1.tgz",
|
||||
"integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cli-truncate": "^4.0.0",
|
||||
"colorette": "^2.0.20",
|
||||
@@ -18884,14 +18889,16 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.difference": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
|
||||
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.escaperegexp": {
|
||||
"version": "4.1.2",
|
||||
@@ -18905,7 +18912,8 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
||||
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
@@ -18920,7 +18928,8 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
@@ -18941,7 +18950,8 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
||||
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/log-symbols": {
|
||||
"version": "6.0.0",
|
||||
@@ -21489,7 +21499,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -22060,6 +22069,7 @@
|
||||
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"minimatch": "^5.1.0"
|
||||
}
|
||||
@@ -22771,7 +22781,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
@@ -22873,7 +22882,6 @@
|
||||
"integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
@@ -24301,7 +24309,6 @@
|
||||
"integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.14.0",
|
||||
@@ -24868,8 +24875,7 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tuf-js": {
|
||||
"version": "3.1.0",
|
||||
@@ -25036,12 +25042,11 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -25084,9 +25089,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
@@ -25183,7 +25188,6 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"napi-postinstall": "^0.3.0"
|
||||
},
|
||||
@@ -25393,7 +25397,6 @@
|
||||
"integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -25559,7 +25562,6 @@
|
||||
"integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.8",
|
||||
@@ -25609,7 +25611,6 @@
|
||||
"integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@discoveryjs/json-ext": "^0.6.1",
|
||||
"@webpack-cli/configtest": "^3.0.1",
|
||||
@@ -25716,7 +25717,6 @@
|
||||
"integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/bonjour": "^3.5.13",
|
||||
"@types/connect-history-api-fallback": "^1.5.4",
|
||||
@@ -26714,7 +26714,6 @@
|
||||
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
@@ -26850,6 +26849,7 @@
|
||||
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"archiver-utils": "^3.0.4",
|
||||
"compress-commons": "^4.1.2",
|
||||
@@ -26865,6 +26865,7 @@
|
||||
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.2.3",
|
||||
"graceful-fs": "^4.2.0",
|
||||
@@ -26887,6 +26888,7 @@
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -26899,6 +26901,7 @@
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
@@ -26920,6 +26923,7 @@
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
@@ -26932,7 +26936,6 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@@ -26951,8 +26954,7 @@
|
||||
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
|
||||
"integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
"@types/inquirer": "8.2.10",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/lowdb": "1.0.15",
|
||||
"@types/node": "22.19.2",
|
||||
"@types/node": "24.10.4",
|
||||
"@types/node-fetch": "2.6.12",
|
||||
"@types/node-forge": "1.3.11",
|
||||
"@types/proper-lockfile": "4.1.4",
|
||||
@@ -135,7 +135,7 @@
|
||||
"ts-loader": "9.5.2",
|
||||
"tsconfig-paths-webpack-plugin": "4.2.0",
|
||||
"type-fest": "5.3.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript": "5.8.3",
|
||||
"webpack": "5.104.1",
|
||||
"webpack-cli": "6.0.1",
|
||||
"webpack-merge": "6.0.1",
|
||||
|
||||
@@ -50,221 +50,36 @@ describe("gsuiteDirectoryService", () => {
|
||||
directoryService = new GSuiteDirectoryService(logService, i18nService, stateService);
|
||||
});
|
||||
|
||||
describe("basic sync fetching users and groups", () => {
|
||||
it("syncs without using filters (includes test data)", async () => {
|
||||
const directoryConfig = getGSuiteConfiguration();
|
||||
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig);
|
||||
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));
|
||||
const syncConfig = getSyncConfiguration({
|
||||
groups: true,
|
||||
users: true,
|
||||
});
|
||||
stateService.getSync.mockResolvedValue(syncConfig);
|
||||
|
||||
it("syncs using user and group filters (exact match for test data)", async () => {
|
||||
const directoryConfig = getGSuiteConfiguration();
|
||||
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig);
|
||||
const result = await directoryService.getEntries(true, true);
|
||||
|
||||
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]);
|
||||
});
|
||||
|
||||
it("syncs only users when groups sync is disabled", async () => {
|
||||
const directoryConfig = getGSuiteConfiguration();
|
||||
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig);
|
||||
|
||||
const syncConfig = getSyncConfiguration({
|
||||
groups: false,
|
||||
users: true,
|
||||
userFilter: INTEGRATION_USER_FILTER,
|
||||
});
|
||||
stateService.getSync.mockResolvedValue(syncConfig);
|
||||
|
||||
const result = await directoryService.getEntries(true, true);
|
||||
|
||||
expect(result[0]).toBeUndefined();
|
||||
expect(result[1]).toEqual(expect.arrayContaining(userFixtures));
|
||||
});
|
||||
|
||||
it("syncs only groups when users sync is disabled", async () => {
|
||||
const directoryConfig = getGSuiteConfiguration();
|
||||
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig);
|
||||
|
||||
const syncConfig = getSyncConfiguration({
|
||||
groups: true,
|
||||
users: false,
|
||||
groupFilter: INTEGRATION_GROUP_FILTER,
|
||||
});
|
||||
stateService.getSync.mockResolvedValue(syncConfig);
|
||||
|
||||
const result = await directoryService.getEntries(true, true);
|
||||
|
||||
expect(result[0]).toEqual(expect.arrayContaining(groupFixtures));
|
||||
expect(result[1]).toEqual([]);
|
||||
});
|
||||
expect(result[0]).toEqual(expect.arrayContaining(groupFixtures));
|
||||
expect(result[1]).toEqual(expect.arrayContaining(userFixtures));
|
||||
});
|
||||
|
||||
describe("users", () => {
|
||||
it("includes disabled users in sync results", async () => {
|
||||
const directoryConfig = getGSuiteConfiguration();
|
||||
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig);
|
||||
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({
|
||||
users: true,
|
||||
userFilter: INTEGRATION_USER_FILTER,
|
||||
});
|
||||
stateService.getSync.mockResolvedValue(syncConfig);
|
||||
|
||||
const result = await directoryService.getEntries(true, true);
|
||||
|
||||
const disabledUser = userFixtures.find((u) => u.email === "testuser5@bwrox.dev");
|
||||
expect(result[1]).toContainEqual(disabledUser);
|
||||
expect(disabledUser.disabled).toBe(true);
|
||||
const syncConfig = getSyncConfiguration({
|
||||
groups: true,
|
||||
users: true,
|
||||
userFilter: INTEGRATION_USER_FILTER,
|
||||
groupFilter: INTEGRATION_GROUP_FILTER,
|
||||
});
|
||||
stateService.getSync.mockResolvedValue(syncConfig);
|
||||
|
||||
it("filters users by org unit path", async () => {
|
||||
const directoryConfig = getGSuiteConfiguration();
|
||||
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig);
|
||||
const result = await directoryService.getEntries(true, true);
|
||||
|
||||
const syncConfig = getSyncConfiguration({
|
||||
users: true,
|
||||
userFilter: INTEGRATION_USER_FILTER,
|
||||
});
|
||||
stateService.getSync.mockResolvedValue(syncConfig);
|
||||
|
||||
const result = await directoryService.getEntries(true, true);
|
||||
|
||||
expect(result[1]).toEqual(userFixtures);
|
||||
expect(result[1].length).toBe(5);
|
||||
});
|
||||
|
||||
it("filters users by email pattern", async () => {
|
||||
const directoryConfig = getGSuiteConfiguration();
|
||||
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig);
|
||||
|
||||
const syncConfig = getSyncConfiguration({
|
||||
users: true,
|
||||
userFilter: "|email:testuser1*",
|
||||
});
|
||||
stateService.getSync.mockResolvedValue(syncConfig);
|
||||
|
||||
const result = await directoryService.getEntries(true, true);
|
||||
|
||||
const testuser1 = userFixtures.find((u) => u.email === "testuser1@bwrox.dev");
|
||||
expect(result[1]).toContainEqual(testuser1);
|
||||
expect(result[1].length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("groups", () => {
|
||||
it("filters groups by name pattern", 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[0]).toEqual(groupFixtures);
|
||||
expect(result[0].length).toBe(2);
|
||||
});
|
||||
|
||||
it("includes group members correctly", 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);
|
||||
|
||||
const groupA = result[0].find((g) => g.name === "Integration Test Group A");
|
||||
expect(groupA).toBeDefined();
|
||||
expect(groupA.userMemberExternalIds.size).toBe(2);
|
||||
expect(groupA.userMemberExternalIds.has("111605910541641314041")).toBe(true);
|
||||
expect(groupA.userMemberExternalIds.has("111147009830456099026")).toBe(true);
|
||||
|
||||
const groupB = result[0].find((g) => g.name === "Integration Test Group B");
|
||||
expect(groupB).toBeDefined();
|
||||
expect(groupB.userMemberExternalIds.size).toBe(2);
|
||||
expect(groupB.userMemberExternalIds.has("111147009830456099026")).toBe(true);
|
||||
expect(groupB.userMemberExternalIds.has("100150970267699397306")).toBe(true);
|
||||
});
|
||||
|
||||
it("handles groups with no members", async () => {
|
||||
const directoryConfig = getGSuiteConfiguration();
|
||||
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig);
|
||||
|
||||
const syncConfig = getSyncConfiguration({
|
||||
groups: true,
|
||||
users: true,
|
||||
userFilter: INTEGRATION_USER_FILTER,
|
||||
groupFilter: "|name:Integration*",
|
||||
});
|
||||
stateService.getSync.mockResolvedValue(syncConfig);
|
||||
|
||||
const result = await directoryService.getEntries(true, true);
|
||||
|
||||
// All test groups should have members, but ensure the code handles empty groups
|
||||
expect(result[0]).toBeDefined();
|
||||
expect(Array.isArray(result[0])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
it("throws error when directory configuration is incomplete", async () => {
|
||||
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(
|
||||
getGSuiteConfiguration({
|
||||
clientEmail: "",
|
||||
}),
|
||||
);
|
||||
|
||||
const syncConfig = getSyncConfiguration({
|
||||
users: true,
|
||||
});
|
||||
stateService.getSync.mockResolvedValue(syncConfig);
|
||||
|
||||
await expect(directoryService.getEntries(true, true)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("throws error when authentication fails with invalid credentials", async () => {
|
||||
const directoryConfig = getGSuiteConfiguration({
|
||||
privateKey: "-----BEGIN PRIVATE KEY-----\nINVALID_KEY\n-----END PRIVATE KEY-----\n",
|
||||
});
|
||||
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig);
|
||||
|
||||
const syncConfig = getSyncConfiguration({
|
||||
users: true,
|
||||
});
|
||||
stateService.getSync.mockResolvedValue(syncConfig);
|
||||
|
||||
await expect(directoryService.getEntries(true, true)).rejects.toThrow();
|
||||
});
|
||||
expect(result).toEqual([groupFixtures, userFixtures]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,22 +14,6 @@ import { BaseDirectoryService } from "../baseDirectory.service";
|
||||
|
||||
import { IDirectoryService } from "./directory.service";
|
||||
|
||||
/**
|
||||
* Google Workspace (formerly G Suite) Directory Service
|
||||
*
|
||||
* This service integrates with Google Workspace to synchronize users and groups
|
||||
* to Bitwarden organizations using the Google Admin SDK Directory API.
|
||||
*
|
||||
* @remarks
|
||||
* Authentication is performed using a service account with domain-wide delegation.
|
||||
* The service account must be granted the following OAuth 2.0 scopes:
|
||||
* - https://www.googleapis.com/auth/admin.directory.user.readonly
|
||||
* - https://www.googleapis.com/auth/admin.directory.group.readonly
|
||||
* - https://www.googleapis.com/auth/admin.directory.group.member.readonly
|
||||
*
|
||||
* @see {@link https://developers.google.com/admin-sdk/directory/v1/guides | Google Admin SDK Directory API}
|
||||
* @see {@link https://support.google.com/a/answer/162106 | Domain-wide delegation of authority}
|
||||
*/
|
||||
export class GSuiteDirectoryService extends BaseDirectoryService implements IDirectoryService {
|
||||
private client: JWT;
|
||||
private service: admin_directory_v1.Admin;
|
||||
@@ -46,29 +30,6 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
|
||||
this.service = google.admin("directory_v1");
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves users and groups from Google Workspace directory
|
||||
* @returns A tuple containing [groups, users] arrays
|
||||
*
|
||||
* @remarks
|
||||
* This function:
|
||||
* 1. Validates the directory type matches GSuite
|
||||
* 2. Loads directory and sync configuration
|
||||
* 3. Authenticates with Google Workspace using service account credentials
|
||||
* 4. Retrieves users (if enabled in sync config)
|
||||
* 5. Retrieves groups and their members (if enabled in sync config)
|
||||
* 6. Applies any user/group filters specified in sync configuration
|
||||
*
|
||||
* User and group filters follow Google Workspace Directory API query syntax:
|
||||
* - Use `|` prefix for custom filters (e.g., "|orgUnitPath='/Engineering'")
|
||||
* - Multiple conditions can be combined with AND/OR operators
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const [groups, users] = await service.getEntries(true, false);
|
||||
* console.log(`Synced ${users.length} users and ${groups.length} groups`);
|
||||
* ```
|
||||
*/
|
||||
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
||||
const type = await this.stateService.getDirectoryType();
|
||||
if (type !== DirectoryType.GSuite) {
|
||||
@@ -104,26 +65,6 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
|
||||
return [groups, users];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all users from Google Workspace directory
|
||||
*
|
||||
* @returns Array of UserEntry objects representing users in the directory
|
||||
*
|
||||
* @remarks
|
||||
* This method performs two separate queries:
|
||||
* 1. Active users (including suspended and archived)
|
||||
* 2. Deleted users (marked with deleted flag)
|
||||
*
|
||||
* The method handles pagination automatically, fetching all pages of results.
|
||||
* Users are filtered based on the userFilter specified in sync configuration.
|
||||
*
|
||||
* User properties mapped:
|
||||
* - referenceId: User's unique Google ID
|
||||
* - externalId: User's unique Google ID (same as referenceId)
|
||||
* - email: User's primary email address (lowercase)
|
||||
* - disabled: True if user is suspended or archived
|
||||
* - deleted: True if user is deleted from the directory
|
||||
*/
|
||||
private async getUsers(): Promise<UserEntry[]> {
|
||||
const entries: UserEntry[] = [];
|
||||
const query = this.createDirectoryQuery(this.syncConfig.userFilter);
|
||||
@@ -191,13 +132,6 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a Google Workspace user object into a UserEntry
|
||||
*
|
||||
* @param user - Google Workspace user object from the API
|
||||
* @param deleted - Whether this user is from the deleted users list
|
||||
* @returns UserEntry object or null if user data is invalid
|
||||
*/
|
||||
private buildUser(user: admin_directory_v1.Schema$User, deleted: boolean) {
|
||||
if ((user.emails == null || user.emails === "") && !deleted) {
|
||||
return null;
|
||||
@@ -212,17 +146,6 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all groups from Google Workspace directory
|
||||
*
|
||||
* @param setFilter - Tuple of [isWhitelist, Set<string>] for filtering groups
|
||||
* @param users - Array of UserEntry objects to reference when processing members
|
||||
* @returns Array of GroupEntry objects representing groups in the directory
|
||||
*
|
||||
* @remarks
|
||||
* For each group, the method also retrieves all group members by calling the
|
||||
* members API. Groups are filtered based on the groupFilter in sync configuration.
|
||||
*/
|
||||
private async getGroups(
|
||||
setFilter: [boolean, Set<string>],
|
||||
users: UserEntry[],
|
||||
@@ -262,19 +185,6 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a Google Workspace group object into a GroupEntry with members
|
||||
*
|
||||
* @param group - Google Workspace group object from the API
|
||||
* @param users - Array of UserEntry objects for reference
|
||||
* @returns GroupEntry object with all members populated
|
||||
*
|
||||
* @remarks
|
||||
* This method retrieves all members of the group, handling three member types:
|
||||
* - USER: Individual user members (only active status users are included)
|
||||
* - GROUP: Nested group members
|
||||
* - CUSTOMER: Special type that includes all users in the domain
|
||||
*/
|
||||
private async buildGroup(group: admin_directory_v1.Schema$Group, users: UserEntry[]) {
|
||||
let nextPageToken: string = null;
|
||||
|
||||
@@ -320,26 +230,6 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates with Google Workspace using service account credentials
|
||||
*
|
||||
* @throws Error if required configuration fields are missing or authentication fails
|
||||
*
|
||||
* @remarks
|
||||
* Authentication uses a JWT with the following required fields:
|
||||
* - clientEmail: Service account email address
|
||||
* - privateKey: Service account private key (PEM format)
|
||||
* - subject: Admin user email to impersonate (for domain-wide delegation)
|
||||
*
|
||||
* The service account must be configured with domain-wide delegation and granted
|
||||
* the required OAuth scopes in the Google Workspace Admin Console.
|
||||
*
|
||||
* Optional configuration:
|
||||
* - domain: Filters results to a specific domain
|
||||
* - customer: Customer ID for multi-domain organizations
|
||||
*
|
||||
* @see {@link https://developers.google.com/identity/protocols/oauth2/service-account | Service account authentication}
|
||||
*/
|
||||
private async auth() {
|
||||
if (
|
||||
this.dirConfig.clientEmail == null ||
|
||||
|
||||
Reference in New Issue
Block a user