Skip to content

Instantly share code, notes, and snippets.

@emmaly
Created March 12, 2023 02:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save emmaly/7c920035cc6598b6f132e5802257934c to your computer and use it in GitHub Desktop.
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
//
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 + "-resource-custodian-role@example.com",
aliases: [({buildingNameLowercaseAlphanumeric}) => buildingNameLowercaseAlphanumeric + "-resource-custodian-role@example.com"],
},
};
//
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