Created
March 12, 2023 02:37
-
-
Save emmaly/7c920035cc6598b6f132e5802257934c to your computer and use it in GitHub Desktop.
Google Workspace Automatic per Building Resource Management Privilege Distribution via Per Building Group Creation & Inclusion in Resource ACL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
const NOOP = true; // must be === false to make *ANY* actual modification work happen, not just !== true. | |
// | |
const GROUP_CREATION_OKAY = false; // must be === true for this to happen | |
const ACLRULE_ADD_OKAY = false; // must be === true for this to happen | |
const ACLRULE_REMOVE_OKAY = false; // must be === true for this to happen | |
const ACLRULE_FIXROLE_OKAY = false; // must be === true for this to happen | |
// | |
const GROUP_TEMPLATE = { | |
name: ({buildingName}) => buildingName + " Resource Custodian Role", | |
description: ({buildingName}) => "Members are responsible for maintaining " + buildingName + " resource calendars and resolving scheduling conflicts.", | |
email: { | |
primary: ({buildingNameLowercaseAlphanumericHyphenated}) => buildingNameLowercaseAlphanumericHyphenated + "[email protected]", | |
aliases: [({buildingNameLowercaseAlphanumeric}) => buildingNameLowercaseAlphanumeric + "[email protected]"], | |
}, | |
}; | |
// | |
const FILTER = { | |
// can be: | |
// `string` as a `v === building.buildingName` | |
// `RegExp` as a `v.test(building.buildingName)` | |
// `({building, buildingName}) => buildingName === "something"` // as a filter for buildingName or buildingName_exclude | |
// `({calendarResource, generatedResourceName}) => generatedResourceName === "something"` // as a filter for generatedResourceName or generatedResourceName_exclude | |
// `({calendarResource, resourceCategory}) => resourceCategory === "something"` // as a filter for resourceCategory or resourceCategory_exclude | |
buildingName: [ | |
// /S/i, | |
// "Chicago", | |
// "Denver", | |
// ({building}) => building?.address?.postalCode?.startsWith("123"), | |
], | |
buildingName_exclude: [ | |
// "London", | |
], | |
generatedResourceName: [], | |
generatedResourceName_exclude: [ | |
/\bHelicopter\b/i, | |
], | |
resourceCategory: [ | |
"CONFERENCE_ROOM", | |
({calendarResource, resourceCategory}) => resourceCategory === "OTHER" && calendarResource.resourceType === "Vehicle", | |
], | |
resourceCategory_exclude: [ | |
({calendarResource}) => calendarResource.resourceType === "Schedule", | |
], | |
}; | |
// | |
const ACLROLE = { // result ends with first matched role in `applicationOrder` | |
"applicationOrder": ["owner", "writer", "reader", "freeBusyReader", "none", "remove"], | |
"owner": [], | |
"writer": [ | |
({calendarResource}) => calendarResource.resourceCategory === "CONFERENCE_ROOM", | |
({calendarResource}) => calendarResource.resourceCategory === "OTHER" && calendarResource.resourceType === "Vehicle", | |
], | |
"reader": [ | |
], | |
"freeBusyReader": [], | |
"none": [], | |
"remove": [ | |
({calendarResource}) => calendarResource.resourceType !== "Schedule", | |
], | |
}; | |
// | |
// // // | |
// // // | |
// // // | |
// // // | |
// // // | |
// // // | |
// // // | |
// // // | |
// // // | |
// // // | |
// // // | |
// // // | |
// // // | |
// // // | |
// // // | |
/** @typedef {Object<string,Admin_directory_v1.Admin.Directory_v1.Schema.Building>} */ | |
const buildingCache = {}; | |
function doRunRun() { | |
iterateBuildings( | |
perBuilding, | |
buildingFilter, | |
); | |
} | |
/** | |
* @param {Admin_directory_v1.Admin.Directory_v1.Schema.Building} building | |
* @returns {boolean} | |
*/ | |
function buildingFilter(building) { | |
if (FILTER?.buildingName?.length && !FILTER.buildingName.filter((v) => { | |
if (typeof v === "string") return v === building?.buildingName; | |
if (typeof v === "object" && v instanceof RegExp) return v.test(building?.buildingName); | |
if (typeof v === "function") return v({building, buildingName:building?.buildingName}); | |
return false; // prefer to false negative if the filter is one we don't understand? | |
}).length) return false; | |
if (FILTER?.buildingName_exclude?.length && FILTER.buildingName_exclude.filter((v) => { | |
if (typeof v === "string") return v === building?.buildingName; | |
if (typeof v === "object" && v instanceof RegExp) return v.test(building?.buildingName); | |
if (typeof v === "function") return v({building, buildingName:building?.buildingName}); | |
return true; // prefer to false positive if the filter is one we don't understand? | |
}).length) return false; | |
return true; // nothing negated it | |
} | |
/** | |
* @param {Admin_directory_v1.Admin.Directory_v1.Schema.CalendarResource} calendarResource | |
* @returns {boolean} | |
*/ | |
function calendarResourceFilter(calendarResource) { | |
if (FILTER?.generatedResourceName?.length && !FILTER.generatedResourceName.filter((v) => { | |
if (typeof v === "string") return v === calendarResource?.generatedResourceName; | |
if (typeof v === "object" && v instanceof RegExp) return v.test(calendarResource?.generatedResourceName); | |
if (typeof v === "function") return v({calendarResource, generatedResourceName:calendarResource?.generatedResourceName}); | |
return false; // prefer to false negative if the filter is one we don't understand? | |
}).length) return false; | |
if (FILTER?.generatedResourceName_exclude?.length && FILTER.generatedResourceName_exclude.filter((v) => { | |
if (typeof v === "string") return v === calendarResource?.generatedResourceName; | |
if (typeof v === "object" && v instanceof RegExp) return v.test(calendarResource?.generatedResourceName); | |
if (typeof v === "function") return v({calendarResource, generatedResourceName:calendarResource?.generatedResourceName}); | |
return true; // prefer to false positive if the filter is one we don't understand? | |
}).length) return false; | |
if (FILTER?.resourceCategory?.length && !FILTER.resourceCategory.filter((v) => { | |
if (typeof v === "string") return v === calendarResource?.resourceCategory; | |
if (typeof v === "object" && v instanceof RegExp) return v.test(calendarResource?.resourceCategory); | |
if (typeof v === "function") return v({calendarResource, resourceCategory:calendarResource?.resourceCategory}); | |
return false; // prefer to false negative if the filter is one we don't understand? | |
}).length) return false; | |
if (FILTER?.resourceCategory_exclude?.length && FILTER.resourceCategory_exclude.filter((v) => { | |
if (typeof v === "string") return v === calendarResource?.resourceCategory; | |
if (typeof v === "object" && v instanceof RegExp) return v.test(calendarResource?.resourceCategory); | |
if (typeof v === "function") return v({calendarResource, resourceCategory:calendarResource?.resourceCategory}); | |
return true; // prefer to false positive if the filter is one we don't understand? | |
}).length) return false; | |
return true; // nothing negated it | |
} | |
/** | |
* @param {string} aclRole | |
* @param {Admin_directory_v1.Admin.Directory_v1.Schema.CalendarResource} calendarResource | |
* @returns {boolean} | |
*/ | |
function calendarResourceAclRoleFilter(aclRole, calendarResource) { | |
return (ACLROLE[aclRole]||[]).filter((filterFn) => filterFn({calendarResource})).length; | |
} | |
/** | |
* @param {Object} o | |
* @param {Admin_directory_v1.Admin.Directory_v1.Schema.Building} o.building | |
* @param {Admin_directory_v1.Admin.Directory_v1.Schema.CalendarResource} o.calendarResource | |
* @param {boolean} o.createIfAbsent | |
* @returns {Admin_directory_v1.Admin.Directory_v1.Schema.Group} | |
*/ | |
function groupFromObject({building, calendarResource, createIfAbsent}) { | |
if (!building && calendarResource?.buildingId) building = buildingCache[calendarResource.buildingId]; // use magic to convert a `calendarResource.buildingId` into a `building` | |
const buildingName = building?.buildingName ? building.buildingName : null; | |
if (!buildingName) return null; | |
const groupTemplateVariables = { | |
buildingName: buildingName.trim(), | |
buildingNameLowercaseAlphanumeric: buildingName.trim().toLowerCase().replace(/[^a-z0-9]+/g, ""), | |
buildingNameLowercaseAlphanumericHyphenated: buildingName.trim().toLowerCase().replace(/[^a-z0-9\-]+/g, "-"), | |
}; | |
const group = AdminDirectory.newGroup(); | |
group.name = GROUP_TEMPLATE.name(groupTemplateVariables); | |
group.description = GROUP_TEMPLATE.description(groupTemplateVariables); | |
group.email = GROUP_TEMPLATE.email.primary(groupTemplateVariables); | |
group.aliases = GROUP_TEMPLATE.email.aliases.map((aliasFxn) => aliasFxn(groupTemplateVariables)).filter((alias) => alias !== group.email); | |
try { | |
const existingGroup = AdminDirectory.Groups.get(group.email); | |
if (existingGroup) return existingGroup; | |
} catch(e) { | |
if (e?.details?.code === 404 && createIfAbsent) { | |
Logger.log((NOOP === false && GROUP_CREATION_OKAY === true ? "Creating" : "NOOP: Would have created") + " Group [" + group.email + "] named [" + group.name + "]."); | |
if (NOOP !== false || GROUP_CREATION_OKAY !== true) return; | |
const newGroup = AdminDirectory.Groups.insert(group); | |
Logger.log(JSON.stringify({type:"group", action:"create", wanted:group, result:newGroup}, null, 2)); | |
return newGroup; | |
} | |
} | |
return null; | |
} | |
/** | |
* @callback buildingCallback | |
* @param {Calendar_v3.Calendar.V3.Schema.Building} building | |
*/ | |
/** | |
* @callback buildingFilterCallback | |
* @param {Calendar_v3.Calendar.V3.Schema.Building} building | |
* @returns {boolean} | |
*/ | |
/** | |
* @param {buildingCallback} buildingCb | |
* @param {buildingFilterCallback} buildingFilterCb | |
* @param {Object<string,string>} options | |
*/ | |
function iterateBuildings(buildingCb, buildingFilterCb, options) { | |
let nextPageToken = ""; | |
do { | |
const page = AdminDirectory.Resources.Buildings.list("my_customer", { | |
...options, | |
nextPageToken, | |
}); | |
nextPageToken = page.nextPageToken; | |
page | |
?.buildings | |
?.filter(buildingFilterCb || (()=>true)) | |
?.forEach(buildingCb || (()=>{})); | |
} while(nextPageToken); | |
} | |
/** | |
* @callback calendarResourceCallback | |
* @param {Calendar_v3.Calendar.V3.Schema.CalendarResource} calendarResource | |
*/ | |
/** | |
* @callback calendarResourceFilterCallback | |
* @param {Calendar_v3.Calendar.V3.Schema.CalendarResource} calendarResource | |
* @returns {boolean} | |
*/ | |
/** | |
* @param {calendarResourceCallback} calendarResourceCb | |
* @param {calendarResourceFilterCallback} calendarResourceFilterCb | |
* @param {Object<string,string>} options | |
*/ | |
function iterateCalendarResources(calendarResourceCb, calendarResourceFilterCb, options) { | |
let nextPageToken = ""; | |
do { | |
const page = AdminDirectory.Resources.Calendars.list("my_customer", { | |
...options, | |
nextPageToken, | |
}); | |
nextPageToken = page.nextPageToken; | |
page | |
?.items | |
?.filter(calendarResourceFilterCb || (()=>true)) | |
?.forEach(calendarResourceCb || (()=>{})); | |
} while(nextPageToken); | |
} | |
/** | |
* @callback aclRuleCallback | |
* @param {Calendar_v3.Calendar.V3.Schema.AclRule} aclRule | |
*/ | |
/** | |
* @callback aclRuleFilterCallback | |
* @param {Calendar_v3.Calendar.V3.Schema.AclRule} aclRule | |
* @returns {boolean} | |
*/ | |
/** | |
* @param {aclRuleCallback} aclRuleCb | |
* @param {aclRuleFilterCallback} aclRuleFilterCb | |
* @param {string} calendarId | |
* @param {Object<string,string>} options | |
*/ | |
function iterateAclRules(aclRuleCb, aclRuleFilterCb, calendarId, options) { | |
let nextPageToken = ""; | |
do { | |
const page = Calendar.Acl.list(calendarId, { | |
...options, | |
nextPageToken, | |
}); | |
nextPageToken = page.nextPageToken; | |
page | |
?.items | |
?.filter(aclRuleFilterCb || (()=>true)) | |
?.forEach(aclRuleCb || (()=>{})); | |
} while(nextPageToken); | |
} | |
/** | |
* @param {string} calendarId | |
* @param {string} groupEmail | |
* @returns {Calendar_v3.Calendar.V3.Schema.AclRule} | |
*/ | |
function getGroupCalendarAclRule(calendarId, groupEmail) { | |
try { | |
return Calendar.Acl.get(calendarId, "group:" + groupEmail); | |
} catch(e) {} | |
return null; | |
} | |
/** | |
* @param {Admin_directory_v1.Admin.Directory_v1.Schema.Building} building | |
*/ | |
function perBuilding(building) { | |
if (!building?.buildingName) return; | |
Logger.log(building.buildingName); | |
if (buildingCache && !buildingCache[building.buildingId]) buildingCache[building.buildingId] = building; // globally cache the `building` by its `buildingId` | |
const group = groupFromObject({building, createIfAbsent:true}); | |
// Logger.log(JSON.stringify({name: building.buildingName, group, building}, null, 2)); | |
if (!group?.email) return; | |
iterateCalendarResources( | |
perCalendarResource, | |
calendarResourceFilter, | |
{ | |
query: "buildingId=" + building.buildingId, | |
}, | |
); | |
} | |
/** | |
* @param {Admin_directory_v1.Admin.Directory_v1.Schema.CalendarResource} calendarResource | |
*/ | |
function perCalendarResource(calendarResource) { | |
if (!calendarResource?.resourceEmail) return; | |
// Logger.log(JSON.stringify(calendarResource, null, 2)); | |
const group = groupFromObject({calendarResource}, false); | |
if (!group?.email) return; | |
// Logger.log(JSON.stringify(group, null, 2)); | |
const calendar = Calendar.Calendars.get(calendarResource.resourceEmail); | |
if (!calendar?.id) return; | |
// Logger.log(JSON.stringify(calendar, null, 2)); | |
/** @typedef {Calendar_v3.Calendar.V3.Schema.AclRule[]} */ | |
const aclRules = []; | |
iterateAclRules( | |
(aclRule) => aclRules.push(aclRule), | |
(aclRule) => ( | |
aclRule.scope.type === "group" && | |
aclRule.scope.value === group.email | |
), | |
calendar.id, | |
); | |
// Logger.log(JSON.stringify(aclRules, null, 2)); | |
const aclRule = getGroupCalendarAclRule(calendar.id, group.email); | |
// Logger.log(JSON.stringify({groupEmail:group.email,calendarId:calendar.id,calendarSummary:calendar.summary,aclRule}, null, 2)); | |
for (let i in ACLROLE.applicationOrder) { | |
const roleName = ACLROLE.applicationOrder[i]; | |
// Logger.log("testing: [" + roleName + "] on [" + calendarResource.generatedResourceName + "]"); | |
if (!calendarResourceAclRoleFilter(roleName, calendarResource)) continue; // no match | |
// warning: never allow the for loop to `continue` beyond the above line; we must break out or return. | |
// Logger.log("--- matched: " + roleName); | |
if (aclRule?.role === roleName || (roleName === "remove" && !aclRule)) break; // nothing to do | |
// Logger.log("--- work to do"); | |
if (roleName === "remove") { | |
Logger.log((NOOP === false && ACLRULE_REMOVE_OKAY === true ? "Deleting role" : "NOOP: Would have deleted") + " ACLRule [(" + aclRule.role + ") " + aclRule.scope.type + ":" + aclRule.scope.value + "] from calendar [" + calendarResource.generatedResourceName + "]."); | |
if (NOOP !== false || ACLRULE_REMOVE_OKAY !== true) return; | |
Calendar.Acl.remove(calendar.id, aclRule.id); | |
Logger.log(JSON.stringify({type:"aclRule", action:"delete", was:aclRule}, null, 2)); | |
break; | |
} | |
if (aclRule) { | |
// update the AclRule's role | |
const aclRuleRoleWas = aclRule.role; | |
aclRule.role = roleName; | |
Logger.log((NOOP === false && ACLRULE_FIXROLE_OKAY === true ? "Updating role" : "NOOP: Would have updated role") + " ACLRule [(" + aclRuleRoleWas + ") " + aclRule.scope.type + ":" + aclRule.scope.value + "] to [" + aclRule.role + "] for calendar [" + calendarResource.generatedResourceName + "]."); | |
if (NOOP !== false || ACLRULE_FIXROLE_OKAY !== true) return; | |
const updatedAclRule = Calendar.Acl.update(aclRule, calendar.id); | |
Logger.log(JSON.stringify({type:"aclRule", action:"updateRole", was:aclRule, result:updatedAclRule}, null, 2)); | |
} else { | |
// create the AclRule | |
const newAclRule = Calendar.newAclRule(); | |
newAclRule.role = "writer"; | |
newAclRule.scope = { | |
type: "group", | |
value: group.email, | |
}; | |
Logger.log((NOOP === false && ACLRULE_ADD_OKAY === true ? "Adding" : "NOOP: Would have added") + " ACLRule [(" + newAclRule.role + ") " + newAclRule.scope.type + ":" + newAclRule.scope.value + "] to calendar [" + calendarResource.generatedResourceName + "]."); | |
if (NOOP !== false || ACLRULE_ADD_OKAY !== true) break; | |
const newlySetAclRule = Calendar.Acl.insert(newAclRule, calendar.id); | |
Logger.log(JSON.stringify({type:"aclRule", action:"create", wanted:newAclRule, result:newlySetAclRule}, null, 2)); | |
} | |
break; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment