Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, expect, it } from 'bun:test';
import {
filterGoogleWorkspaceUsersForChecks,
parseGoogleWorkspaceCheckUserFilter,
shouldIncludeGoogleWorkspaceUserForCheck,
} from '../check-user-filter';
import type { GoogleWorkspaceUser } from '../types';

const baseUser = (overrides: Partial<GoogleWorkspaceUser>): GoogleWorkspaceUser => ({
id: 'u1',
primaryEmail: 'a@example.com',
name: { givenName: 'A', familyName: 'B', fullName: 'A B' },
isAdmin: false,
isDelegatedAdmin: false,
isEnrolledIn2Sv: true,
isEnforcedIn2Sv: false,
suspended: false,
archived: false,
creationTime: '',
lastLoginTime: '',
orgUnitPath: '/Staff',
...overrides,
});

describe('parseGoogleWorkspaceCheckUserFilter', () => {
it('parses connection-style variables', () => {
const config = parseGoogleWorkspaceCheckUserFilter({
target_org_units: ['/Staff'],
sync_excluded_emails: ['skip@example.com'],
sync_included_emails: [],
sync_user_filter_mode: 'exclude',
include_suspended: 'false',
});
expect(config.targetOrgUnits).toEqual(['/Staff']);
expect(config.excludedTerms).toContain('skip@example.com');
expect(config.userFilterMode).toBe('exclude');
expect(config.includeSuspended).toBe(false);
});
});

describe('shouldIncludeGoogleWorkspaceUserForCheck', () => {
it('drops users outside target OUs', () => {
const config = parseGoogleWorkspaceCheckUserFilter({
target_org_units: ['/Engineering'],
include_suspended: 'false',
});
expect(
shouldIncludeGoogleWorkspaceUserForCheck(
baseUser({ orgUnitPath: '/Sales' }),
config,
),
).toBe(false);
expect(
shouldIncludeGoogleWorkspaceUserForCheck(
baseUser({ orgUnitPath: '/Engineering/TeamA' }),
config,
),
).toBe(true);
});

it('respects exclude email terms', () => {
const config = parseGoogleWorkspaceCheckUserFilter({
sync_excluded_emails: ['a@example.com'],
sync_user_filter_mode: 'exclude',
include_suspended: 'false',
});
expect(shouldIncludeGoogleWorkspaceUserForCheck(baseUser({}), config)).toBe(false);
});
});

