import { File } from "@google-cloud/storage"; const ACL_POLICY_METADATA_KEY = "custom:aclPolicy"; // Can be flexibly defined according to the use case. // // Examples: // - USER_LIST: the users from a list stored in the database; // - EMAIL_DOMAIN: the users whose email is in a specific domain; // - GROUP_MEMBER: the users who are members of a specific group; // - SUBSCRIBER: the users who are subscribers of a specific service / content // creator. export enum ObjectAccessGroupType {} export interface ObjectAccessGroup { type: ObjectAccessGroupType; // The logic id that identifies qualified group members. Format depends on the // ObjectAccessGroupType — e.g. a user-list DB id, an email domain, a group id. id: string; } export enum ObjectPermission { READ = "read", WRITE = "write", } export interface ObjectAclRule { group: ObjectAccessGroup; permission: ObjectPermission; } // Stored as object custom metadata under "custom:aclPolicy" (JSON string). export interface ObjectAclPolicy { owner: string; visibility: "public" | "private"; aclRules?: Array; } function isPermissionAllowed( requested: ObjectPermission, granted: ObjectPermission, ): boolean { if (requested === ObjectPermission.READ) { return [ObjectPermission.READ, ObjectPermission.WRITE].includes(granted); } return granted === ObjectPermission.WRITE; } abstract class BaseObjectAccessGroup implements ObjectAccessGroup { constructor( public readonly type: ObjectAccessGroupType, public readonly id: string, ) {} public abstract hasMember(userId: string): Promise; } function createObjectAccessGroup( group: ObjectAccessGroup, ): BaseObjectAccessGroup { switch (group.type) { // Implement per access group type, e.g.: // case "USER_LIST": // return new UserListAccessGroup(group.id); default: throw new Error(`Unknown access group type: ${group.type}`); } } export async function setObjectAclPolicy( objectFile: File, aclPolicy: ObjectAclPolicy, ): Promise { const [exists] = await objectFile.exists(); if (!exists) { throw new Error(`Object not found: ${objectFile.name}`); } await objectFile.setMetadata({ metadata: { [ACL_POLICY_METADATA_KEY]: JSON.stringify(aclPolicy), }, }); } export async function getObjectAclPolicy( objectFile: File, ): Promise { const [metadata] = await objectFile.getMetadata(); const aclPolicy = metadata?.metadata?.[ACL_POLICY_METADATA_KEY]; if (!aclPolicy) { return null; } return JSON.parse(aclPolicy as string); } export async function canAccessObject({ userId, objectFile, requestedPermission, }: { userId?: string; objectFile: File; requestedPermission: ObjectPermission; }): Promise { const aclPolicy = await getObjectAclPolicy(objectFile); if (!aclPolicy) { return false; } if ( aclPolicy.visibility === "public" && requestedPermission === ObjectPermission.READ ) { return true; } if (!userId) { return false; } if (aclPolicy.owner === userId) { return true; } for (const rule of aclPolicy.aclRules || []) { const accessGroup = createObjectAccessGroup(rule.group); if ( (await accessGroup.hasMember(userId)) && isPermissionAllowed(requestedPermission, rule.permission) ) { return true; } } return false; }