From cfa39e3a25696cfb45a8a7cf1539dc2d36634368 Mon Sep 17 00:00:00 2001 From: chasprowebdev Date: Sun, 22 Mar 2026 22:28:48 -0400 Subject: [PATCH 1/2] fix(integration-platform): filter GWS employees by org units and filter mode on employee-access --- .../checks/employee-access.ts | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/integration-platform/src/manifests/google-workspace/checks/employee-access.ts b/packages/integration-platform/src/manifests/google-workspace/checks/employee-access.ts index 615648e7fb..f2618a65fb 100644 --- a/packages/integration-platform/src/manifests/google-workspace/checks/employee-access.ts +++ b/packages/integration-platform/src/manifests/google-workspace/checks/employee-access.ts @@ -1,12 +1,13 @@ import { TASK_TEMPLATES } from '../../../task-mappings'; import type { CheckContext, IntegrationCheck } from '../../../types'; +import { matchesSyncFilterTerms, parseSyncFilterTerms } from '../../../sync-filter/email-exclusion-terms'; import type { GoogleWorkspaceRoleAssignmentsResponse, GoogleWorkspaceRolesResponse, GoogleWorkspaceUser, GoogleWorkspaceUsersResponse, } from '../types'; -import { includeSuspendedVariable } from '../variables'; +import { includeSuspendedVariable, targetOrgUnitsVariable } from '../variables'; /** * Employee Access Review Check @@ -18,11 +19,17 @@ 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 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'; // Fetch all roles first to build a role ID -> name map @@ -123,7 +130,7 @@ export const employeeAccessCheck: IntegrationCheck = { ctx.log(`Fetched ${allUsers.length} total users`); - // Filter users + // Filter users (same rules as 2FA check and employee sync) const activeUsers = allUsers.filter((user) => { if (user.suspended && !includeSuspended) { return false; @@ -131,6 +138,30 @@ export const employeeAccessCheck: IntegrationCheck = { if (user.archived) { return false; } + + 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; }); From 0408858b8c00b7174cf15a6384c092961e5b3c4b Mon Sep 17 00:00:00 2001 From: chasprowebdev Date: Sun, 22 Mar 2026 23:18:10 -0400 Subject: [PATCH 2/2] fix(integration-platform): remove duplcated user filtering logic across two check files --- .../__tests__/check-user-filter.test.ts | 84 +++++++++++++++++++ .../google-workspace/check-user-filter.ts | 82 ++++++++++++++++++ .../checks/employee-access.ts | 49 ++--------- .../checks/two-factor-auth.ts | 53 ++---------- 4 files changed, 180 insertions(+), 88 deletions(-) create mode 100644 packages/integration-platform/src/manifests/google-workspace/__tests__/check-user-filter.test.ts create mode 100644 packages/integration-platform/src/manifests/google-workspace/check-user-filter.ts diff --git a/packages/integration-platform/src/manifests/google-workspace/__tests__/check-user-filter.test.ts b/packages/integration-platform/src/manifests/google-workspace/__tests__/check-user-filter.test.ts new file mode 100644 index 0000000000..d32a462d52 --- /dev/null +++ b/packages/integration-platform/src/manifests/google-workspace/__tests__/check-user-filter.test.ts @@ -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 => ({ + 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); + }); +}); diff --git a/packages/integration-platform/src/manifests/google-workspace/check-user-filter.ts b/packages/integration-platform/src/manifests/google-workspace/check-user-filter.ts new file mode 100644 index 0000000000..04446131d5 --- /dev/null +++ b/packages/integration-platform/src/manifests/google-workspace/check-user-filter.ts @@ -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)); +} diff --git a/packages/integration-platform/src/manifests/google-workspace/checks/employee-access.ts b/packages/integration-platform/src/manifests/google-workspace/checks/employee-access.ts index f2618a65fb..b04a54f8c3 100644 --- a/packages/integration-platform/src/manifests/google-workspace/checks/employee-access.ts +++ b/packages/integration-platform/src/manifests/google-workspace/checks/employee-access.ts @@ -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 { GoogleWorkspaceRoleAssignmentsResponse, GoogleWorkspaceRolesResponse, @@ -24,13 +27,7 @@ export const employeeAccessCheck: IntegrationCheck = { run: async (ctx: CheckContext) => { ctx.log('Starting Google Workspace Employee Access 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 roles first to build a role ID -> name map ctx.log('Fetching available roles...'); @@ -130,40 +127,8 @@ export const employeeAccessCheck: IntegrationCheck = { ctx.log(`Fetched ${allUsers.length} total users`); - // Filter users (same rules as 2FA check and employee sync) - const activeUsers = allUsers.filter((user) => { - if (user.suspended && !includeSuspended) { - return false; - } - if (user.archived) { - return false; - } - - 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; - }); + // 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`); diff --git a/packages/integration-platform/src/manifests/google-workspace/checks/two-factor-auth.ts b/packages/integration-platform/src/manifests/google-workspace/checks/two-factor-auth.ts index e784ab03a9..3493f6dc37 100644 --- a/packages/integration-platform/src/manifests/google-workspace/checks/two-factor-auth.ts +++ b/packages/integration-platform/src/manifests/google-workspace/checks/two-factor-auth.ts @@ -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'; @@ -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[] = []; @@ -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`);