1
0
mirror of https://github.com/bitwarden/directory-connector synced 2026-01-10 04:23:26 +00:00

Compare commits

..

2 Commits

Author SHA1 Message Date
Brandon
5761a391f7 wip 2026-01-09 17:01:26 -05:00
Brandon
8cd2850e8d add docs and tests 2026-01-09 12:05:14 -05:00
4 changed files with 623 additions and 28 deletions

300
docs/google-workspace.md Normal file
View File

@@ -0,0 +1,300 @@
# 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/)

View File

@@ -76,7 +76,7 @@
"@angular-devkit/build-angular": "20.3.3",
"@angular-eslint/eslint-plugin-template": "20.7.0",
"@angular-eslint/template-parser": "20.7.0",
"@angular/compiler-cli": "21.0.6",
"@angular/compiler-cli": "20.3.15",
"@electron/notarize": "2.5.0",
"@electron/rebuild": "4.0.1",
"@fluffy-spoon/substitute": "1.208.0",
@@ -143,16 +143,16 @@
"zone.js": "0.15.1"
},
"dependencies": {
"@angular/animations": "21.0.6",
"@angular/animations": "20.3.15",
"@angular/cdk": "20.2.14",
"@angular/cli": "20.3.3",
"@angular/common": "21.0.6",
"@angular/common": "20.3.15",
"@angular/compiler": "20.3.15",
"@angular/core": "20.3.15",
"@angular/forms": "21.0.6",
"@angular/platform-browser": "21.0.6",
"@angular/platform-browser-dynamic": "21.0.6",
"@angular/router": "21.0.6",
"@angular/forms": "20.3.15",
"@angular/platform-browser": "20.3.15",
"@angular/platform-browser-dynamic": "20.3.15",
"@angular/router": "20.3.15",
"@microsoft/microsoft-graph-client": "3.0.7",
"big-integer": "1.6.52",
"bootstrap": "5.3.7",

View File

@@ -50,36 +50,221 @@ describe("gsuiteDirectoryService", () => {
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);
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);
const syncConfig = getSyncConfiguration({
groups: true,
users: true,
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));
});
stateService.getSync.mockResolvedValue(syncConfig);
const result = await directoryService.getEntries(true, true);
it("syncs using user and group filters (exact match for test data)", async () => {
const directoryConfig = getGSuiteConfiguration();
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig);
expect(result[0]).toEqual(expect.arrayContaining(groupFixtures));
expect(result[1]).toEqual(expect.arrayContaining(userFixtures));
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([]);
});
});
it("syncs using user and group filters (exact match for test data)", async () => {
const directoryConfig = getGSuiteConfiguration();
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig);
describe("users", () => {
it("includes disabled users in sync results", 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,
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);
});
stateService.getSync.mockResolvedValue(syncConfig);
const result = await directoryService.getEntries(true, true);
it("filters users by org unit path", async () => {
const directoryConfig = getGSuiteConfiguration();
stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig);
expect(result).toEqual([groupFixtures, userFixtures]);
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();
});
});
});

View File

@@ -14,6 +14,22 @@ 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;
@@ -30,6 +46,29 @@ 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) {
@@ -65,6 +104,26 @@ 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);
@@ -132,6 +191,13 @@ 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;
@@ -146,6 +212,17 @@ 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[],
@@ -185,6 +262,19 @@ 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;
@@ -230,6 +320,26 @@ 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 ||