export type GoogleDiscoveryServicePolicy = { readonly presetId: string; /** Preferred user-consent scopes. Discovery lists alternatives, not a request bundle. */ readonly consentScopes: readonly string[]; readonly authoritativeScopes?: Readonly<Record<string, string>>; readonly fallbackMethodScopes?: readonly string[]; readonly blockedMethodIds?: ReadonlySet<string>; readonly blockedMethodPrefixes?: readonly string[]; readonly blockedMethodSubstrings?: readonly string[]; readonly hiddenParameters?: ReadonlySet<string>; readonly requiredParameters?: Readonly<Record<string, ReadonlySet<string>>>; readonly requiredRequestBodies?: ReadonlySet<string>; readonly hiddenSchemaProperties?: Readonly<Record<string, ReadonlySet<string>>>; readonly requiredSchemaProperties?: Readonly<Record<string, ReadonlySet<string>>>;}; const auth = (scope: string): string => `https://www.googleapis.com/auth/${scope}`; const GOOGLE_PHOTOS_LIBRARY_SCOPES = { [auth("photoslibrary.appendonly")]: "Add photos and videos to Google Photos", [auth("photoslibrary.edit.appcreateddata")]: "Edit app-created albums and media in Google Photos", [auth("photoslibrary.readonly.appcreateddata")]: "Read app-created albums and media in Google Photos",} as const; export const googleOAuthConsentScopes: Readonly<Record<string, readonly string[]>> = { "google-calendar": [auth("calendar")], "google-meet": [ auth("meetings.space.created"), auth("meetings.space.readonly"), auth("meetings.space.settings"), ], "google-gmail": ["https://mail.google.com/", auth("gmail.settings.basic")], "google-sheets": [auth("spreadsheets"), auth("drive.file")], "google-drive": [auth("drive")], "google-docs": [auth("documents")], "google-slides": [auth("presentations")], "google-forms": [auth("forms.body"), auth("forms.responses.readonly")], "google-tasks": [auth("tasks")], "google-people": [ auth("contacts"), auth("contacts.other.readonly"), auth("directory.readonly"), auth("user.addresses.read"), auth("user.birthday.read"), auth("user.emails.read"), auth("user.gender.read"), auth("user.organization.read"), auth("user.phonenumbers.read"), ], "google-photos-library": Object.keys(GOOGLE_PHOTOS_LIBRARY_SCOPES), "google-photos-picker": [auth("photospicker.mediaitems.readonly")], "google-chat": [ auth("chat.spaces"), auth("chat.memberships"), auth("chat.messages"), auth("chat.customemojis"), auth("chat.delete"), auth("chat.users.readstate"), auth("chat.users.spacesettings"), auth("chat.users.sections"), auth("chat.users.availability"), ], // Keep requires Workspace domain-wide delegation and is not placed in the // ordinary user-facing catalog. Retain its scope for stored/enterprise data. "google-keep": [auth("keep")], "google-youtube-data": [auth("youtube.force-ssl"), auth("youtube.channel-memberships.creator")], "google-search-console": [auth("webmasters")], "google-classroom": [ auth("classroom.announcements"), auth("classroom.courses"), auth("classroom.coursework.me"), auth("classroom.coursework.students"), auth("classroom.courseworkmaterials"), auth("classroom.guardianlinks.me.readonly"), auth("classroom.guardianlinks.students.readonly"), auth("classroom.profile.emails"), auth("classroom.profile.photos"), auth("classroom.rosters"), auth("classroom.topics"), ], "google-admin-directory": [ auth("admin.chrome.printers"), auth("admin.directory.customer"), auth("admin.directory.device.chromeos"), auth("admin.directory.device.mobile"), auth("admin.directory.domain"), auth("admin.directory.group"), auth("admin.directory.orgunit"), auth("admin.directory.resource.calendar"), auth("admin.directory.rolemanagement"), auth("admin.directory.user"), auth("admin.directory.user.security"), auth("admin.directory.userschema"), ], "google-admin-reports": [ auth("admin.reports.audit.readonly"), auth("admin.reports.usage.readonly"), ], "google-apps-script": [ auth("script.projects"), auth("script.deployments"), auth("script.processes"), auth("script.metrics"), ], "google-bigquery": [auth("bigquery")], "google-cloud-resource-manager": [auth("cloud-platform")],}; export const googleOAuthConsentScopesForPreset = (presetId: string): readonly string[] => googleOAuthConsentScopes[presetId] ?? []; const policy = ( presetId: string, overrides: Omit<GoogleDiscoveryServicePolicy, "presetId" | "consentScopes"> = {},): GoogleDiscoveryServicePolicy => ({ presetId, consentScopes: googleOAuthConsentScopesForPreset(presetId), ...overrides,}); const GOOGLE_DISCOVERY_POLICIES: Readonly<Record<string, GoogleDiscoveryServicePolicy>> = { "calendar/v3": policy("google-calendar", { blockedMethodIds: new Set(["calendar.calendars.transferOwnership"]), }), "meet/v2": policy("google-meet"), "gmail/v1": policy("google-gmail", { blockedMethodPrefixes: ["gmail.users.settings.delegates."], }), "sheets/v4": policy("google-sheets", { hiddenSchemaProperties: { Request: new Set([ "addDataSource", "updateDataSource", "refreshDataSource", "cancelDataSourceRefresh", ]), }, }), "drive/v3": policy("google-drive", { blockedMethodIds: new Set(["drive.apps.list"]), }), "docs/v1": policy("google-docs"), "slides/v1": policy("google-slides"), "forms/v1": policy("google-forms"), "tasks/v1": policy("google-tasks"), "people/v1": policy("google-people", { requiredParameters: { "people.people.createContact": new Set(["personFields"]), "people.people.get": new Set(["personFields"]), "people.people.getBatchGet": new Set(["resourceNames", "personFields"]), "people.people.connections.list": new Set(["personFields"]), "people.people.searchContacts": new Set(["query", "readMask"]), "people.people.updateContact": new Set(["updatePersonFields"]), "people.people.listDirectoryPeople": new Set(["sources", "readMask"]), "people.people.searchDirectoryPeople": new Set(["query", "sources", "readMask"]), "people.otherContacts.list": new Set(["readMask"]), "people.otherContacts.search": new Set(["query", "readMask"]), "people.contactGroups.batchGet": new Set(["resourceNames"]), }, requiredRequestBodies: new Set([ "people.people.createContact", "people.people.batchCreateContacts", "people.people.batchUpdateContacts", "people.people.batchDeleteContacts", "people.people.updateContact", "people.people.updateContactPhoto", "people.contactGroups.create", "people.contactGroups.update", "people.contactGroups.members.modify", "people.otherContacts.copyOtherContactToMyContactsGroup", ]), requiredSchemaProperties: { BatchCreateContactsRequest: new Set(["contacts", "readMask"]), BatchUpdateContactsRequest: new Set(["contacts", "updateMask", "readMask"]), BatchDeleteContactsRequest: new Set(["resourceNames"]), UpdateContactPhotoRequest: new Set(["photoBytes"]), CreateContactGroupRequest: new Set(["contactGroup"]), UpdateContactGroupRequest: new Set(["contactGroup"]), CopyOtherContactToMyContactsGroupRequest: new Set(["copyMask"]), }, }), "photoslibrary/v1": policy("google-photos-library", { authoritativeScopes: GOOGLE_PHOTOS_LIBRARY_SCOPES, blockedMethodIds: new Set([ "photoslibrary.albums.share", "photoslibrary.albums.unshare", "photoslibrary.sharedAlbums.get", "photoslibrary.sharedAlbums.join", "photoslibrary.sharedAlbums.leave", "photoslibrary.sharedAlbums.list", ]), }), "photospicker/v1": policy("google-photos-picker", { authoritativeScopes: { [auth("photospicker.mediaitems.readonly")]: "Read selected Google Photos media", }, fallbackMethodScopes: [auth("photospicker.mediaitems.readonly")], hiddenParameters: new Set(["access_token", "oauth_token", "key"]), requiredParameters: { "photospicker.mediaItems.list": new Set(["sessionId"]), }, hiddenSchemaProperties: { PickingConfig: new Set(["showEducationBanner", "showZeroState", "showExpandedAppBar"]), }, }), "chat/v1": policy("google-chat", { blockedMethodIds: new Set([ "chat.spaces.completeImport", "chat.spaces.messages.attachments.get", ]), }), // Keep is intentionally not given an inferred ordinary-user consent set. "keep/v1": { ...policy("google-keep"), consentScopes: [] }, "youtube/v3": policy("google-youtube-data", { blockedMethodIds: new Set(["youtube.abuseReports.insert", "youtube.tests.insert"]), blockedMethodPrefixes: ["youtube.thirdPartyLinks."], hiddenParameters: new Set([ "onBehalfOfContentOwner", "onBehalfOfContentOwnerChannel", "managedByMe", "forContentOwner", ]), }), "searchconsole/v1": policy("google-search-console"), "classroom/v1": policy("google-classroom", { blockedMethodIds: new Set(["classroom.courses.teachers.create"]), blockedMethodPrefixes: ["classroom.courses.studentGroups.", "classroom.courses.posts."], blockedMethodSubstrings: [".addOnAttachments.", ".getAddOnContext"], }), "admin/directory_v1": policy("google-admin-directory", { blockedMethodIds: new Set(["directory.chromeosdevices.action"]), }), "admin/reports_v1": policy("google-admin-reports"), "script/v1": policy("google-apps-script", { blockedMethodIds: new Set(["script.scripts.run"]), }), "bigquery/v2": policy("google-bigquery"), "cloudresourcemanager/v3": policy("google-cloud-resource-manager"),}; export const googleDiscoveryPolicyFor = ( service: string, version: string,): GoogleDiscoveryServicePolicy | undefined => GOOGLE_DISCOVERY_POLICIES[`${service}/${version}`]; export const isGoogleDiscoveryMethodAllowed = ( policyValue: GoogleDiscoveryServicePolicy | undefined, methodId: string,): boolean => { if (!policyValue) return true; if (policyValue.blockedMethodIds?.has(methodId)) return false; if (policyValue.blockedMethodPrefixes?.some((prefix) => methodId.startsWith(prefix))) return false; if (policyValue.blockedMethodSubstrings?.some((part) => methodId.includes(part))) return false; return true;};
export type GoogleDiscoveryServicePolicy = {
readonly presetId: string;
/** Preferred user-consent scopes. Discovery lists alternatives, not a request bundle. */
readonly consentScopes: readonly string[];
readonly authoritativeScopes?: Readonly<Record<string, string>>;
readonly fallbackMethodScopes?: readonly string[];
readonly blockedMethodIds?: ReadonlySet<string>;
readonly blockedMethodPrefixes?: readonly string[];
readonly blockedMethodSubstrings?: readonly string[];
readonly hiddenParameters?: ReadonlySet<string>;
readonly requiredParameters?: Readonly<Record<string, ReadonlySet<string>>>;
readonly requiredRequestBodies?: ReadonlySet<string>;
readonly hiddenSchemaProperties?: Readonly<Record<string, ReadonlySet<string>>>;
readonly requiredSchemaProperties?: Readonly<Record<string, ReadonlySet<string>>>;
};
const auth = (scope: string): string => `https://www.googleapis.com/auth/${scope}`;
const GOOGLE_PHOTOS_LIBRARY_SCOPES = {
[auth("photoslibrary.appendonly")]: "Add photos and videos to Google Photos",
[auth("photoslibrary.edit.appcreateddata")]: "Edit app-created albums and media in Google Photos",
[auth("photoslibrary.readonly.appcreateddata")]:
"Read app-created albums and media in Google Photos",
} as const;
export const googleOAuthConsentScopes: Readonly<Record<string, readonly string[]>> = {
"google-calendar": [auth("calendar")],
"google-meet": [
auth("meetings.space.created"),
auth("meetings.space.readonly"),
auth("meetings.space.settings"),
],
"google-gmail": ["https://mail.google.com/", auth("gmail.settings.basic")],
"google-sheets": [auth("spreadsheets"), auth("drive.file")],
"google-drive": [auth("drive")],
"google-docs": [auth("documents")],
"google-slides": [auth("presentations")],
"google-forms": [auth("forms.body"), auth("forms.responses.readonly")],
"google-tasks": [auth("tasks")],
"google-people": [
auth("contacts"),
auth("contacts.other.readonly"),
auth("directory.readonly"),
auth("user.addresses.read"),
auth("user.birthday.read"),
auth("user.emails.read"),
auth("user.gender.read"),
auth("user.organization.read"),
auth("user.phonenumbers.read"),
],
"google-photos-library": Object.keys(GOOGLE_PHOTOS_LIBRARY_SCOPES),
"google-photos-picker": [auth("photospicker.mediaitems.readonly")],
"google-chat": [
auth("chat.spaces"),
auth("chat.memberships"),
auth("chat.messages"),
auth("chat.customemojis"),
auth("chat.delete"),
auth("chat.users.readstate"),
auth("chat.users.spacesettings"),
auth("chat.users.sections"),
auth("chat.users.availability"),
],
// Keep requires Workspace domain-wide delegation and is not placed in the
// ordinary user-facing catalog. Retain its scope for stored/enterprise data.
"google-keep": [auth("keep")],
"google-youtube-data": [auth("youtube.force-ssl"), auth("youtube.channel-memberships.creator")],
"google-search-console": [auth("webmasters")],
"google-classroom": [
auth("classroom.announcements"),
auth("classroom.courses"),
auth("classroom.coursework.me"),
auth("classroom.coursework.students"),
auth("classroom.courseworkmaterials"),
auth("classroom.guardianlinks.me.readonly"),
auth("classroom.guardianlinks.students.readonly"),
auth("classroom.profile.emails"),
auth("classroom.profile.photos"),
auth("classroom.rosters"),
auth("classroom.topics"),
],
"google-admin-directory": [
auth("admin.chrome.printers"),
auth("admin.directory.customer"),
auth("admin.directory.device.chromeos"),
auth("admin.directory.device.mobile"),
auth("admin.directory.domain"),
auth("admin.directory.group"),
auth("admin.directory.orgunit"),
auth("admin.directory.resource.calendar"),
auth("admin.directory.rolemanagement"),
auth("admin.directory.user"),
auth("admin.directory.user.security"),
auth("admin.directory.userschema"),
],
"google-admin-reports": [
auth("admin.reports.audit.readonly"),
auth("admin.reports.usage.readonly"),
],
"google-apps-script": [
auth("script.projects"),
auth("script.deployments"),
auth("script.processes"),
auth("script.metrics"),
],
"google-bigquery": [auth("bigquery")],
"google-cloud-resource-manager": [auth("cloud-platform")],
};
export const googleOAuthConsentScopesForPreset = (presetId: string): readonly string[] =>
googleOAuthConsentScopes[presetId] ?? [];
const policy = (
presetId: string,
overrides: Omit<GoogleDiscoveryServicePolicy, "presetId" | "consentScopes"> = {},
): GoogleDiscoveryServicePolicy => ({
presetId,
consentScopes: googleOAuthConsentScopesForPreset(presetId),
...overrides,
});
const GOOGLE_DISCOVERY_POLICIES: Readonly<Record<string, GoogleDiscoveryServicePolicy>> = {
"calendar/v3": policy("google-calendar", {
blockedMethodIds: new Set(["calendar.calendars.transferOwnership"]),
}),
"meet/v2": policy("google-meet"),
"gmail/v1": policy("google-gmail", {
blockedMethodPrefixes: ["gmail.users.settings.delegates."],
}),
"sheets/v4": policy("google-sheets", {
hiddenSchemaProperties: {
Request: new Set([
"addDataSource",
"updateDataSource",
"refreshDataSource",
"cancelDataSourceRefresh",
]),
},
}),
"drive/v3": policy("google-drive", {
blockedMethodIds: new Set(["drive.apps.list"]),
}),
"docs/v1": policy("google-docs"),
"slides/v1": policy("google-slides"),
"forms/v1": policy("google-forms"),
"tasks/v1": policy("google-tasks"),
"people/v1": policy("google-people", {
requiredParameters: {
"people.people.createContact": new Set(["personFields"]),
"people.people.get": new Set(["personFields"]),
"people.people.getBatchGet": new Set(["resourceNames", "personFields"]),
"people.people.connections.list": new Set(["personFields"]),
"people.people.searchContacts": new Set(["query", "readMask"]),
"people.people.updateContact": new Set(["updatePersonFields"]),
"people.people.listDirectoryPeople": new Set(["sources", "readMask"]),
"people.people.searchDirectoryPeople": new Set(["query", "sources", "readMask"]),
"people.otherContacts.list": new Set(["readMask"]),
"people.otherContacts.search": new Set(["query", "readMask"]),
"people.contactGroups.batchGet": new Set(["resourceNames"]),
},
requiredRequestBodies: new Set([
"people.people.createContact",
"people.people.batchCreateContacts",
"people.people.batchUpdateContacts",
"people.people.batchDeleteContacts",
"people.people.updateContact",
"people.people.updateContactPhoto",
"people.contactGroups.create",
"people.contactGroups.update",
"people.contactGroups.members.modify",
"people.otherContacts.copyOtherContactToMyContactsGroup",
]),
requiredSchemaProperties: {
BatchCreateContactsRequest: new Set(["contacts", "readMask"]),
BatchUpdateContactsRequest: new Set(["contacts", "updateMask", "readMask"]),
BatchDeleteContactsRequest: new Set(["resourceNames"]),
UpdateContactPhotoRequest: new Set(["photoBytes"]),
CreateContactGroupRequest: new Set(["contactGroup"]),
UpdateContactGroupRequest: new Set(["contactGroup"]),
CopyOtherContactToMyContactsGroupRequest: new Set(["copyMask"]),
},
}),
"photoslibrary/v1": policy("google-photos-library", {
authoritativeScopes: GOOGLE_PHOTOS_LIBRARY_SCOPES,
blockedMethodIds: new Set([
"photoslibrary.albums.share",
"photoslibrary.albums.unshare",
"photoslibrary.sharedAlbums.get",
"photoslibrary.sharedAlbums.join",
"photoslibrary.sharedAlbums.leave",
"photoslibrary.sharedAlbums.list",
]),
}),
"photospicker/v1": policy("google-photos-picker", {
authoritativeScopes: {
[auth("photospicker.mediaitems.readonly")]: "Read selected Google Photos media",
},
fallbackMethodScopes: [auth("photospicker.mediaitems.readonly")],
hiddenParameters: new Set(["access_token", "oauth_token", "key"]),
requiredParameters: {
"photospicker.mediaItems.list": new Set(["sessionId"]),
},
hiddenSchemaProperties: {
PickingConfig: new Set(["showEducationBanner", "showZeroState", "showExpandedAppBar"]),
},
}),
"chat/v1": policy("google-chat", {
blockedMethodIds: new Set([
"chat.spaces.completeImport",
"chat.spaces.messages.attachments.get",
]),
}),
// Keep is intentionally not given an inferred ordinary-user consent set.
"keep/v1": { ...policy("google-keep"), consentScopes: [] },
"youtube/v3": policy("google-youtube-data", {
blockedMethodIds: new Set(["youtube.abuseReports.insert", "youtube.tests.insert"]),
blockedMethodPrefixes: ["youtube.thirdPartyLinks."],
hiddenParameters: new Set([
"onBehalfOfContentOwner",
"onBehalfOfContentOwnerChannel",
"managedByMe",
"forContentOwner",
]),
}),
"searchconsole/v1": policy("google-search-console"),
"classroom/v1": policy("google-classroom", {
blockedMethodIds: new Set(["classroom.courses.teachers.create"]),
blockedMethodPrefixes: ["classroom.courses.studentGroups.", "classroom.courses.posts."],
blockedMethodSubstrings: [".addOnAttachments.", ".getAddOnContext"],
}),
"admin/directory_v1": policy("google-admin-directory", {
blockedMethodIds: new Set(["directory.chromeosdevices.action"]),
}),
"admin/reports_v1": policy("google-admin-reports"),
"script/v1": policy("google-apps-script", {
blockedMethodIds: new Set(["script.scripts.run"]),
}),
"bigquery/v2": policy("google-bigquery"),
"cloudresourcemanager/v3": policy("google-cloud-resource-manager"),
};
export const googleDiscoveryPolicyFor = (
service: string,
version: string,
): GoogleDiscoveryServicePolicy | undefined => GOOGLE_DISCOVERY_POLICIES[`${service}/${version}`];
export const isGoogleDiscoveryMethodAllowed = (
policyValue: GoogleDiscoveryServicePolicy | undefined,
methodId: string,
): boolean => {
if (!policyValue) return true;
if (policyValue.blockedMethodIds?.has(methodId)) return false;
if (policyValue.blockedMethodPrefixes?.some((prefix) => methodId.startsWith(prefix)))
return false;
if (policyValue.blockedMethodSubstrings?.some((part) => methodId.includes(part))) return false;
return true;
};