Skip to content

Commit

Permalink
Added automatic removal of past parents from groups
Browse files Browse the repository at this point in the history
  • Loading branch information
bootsie123 committed Sep 24, 2023
1 parent 4245a3e commit e08c872
Show file tree
Hide file tree
Showing 14 changed files with 188 additions and 11 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Google Groups offers a convient way to have mailing and distribution lists for t
- Individual control over whether to sync parents and/or students
- Automatic creation of missing Google Groups (including auto naming and setting of default permissions)
- Automatic creation of missing student Google Accounts
- Automatic removal of past parents from Google Groups
- Automatic updating of student emails in Blackbaud (matches a students email in Blackbaud with their email found in the Google domain)
- Sync job reports via email
- Built to be hosted using Azure Functions (free plan)
Expand Down Expand Up @@ -136,6 +137,7 @@ _Note: The settings in bold are required. Refer to their corresponding configura
| **BLACKBAUD_SUBSCRIPTION_KEY** | String | | The primary access key of your Blackbaud SKY API subscription |
| BLACKBAUD_STUDENT_ROLE | String | Student | The Blackbaud role used when searching for students to sync to Google Groups |
| BLACKBAUD_PARENT_ROLE | String | Parent | The Blackbaud role used when searching for parents to sync to Google Groups |
| BLACKBAUD_PAST_PARENT_ROLE | String | Parent of Past Student | An additional Blackbaud role used when searching for parents to remove from Google Groups |
| **GOOGLE_DOMAIN** | String | | The FQDN of your Google Workspace organization |
| GOOGLE_ACCOUNT_CREATION_PASSWORD | String | TEMP_PASSWORD_CHANGE_THIS! | The default password used when creating Google accounts |
| GOOGLE_ACCOUNT_CREATION_ORG_UNIT_PATH | String | /Students | The default Organizational Unit (OU) path to search for OU's following the GOOGLE_STUDENT_GROUP_NAME naming convention. By default, accounts are put into this OU if a sub OU with the correct name cannot be found |
Expand Down
5 changes: 3 additions & 2 deletions azuredeploy.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.17.1.54307",
"templateHash": "12708229036174630440"
"version": "0.21.1.54444",
"templateHash": "4048911328218616693"
}
},
"parameters": {
Expand Down Expand Up @@ -80,6 +80,7 @@
"BLACKBAUD_SUBSCRIPTION_KEY": "MUST_CHANGE_THIS",
"BLACKBAUD_STUDENT_ROLE": "Student",
"BLACKBAUD_PARENT_ROLE": "Parent",
"BLACKBAUD_PAST_PARENT_ROLE": "Parent of Past Student",
"GOOGLE_DOMAIN": "MUST_CHANGE_THIS",
"GOOGLE_ACCOUNT_CREATION_PASSWORD": "TEMP_PASSWORD_CHANGE_THIS!",
"GOOGLE_ACCOUNT_CREATION_ORG_UNIT_PATH": "/Students",
Expand Down
1 change: 1 addition & 0 deletions default.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"BLACKBAUD_SUBSCRIPTION_KEY": "MUST_CHANGE_THIS",
"BLACKBAUD_STUDENT_ROLE": "Student",
"BLACKBAUD_PARENT_ROLE": "Parent",
"BLACKBAUD_PAST_PARENT_ROLE": "Parent of Past Student",
"GOOGLE_DOMAIN": "MUST_CHANGE_THIS",
"GOOGLE_ACCOUNT_CREATION_PASSWORD": "TEMP_PASSWORD_CHANGE_THIS!",
"GOOGLE_ACCOUNT_CREATION_ORG_UNIT_PATH": "/Students",
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bg-group-sync",
"version": "1.0.1",
"version": "1.1.0",
"description": "An automated tool to sync parents and students in Blackbaud to Google Groups",
"main": "dist/functions/**/*.js",
"scripts": {
Expand Down
3 changes: 2 additions & 1 deletion src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export default {
},
sync: {
studentRole: process.env.BLACKBAUD_STUDENT_ROLE || "Student",
parentRole: process.env.BLACKBAUD_PARENT_ROLE || "Parent"
parentRole: process.env.BLACKBAUD_PARENT_ROLE || "Parent",
pastParentRole: process.env.BLACKBAUD_PAST_PARENT_ROLE || "Parent of Past Student"
}
},
google: {
Expand Down
2 changes: 1 addition & 1 deletion src/functions/blackbaud/blackbaudGetUsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export async function blackbaudGetUsers(

const roles = (await blackbaud.getRoles()).value;

const userRole = roles.find(role => role.name == userRoleName);
const userRole = roles.find(role => role.name === userRoleName);

if (!userRole) {
logger.log(Severity.Error, `Unable to find role of name ${userRoleName}`);
Expand Down
43 changes: 43 additions & 0 deletions src/functions/google/googleFindGroups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as df from "durable-functions";
import { InvocationContext } from "@azure/functions";
import { admin_directory_v1 as adminDirectoryV1 } from "googleapis";

import { Logger, Severity } from "../../lib/Logger";
import { GoogleAPI } from "../../lib/Google";

import environment from "../../environment";

export const FUNCTION_NAME = "googleFindGroups";

/**
* Finds Google Groups using the specified query
* @param params A {@link adminDirectoryV1.Params$Resource$Groups$List} object with query options
* @param context The invocation context for the function
* @returns A list of {@link adminDirectoryV1.Schema$Group} groups if found, otherwise undefined
*/
export async function googleFindGroups(
params: adminDirectoryV1.Params$Resource$Groups$List,
context: InvocationContext
): Promise<adminDirectoryV1.Schema$Group[]> {
const logger = new Logger(context, "Google");

const google = new GoogleAPI();

try {
const groupList = await google.listGroups({
domain: environment.google.domain,
...params
});

return groupList.groups;
} catch (err) {
logger.log(Severity.Error, err, "\nInput Parameters:", params);

throw err;
}
}

df.app.activity(FUNCTION_NAME, {
extraInputs: [df.input.durableClient()],
handler: googleFindGroups
});
52 changes: 52 additions & 0 deletions src/functions/google/googleRemoveMemberFromGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as df from "durable-functions";
import { InvocationContext } from "@azure/functions";

import { Logger, Severity } from "../../lib/Logger";
import { GoogleAPI } from "../../lib/Google";

export const FUNCTION_NAME = "googleRemoveMemberFromGroup";

/**
* Outlines the parameters needed for {@link googleRemoveMemberFromGroup}
*/
export interface GoogleRemoveMemberFromGroupParams {
/** The ID of the Google Group */
groupId: string;
/** The email of the user to remove */
email: string;
}

/**
* Removes a member from the specified Google Group
* @param params A {@link GoogleRemoveMemberFromGroupParams} object containing the group ID and email of the member to remove
* @param context The invocation context for the function
* @returns True if successful
*/
export async function googleRemoveMemberFromGroup(
params: GoogleRemoveMemberFromGroupParams,
context: InvocationContext
): Promise<boolean> {
const logger = new Logger(context, "Google");

const google = new GoogleAPI();

try {
await google.removeMemberFromGroup({
groupKey: params.groupId,
memberKey: params.email
});

return true;
} catch (err) {
if (err?.includes("Resource Not Found: memberKey")) return true;

logger.log(Severity.Error, err, "\nInput Parameters:", params);

throw err;
}
}

df.app.activity(FUNCTION_NAME, {
extraInputs: [df.input.durableClient()],
handler: googleRemoveMemberFromGroup
});
7 changes: 7 additions & 0 deletions src/functions/orchestrators/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ export function* syncOrchestrationHandler(context: df.OrchestrationContext) {
processor: processParent
})
);

tasks.push(
context.df.callSubOrchestrator(syncUsers, {
blackbaudRole: environment.blackbaud.sync.pastParentRole,
processor: processParent
})
);
}

if (tasks.length > 0) {
Expand Down
48 changes: 47 additions & 1 deletion src/functions/orchestrators/processParent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { FUNCTION_NAME as blackbaudGetUser } from "../blackbaud/blackbaudGetUser
import { FUNCTION_NAME as googleFindGroup } from "../google/googleFindGroup";
import { FUNCTION_NAME as googleCreateGroup } from "../google/googleCreateGroup";
import { FUNCTION_NAME as googleAddMemberToGroup } from "../google/googleAddMemberToGroup";
import { FUNCTION_NAME as googleRemoveMemberFromGroup } from "../google/googleRemoveMemberFromGroup";
import { FUNCTION_NAME as googleFindGroups } from "../google/googleFindGroups";

import { genGroupEmail } from "../../utils";

Expand Down Expand Up @@ -60,12 +62,56 @@ export function* processParentHandler(
role => role.name === environment.blackbaud.sync.studentRole
);

if (roles.length > 0) {
if (roles.length < 1) continue;

if (user.student_info.grad_year === null) {
logger.forceLog(
Severity.Warning,
`Graduation year missing for student ${user.first_name} ${user.last_name} with parent ${parent.first_name} ${parent.last_name}`
);
} else {
gradYears.add(user.student_info.grad_year);
}
}
}

const groups: adminDirectoryV1.Schema$Group[] = yield context.df.callActivity(
googleFindGroups,
{
query: `name:${environment.google.parentGroupName.trim().replaceAll(" ", "+")}*`
}
);

const gradYearsArray = Array.from(gradYears);

const removalTasks = [];

for (const group of groups || []) {
if (gradYearsArray.some(year => group.name.includes(year))) continue;

removalTasks.push(
context.df.callActivity(googleRemoveMemberFromGroup, {
groupId: group.id,
email: parent.email
})
);
}

if (removalTasks.length > 0) {
try {
yield context.df.Task.all(removalTasks);
} catch (err) {
logger.log(
Severity.Warning,
`Unable to remove parent ${parent.email} from groups. Continuing with sync...`
);
}
}

if (!parent.roles.some(role => role.name === environment.blackbaud.sync.parentRole)) {
return { status: "success" };
}

for (const gradYear of gradYears) {
const parentGroupName = genGroupEmail(
environment.google.parentGroupEmailPrefix,
Expand Down
10 changes: 7 additions & 3 deletions src/functions/orchestrators/syncUsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ export function* syncUsersHandler(

const params: syncUsersHandlerParams = context.df.getInput();

const roleName = params.blackbaudRole.toLowerCase();
const roleName = params.blackbaudRole;

try {
const users = yield context.df.callActivity(blackbaudGetUsers, params.blackbaudRole);
const users: any[] = yield context.df.callActivity(blackbaudGetUsers, params.blackbaudRole);

if (users.length < 1) {
logger.log(Severity.Warning, `No ${roleName}s found. Skipping sync to Google Groups`);
Expand All @@ -80,7 +80,11 @@ export function* syncUsersHandler(

logger.log(Severity.Info, `Syncing ${tasks.length} ${roleName}s with Google Groups...`);

const results: ProcessResults[] = yield context.df.Task.all(tasks);
let results: ProcessResults[] = [];

if (tasks.length > 0) {
results = yield context.df.Task.all(tasks);
}

let succeeded = 0;

Expand Down
19 changes: 19 additions & 0 deletions src/lib/Google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,25 @@ export class GoogleAPI {
}
}

/**
* Removes a member from the specified group
* @param params The parameters containing the group ID and member to remove
* @param options Additional options
* @returns A message containing the status of the operation
*/
async removeMemberFromGroup(
params: adminDirectoryV1.Params$Resource$Members$Delete,
options?: MethodOptions
): Promise<any> {
try {
const res = await this.directory.members.delete(params, options);

return res.data;
} catch (err) {
return this.apiErrorHandler(err, this.removeMemberFromGroup, params, options);
}
}

/**
* Creates a new user
* @param params The parameters containing the user's name, email, and password
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"outDir": "dist",
"rootDir": "src",
"sourceMap": true,
"strict": false
"strict": false,
"lib": ["es2021"]
}
}

0 comments on commit e08c872

Please sign in to comment.