Skip to content

Commit 42331ca

Browse files
committed
feat(integration-platform): add employee access review check for Google Workspace
1 parent dbe64d2 commit 42331ca

File tree

4 files changed

+225
-2
lines changed

4 files changed

+225
-2
lines changed
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { TASK_TEMPLATES } from '../../../task-mappings';
2+
import type { CheckContext, IntegrationCheck } from '../../../types';
3+
import type {
4+
GoogleWorkspaceRoleAssignmentsResponse,
5+
GoogleWorkspaceRolesResponse,
6+
GoogleWorkspaceUser,
7+
GoogleWorkspaceUsersResponse,
8+
} from '../types';
9+
import { includeSuspendedVariable } from '../variables';
10+
11+
/**
12+
* Employee Access Review Check
13+
* Fetches all users from Google Workspace with their roles for access review.
14+
* Maps to: Access Review Log task
15+
*/
16+
export const employeeAccessCheck: IntegrationCheck = {
17+
id: 'employee-access',
18+
name: 'Employee Access Review',
19+
description: 'Fetch all employees and their roles from Google Workspace for access review',
20+
taskMapping: TASK_TEMPLATES.employeeAccess,
21+
variables: [includeSuspendedVariable],
22+
23+
run: async (ctx: CheckContext) => {
24+
ctx.log('Starting Google Workspace Employee Access check');
25+
26+
const includeSuspended = ctx.variables.include_suspended === 'true';
27+
28+
// Fetch all roles first to build a role ID -> name map
29+
ctx.log('Fetching available roles...');
30+
const roleMap = new Map<string, string>();
31+
32+
try {
33+
let rolesPageToken: string | undefined;
34+
do {
35+
const params: Record<string, string> = { customer: 'my_customer' };
36+
if (rolesPageToken) {
37+
params.pageToken = rolesPageToken;
38+
}
39+
40+
const rolesResponse = await ctx.fetch<GoogleWorkspaceRolesResponse>(
41+
'/admin/directory/v1/customer/my_customer/roles',
42+
{ params },
43+
);
44+
45+
if (rolesResponse.items) {
46+
for (const role of rolesResponse.items) {
47+
roleMap.set(role.roleId, role.roleName);
48+
}
49+
}
50+
51+
rolesPageToken = rolesResponse.nextPageToken;
52+
} while (rolesPageToken);
53+
54+
ctx.log(`Fetched ${roleMap.size} roles`);
55+
} catch (error) {
56+
ctx.log(
57+
'Could not fetch roles (may need additional permissions), continuing with basic info',
58+
);
59+
}
60+
61+
// Fetch role assignments to map users to their roles
62+
ctx.log('Fetching role assignments...');
63+
const userRolesMap = new Map<string, string[]>(); // userId -> roleNames[]
64+
65+
try {
66+
let assignmentsPageToken: string | undefined;
67+
do {
68+
const params: Record<string, string> = { customer: 'my_customer' };
69+
if (assignmentsPageToken) {
70+
params.pageToken = assignmentsPageToken;
71+
}
72+
73+
const assignmentsResponse = await ctx.fetch<GoogleWorkspaceRoleAssignmentsResponse>(
74+
'/admin/directory/v1/customer/my_customer/roleassignments',
75+
{ params },
76+
);
77+
78+
if (assignmentsResponse.items) {
79+
for (const assignment of assignmentsResponse.items) {
80+
const roleName = roleMap.get(assignment.roleId) || `Role ${assignment.roleId}`;
81+
const existing = userRolesMap.get(assignment.assignedTo) || [];
82+
existing.push(roleName);
83+
userRolesMap.set(assignment.assignedTo, existing);
84+
}
85+
}
86+
87+
assignmentsPageToken = assignmentsResponse.nextPageToken;
88+
} while (assignmentsPageToken);
89+
90+
ctx.log(`Fetched ${userRolesMap.size} user role assignments`);
91+
} catch (error) {
92+
ctx.log(
93+
'Could not fetch role assignments (may need additional permissions), continuing with basic admin status',
94+
);
95+
}
96+
97+
// Fetch all users with pagination
98+
ctx.log('Fetching users...');
99+
const allUsers: GoogleWorkspaceUser[] = [];
100+
let pageToken: string | undefined;
101+
102+
do {
103+
const params: Record<string, string> = {
104+
customer: 'my_customer',
105+
maxResults: '500',
106+
projection: 'full',
107+
};
108+
109+
if (pageToken) {
110+
params.pageToken = pageToken;
111+
}
112+
113+
const response = await ctx.fetch<GoogleWorkspaceUsersResponse>('/admin/directory/v1/users', {
114+
params,
115+
});
116+
117+
if (response.users) {
118+
allUsers.push(...response.users);
119+
}
120+
121+
pageToken = response.nextPageToken;
122+
} while (pageToken);
123+
124+
ctx.log(`Fetched ${allUsers.length} total users`);
125+
126+
// Filter users
127+
const activeUsers = allUsers.filter((user) => {
128+
if (user.suspended && !includeSuspended) {
129+
return false;
130+
}
131+
if (user.archived) {
132+
return false;
133+
}
134+
return true;
135+
});
136+
137+
ctx.log(`Found ${activeUsers.length} active users after filtering`);
138+
139+
// Build the employee list with roles
140+
const employeeList = activeUsers.map((user) => {
141+
// Get assigned roles from the role assignments
142+
const assignedRoles = userRolesMap.get(user.id) || [];
143+
144+
// Derive a role description
145+
let role: string;
146+
if (user.isAdmin) {
147+
role = 'Super Admin';
148+
} else if (user.isDelegatedAdmin) {
149+
role = 'Delegated Admin';
150+
} else if (assignedRoles.length > 0) {
151+
role = assignedRoles.join(', ');
152+
} else {
153+
role = 'User';
154+
}
155+
156+
return {
157+
email: user.primaryEmail,
158+
name: user.name.fullName,
159+
role,
160+
roles: assignedRoles.length > 0 ? assignedRoles : user.isAdmin ? ['Super Admin'] : ['User'],
161+
isAdmin: user.isAdmin,
162+
isDelegatedAdmin: user.isDelegatedAdmin,
163+
orgUnit: user.orgUnitPath,
164+
suspended: user.suspended,
165+
creationTime: user.creationTime,
166+
lastLoginTime: user.lastLoginTime,
167+
};
168+
});
169+
170+
// Group users by role for summary
171+
const superAdmins = activeUsers.filter((u) => u.isAdmin);
172+
const delegatedAdmins = activeUsers.filter((u) => u.isDelegatedAdmin && !u.isAdmin);
173+
const regularUsers = activeUsers.filter((u) => !u.isAdmin && !u.isDelegatedAdmin);
174+
175+
// Pass with the full employee list as evidence
176+
ctx.pass({
177+
title: 'Employee Access List',
178+
resourceType: 'organization',
179+
resourceId: 'google-workspace',
180+
description: `Retrieved ${activeUsers.length} employees from Google Workspace (${superAdmins.length} super admins, ${delegatedAdmins.length} delegated admins, ${regularUsers.length} regular users)`,
181+
evidence: {
182+
totalUsers: activeUsers.length,
183+
superAdminCount: superAdmins.length,
184+
delegatedAdminCount: delegatedAdmins.length,
185+
regularUserCount: regularUsers.length,
186+
reviewedAt: new Date().toISOString(),
187+
employees: employeeList,
188+
},
189+
});
190+
191+
ctx.log('Google Workspace Employee Access check complete');
192+
},
193+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { twoFactorAuthCheck } from './two-factor-auth';
2+
export { employeeAccessCheck } from './employee-access';

packages/integration-platform/src/manifests/google-workspace/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { IntegrationManifest } from '../../types';
2-
import { twoFactorAuthCheck } from './checks';
2+
import { twoFactorAuthCheck, employeeAccessCheck } from './checks';
33

44
export const googleWorkspaceManifest: IntegrationManifest = {
55
id: 'google-workspace',
@@ -47,5 +47,5 @@ Note: The user authorizing must be a Google Workspace admin.`,
4747

4848
capabilities: ['checks', 'sync'],
4949

50-
checks: [twoFactorAuthCheck],
50+
checks: [twoFactorAuthCheck, employeeAccessCheck],
5151
};

packages/integration-platform/src/manifests/google-workspace/types.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,32 @@ export interface GoogleWorkspaceDomainsResponse {
5151
domains: GoogleWorkspaceDomain[];
5252
}
5353

54+
// Role types
55+
export interface GoogleWorkspaceRole {
56+
roleId: string;
57+
roleName: string;
58+
roleDescription?: string;
59+
isSystemRole: boolean;
60+
isSuperAdminRole: boolean;
61+
}
62+
63+
export interface GoogleWorkspaceRolesResponse {
64+
kind: string;
65+
items: GoogleWorkspaceRole[];
66+
nextPageToken?: string;
67+
}
68+
69+
export interface GoogleWorkspaceRoleAssignment {
70+
roleAssignmentId: string;
71+
roleId: string;
72+
assignedTo: string; // User ID
73+
scopeType: 'CUSTOMER' | 'ORG_UNIT';
74+
orgUnitId?: string;
75+
}
76+
77+
export interface GoogleWorkspaceRoleAssignmentsResponse {
78+
kind: string;
79+
items: GoogleWorkspaceRoleAssignment[];
80+
nextPageToken?: string;
81+
}
82+

0 commit comments

Comments
 (0)