describe('filterGoogleWorkspaceUsersForChecks', () => {
it('filters an array', () => {
const config = parseGoogleWorkspaceCheckUserFilter({
sync_user_filter_mode: 'include',
sync_included_emails: ['keep@example.com'],
include_suspended: 'false',
});
const users = [
baseUser({ primaryEmail: 'keep@example.com' }),
baseUser({ id: 'u2', primaryEmail: 'drop@example.com' }),
];
expect(filterGoogleWorkspaceUsersForChecks(users, config)).toHaveLength(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { matchesSyncFilterTerms, parseSyncFilterTerms } from '../../sync-filter/email-exclusion-terms';
import type { CheckVariableValues } from '../../types';
import type { GoogleWorkspaceUser } from './types';

/** Sync mode for directory users — aligned with `sync_user_filter_mode` connection variables. */
export type GoogleWorkspaceUserSyncFilterMode = 'all' | 'exclude' | 'include';

/** Parsed filter state shared by GWS checks (2FA, employee access) and aligned with employee sync. */
export interface GoogleWorkspaceCheckUserFilterConfig {
targetOrgUnits: string[] | undefined;
excludedTerms: string[];
includedTerms: string[];
userFilterMode: GoogleWorkspaceUserSyncFilterMode | undefined;
includeSuspended: boolean;
}

/**
* Reads integration variables into a filter config (org units, sync email include/exclude).
*/
export function parseGoogleWorkspaceCheckUserFilter(
variables: CheckVariableValues,
): GoogleWorkspaceCheckUserFilterConfig {
return {
targetOrgUnits: variables.target_org_units as string[] | undefined,
excludedTerms: parseSyncFilterTerms(
variables.sync_excluded_emails ?? variables.excluded_emails,
),
includedTerms: parseSyncFilterTerms(variables.sync_included_emails),
userFilterMode: variables.sync_user_filter_mode as GoogleWorkspaceUserSyncFilterMode | undefined,
includeSuspended: variables.include_suspended === 'true',
};
}

/**
* Whether a directory user should be included in a GWS security check, using the same rules as
* `sync.controller.ts` employee sync (OU first, then email terms).
*/
export function shouldIncludeGoogleWorkspaceUserForCheck(
user: GoogleWorkspaceUser,
config: GoogleWorkspaceCheckUserFilterConfig,
): boolean {
if (user.suspended && !config.includeSuspended) {
return false;
}

if (user.archived) {
return false;
}

const { targetOrgUnits } = config;
if (targetOrgUnits && targetOrgUnits.length > 0) {
const userOu = user.orgUnitPath ?? '/';
const inOrgUnit = targetOrgUnits.some(
(ou) => ou === '/' || userOu === ou || userOu.startsWith(`${ou}/`),
);
if (!inOrgUnit) {
return false;
}
}

const email = user.primaryEmail.toLowerCase();

if (config.userFilterMode === 'exclude' && config.excludedTerms.length > 0) {
return !matchesSyncFilterTerms(email, config.excludedTerms);
}

if (config.userFilterMode === 'include') {
if (config.includedTerms.length === 0) {
return true;
}
return matchesSyncFilterTerms(email, config.includedTerms);
}

return true;
}

export function filterGoogleWorkspaceUsersForChecks(
users: GoogleWorkspaceUser[],
config: GoogleWorkspaceCheckUserFilterConfig,
): GoogleWorkspaceUser[] {
return users.filter((user) => shouldIncludeGoogleWorkspaceUserForCheck(user, config));
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { TASK_TEMPLATES } from '../../../task-mappings';
import type { CheckContext, IntegrationCheck } from '../../../types';
import {
filterGoogleWorkspaceUsersForChecks,
parseGoogleWorkspaceCheckUserFilter,
} from '../check-user-filter';
import type {
GoogleWorkspaceRoleAssignmentsResponse,
GoogleWorkspaceRolesResponse,
GoogleWorkspaceUser,
GoogleWorkspaceUsersResponse,
} from '../types';
import { includeSuspendedVariable } from '../variables';
import { includeSuspendedVariable, targetOrgUnitsVariable } from '../variables';

/**
* Employee Access Review Check
Expand All @@ -18,12 +22,12 @@ export const employeeAccessCheck: IntegrationCheck = {
name: 'Employee Access Review',
description: 'Fetch all employees and their roles from Google Workspace for access review',
taskMapping: TASK_TEMPLATES.employeeAccess,
variables: [includeSuspendedVariable],
variables: [targetOrgUnitsVariable, includeSuspendedVariable],

run: async (ctx: CheckContext) => {
ctx.log('Starting Google Workspace Employee Access check');

const includeSuspended = ctx.variables.include_suspended === 'true';
const userFilterConfig = parseGoogleWorkspaceCheckUserFilter(ctx.variables);

// Fetch all roles first to build a role ID -> name map
ctx.log('Fetching available roles...');
Expand Down Expand Up @@ -123,16 +127,8 @@ export const employeeAccessCheck: IntegrationCheck = {

ctx.log(`Fetched ${allUsers.length} total users`);

// Filter users
const activeUsers = allUsers.filter((user) => {
if (user.suspended && !includeSuspended) {
return false;
}
if (user.archived) {
return false;
}
return true;
});
// Same rules as 2FA check and employee sync (sync.controller.ts)
const activeUsers = filterGoogleWorkspaceUsersForChecks(allUsers, userFilterConfig);

ctx.log(`Found ${activeUsers.length} active users after filtering`);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { TASK_TEMPLATES } from '../../../task-mappings';
import type { CheckContext, IntegrationCheck } from '../../../types';
import { matchesSyncFilterTerms, parseSyncFilterTerms } from '../../../sync-filter/email-exclusion-terms';
import {
filterGoogleWorkspaceUsersForChecks,
parseGoogleWorkspaceCheckUserFilter,
} from '../check-user-filter';
import type { GoogleWorkspaceUser, GoogleWorkspaceUsersResponse } from '../types';
import { includeSuspendedVariable, targetOrgUnitsVariable } from '../variables';

Expand All @@ -18,13 +21,7 @@ export const twoFactorAuthCheck: IntegrationCheck = {
run: async (ctx: CheckContext) => {
ctx.log('Starting Google Workspace 2FA check');

const targetOrgUnits = ctx.variables.target_org_units as string[] | undefined;
const excludedTerms = parseSyncFilterTerms(
ctx.variables.sync_excluded_emails ?? ctx.variables.excluded_emails,
);
const includedTerms = parseSyncFilterTerms(ctx.variables.sync_included_emails);
const userFilterMode = ctx.variables.sync_user_filter_mode as 'all' | 'exclude' | 'include' | undefined;
const includeSuspended = ctx.variables.include_suspended === 'true';
const userFilterConfig = parseGoogleWorkspaceCheckUserFilter(ctx.variables);

// Fetch all users with pagination
const allUsers: GoogleWorkspaceUser[] = [];
Expand Down Expand Up @@ -54,44 +51,8 @@ export const twoFactorAuthCheck: IntegrationCheck = {

ctx.log(`Fetched ${allUsers.length} total users`);

// Filter users based on settings
const usersToCheck = allUsers.filter((user) => {
// Skip suspended users unless explicitly included
if (user.suspended && !includeSuspended) {
return false;
}

// Skip archived users
if (user.archived) {
return false;
}

// Org units first, then sync email filter — same order as employee sync (sync.controller.ts)
if (targetOrgUnits && targetOrgUnits.length > 0) {
const userOu = user.orgUnitPath ?? '/';
const inOrgUnit = targetOrgUnits.some(
(ou) => ou === '/' || userOu === ou || userOu.startsWith(`${ou}/`),
);
if (!inOrgUnit) {
return false;
}
}

const email = user.primaryEmail.toLowerCase();

if (userFilterMode === 'exclude' && excludedTerms.length > 0) {
return !matchesSyncFilterTerms(email, excludedTerms);
}

if (userFilterMode === 'include') {
if (includedTerms.length === 0) {
return true;
}
return matchesSyncFilterTerms(email, includedTerms);
}

return true;
});
// Org units + sync email filter — same rules as employee sync (sync.controller.ts)
const usersToCheck = filterGoogleWorkspaceUsersForChecks(allUsers, userFilterConfig);

ctx.log(`Checking ${usersToCheck.length} users after filtering`);

Expand Down
Loading