permission-manager.ts•17.6 kB
import { google, drive_v3 } from 'googleapis';
import { AuthService } from '../auth/auth-service.js';
import { Permission, FileCapabilities } from '../types/drive.js';
/**
* Permission and Metadata Manager for Google Drive
* Handles permission checking, metadata extraction, and shared drive operations
*/
export class PermissionManager {
private driveApi: drive_v3.Drive;
private authService: AuthService;
constructor(authService: AuthService) {
this.authService = authService;
this.driveApi = google.drive({
version: 'v3',
auth: authService.getOAuthManager().getOAuth2Client()
});
}
/**
* Check if user has permission to access a file
*/
async checkFilePermission(fileId: string, requiredPermission: 'read' | 'write' | 'comment' = 'read'): Promise<PermissionCheckResult> {
return this.authService.executeWithAuth(async () => {
try {
// Get file metadata with permissions and capabilities
const response = await this.driveApi.files.get({
fileId,
fields: 'id, name, permissions, capabilities, owners, trashed, mimeType',
supportsAllDrives: true
});
const file = response.data;
// Check if file is trashed
if (file.trashed) {
return {
hasPermission: false,
reason: 'File is in trash',
canAccess: false,
effectiveRole: null
};
}
// Get user's effective permission
const effectiveRole = this.getUserEffectiveRole(file.permissions || [], file.owners || []);
// Check if user has required permission level
const hasPermission = this.checkPermissionLevel(effectiveRole, requiredPermission, file.capabilities);
const result: PermissionCheckResult = {
hasPermission,
reason: hasPermission ? 'Access granted' : `Insufficient permissions. Required: ${requiredPermission}, User role: ${effectiveRole || 'none'}`,
canAccess: hasPermission,
effectiveRole
};
if (file.capabilities) {
result.capabilities = this.mapGoogleCapabilitiesToFileCapabilities(file.capabilities);
}
return result;
} catch (error: any) {
// Handle specific Google API errors
if (error.code === 404) {
return {
hasPermission: false,
reason: 'File not found or no access',
canAccess: false,
effectiveRole: null
};
}
if (error.code === 403) {
return {
hasPermission: false,
reason: 'Access forbidden',
canAccess: false,
effectiveRole: null
};
}
throw error;
}
}, 'checkFilePermission');
}
/**
* Get comprehensive file metadata including permissions and sharing info
*/
async getComprehensiveMetadata(fileId: string): Promise<ComprehensiveFileMetadata> {
return this.authService.executeWithAuth(async () => {
const response = await this.driveApi.files.get({
fileId,
fields: 'id, name, mimeType, size, modifiedTime, createdTime, owners, permissions, webViewLink, webContentLink, parents, description, starred, trashed, version, lastModifyingUser, sharingUser, capabilities, viewedByMeTime, shared, ownedByMe, viewersCanCopyContent, copyRequiresWriterPermission, hasAugmentedPermissions, permissionIds, originalFilename, fullFileExtension, fileExtension, md5Checksum, sha1Checksum, sha256Checksum, headRevisionId, isAppAuthorized, exportLinks, driveId, teamDriveId, hasComments, commentCount, folderColorRgb, quotaBytesUsed',
supportsAllDrives: true
});
const file = response.data;
// Get sharing information
const sharingInfo = await this.getSharingInformation(fileId);
// Get revision history (limited)
const revisions = await this.getFileRevisions(fileId, 5); // Get last 5 revisions
return {
basicMetadata: {
id: file.id!,
name: file.name || 'Untitled',
mimeType: file.mimeType || 'application/octet-stream',
size: parseInt(file.size || '0'),
modifiedTime: new Date(file.modifiedTime || Date.now()),
createdTime: new Date(file.createdTime || Date.now()),
owners: (file.owners || []).map(owner => this.mapGoogleUserToUser(owner)),
permissions: (file.permissions || []).map(permission => this.mapGooglePermissionToPermission(permission)),
webViewLink: file.webViewLink || '',
...(file.webContentLink && { webContentLink: file.webContentLink }),
parents: file.parents || [],
description: file.description || '',
starred: file.starred || false,
trashed: file.trashed || false,
version: file.version || '1',
lastModifyingUser: this.mapGoogleUserToUser(file.lastModifyingUser || {}),
sharingUser: file.sharingUser ? this.mapGoogleUserToUser(file.sharingUser) : undefined,
capabilities: this.mapGoogleCapabilitiesToFileCapabilities(file.capabilities || {})
},
extendedMetadata: {
...(file.viewedByMeTime && { viewedByMeTime: new Date(file.viewedByMeTime) }),
shared: file.shared || false,
ownedByMe: file.ownedByMe || false,
...(file.viewersCanCopyContent !== undefined && file.viewersCanCopyContent !== null && { viewersCanCopyContent: file.viewersCanCopyContent }),
...(file.copyRequiresWriterPermission !== undefined && file.copyRequiresWriterPermission !== null && { copyRequiresWriterPermission: file.copyRequiresWriterPermission }),
...(file.hasAugmentedPermissions !== undefined && file.hasAugmentedPermissions !== null && { hasAugmentedPermissions: file.hasAugmentedPermissions }),
permissionIds: file.permissionIds || [],
...(file.originalFilename && { originalFilename: file.originalFilename }),
...(file.fullFileExtension && { fullFileExtension: file.fullFileExtension }),
...(file.fileExtension && { fileExtension: file.fileExtension }),
...(file.md5Checksum && { md5Checksum: file.md5Checksum }),
...(file.sha1Checksum && { sha1Checksum: file.sha1Checksum }),
...(file.sha256Checksum && { sha256Checksum: file.sha256Checksum }),
...(file.headRevisionId && { headRevisionId: file.headRevisionId }),
...(file.isAppAuthorized !== undefined && file.isAppAuthorized !== null && { isAppAuthorized: file.isAppAuthorized }),
...(file.exportLinks && { exportLinks: file.exportLinks }),
...(file.driveId && { driveId: file.driveId }),
...(file.teamDriveId && { teamDriveId: file.teamDriveId }),
...(file.folderColorRgb && { folderColorRgb: file.folderColorRgb }),
...(file.quotaBytesUsed && { quotaBytesUsed: parseInt(file.quotaBytesUsed) })
},
sharingInfo,
revisions
};
}, 'getComprehensiveMetadata');
}
/**
* Get sharing information for a file
*/
async getSharingInformation(fileId: string): Promise<SharingInformation> {
return this.authService.executeWithAuth(async () => {
// Get all permissions for the file
const permissionsResponse = await this.driveApi.permissions.list({
fileId,
fields: 'permissions(id, type, role, emailAddress, domain, displayName, expirationTime, deleted)',
supportsAllDrives: true
});
const permissions = permissionsResponse.data.permissions || [];
// Categorize permissions
const publicAccess = permissions.find(p => p.type === 'anyone');
const domainAccess = permissions.filter(p => p.type === 'domain');
const userAccess = permissions.filter(p => p.type === 'user' && p.role !== 'owner');
const groupAccess = permissions.filter(p => p.type === 'group');
return {
isPublic: !!publicAccess,
publicRole: publicAccess?.role as any,
domainSharing: domainAccess.map(p => ({
domain: p.domain!,
role: p.role as any
})),
userSharing: userAccess.map(p => ({
emailAddress: p.emailAddress!,
displayName: p.displayName || 'Unknown User',
role: p.role as any,
...(p.expirationTime && { expirationTime: new Date(p.expirationTime) })
})),
groupSharing: groupAccess.map(p => ({
emailAddress: p.emailAddress!,
displayName: p.displayName || 'Unknown Group',
role: p.role as any
})),
totalSharedUsers: userAccess.length + groupAccess.length,
hasExpiringPermissions: permissions.some(p => p.expirationTime)
};
}, 'getSharingInformation');
}
/**
* Get file revision history
*/
async getFileRevisions(fileId: string, maxRevisions: number = 10): Promise<FileRevision[]> {
return this.authService.executeWithAuth(async () => {
try {
const response = await this.driveApi.revisions.list({
fileId,
fields: 'revisions(id, modifiedTime, lastModifyingUser, size, originalFilename, md5Checksum, keepForever, published)',
pageSize: maxRevisions
});
return (response.data.revisions || []).map(revision => ({
id: revision.id!,
modifiedTime: new Date(revision.modifiedTime || Date.now()),
lastModifyingUser: this.mapGoogleUserToUser(revision.lastModifyingUser || {}),
...(revision.size && { size: parseInt(revision.size) }),
...(revision.originalFilename && { originalFilename: revision.originalFilename }),
...(revision.md5Checksum && { md5Checksum: revision.md5Checksum }),
keepForever: revision.keepForever || false,
published: revision.published || false
}));
} catch (error: any) {
// Some files don't support revisions
if (error.code === 403 || error.code === 404) {
return [];
}
throw error;
}
}, 'getFileRevisions');
}
/**
* Check if user can access shared drives
*/
async listAccessibleSharedDrives(): Promise<SharedDriveInfo[]> {
return this.authService.executeWithAuth(async () => {
try {
const response = await this.driveApi.drives.list({
fields: 'drives(id, name, colorRgb, backgroundImageFile, capabilities, createdTime, hidden, restrictions)'
});
return (response.data.drives || []).map(drive => ({
id: drive.id!,
name: drive.name!,
...(drive.colorRgb && { colorRgb: drive.colorRgb }),
...(drive.backgroundImageFile && { backgroundImageFile: drive.backgroundImageFile }),
...(drive.capabilities && { capabilities: drive.capabilities }),
createdTime: new Date(drive.createdTime || Date.now()),
hidden: drive.hidden || false,
...(drive.restrictions && { restrictions: drive.restrictions })
}));
} catch (error: any) {
// User might not have access to shared drives
if (error.code === 403) {
return [];
}
throw error;
}
}, 'listAccessibleSharedDrives');
}
/**
* Get user's effective role for a file
*/
private getUserEffectiveRole(permissions: drive_v3.Schema$Permission[], owners: drive_v3.Schema$User[]): string | null {
// Check if user is owner
const isOwner = owners.some(owner => owner.me);
if (isOwner) {
return 'owner';
}
// Find user's direct permissions
const userPermissions = permissions.filter(p =>
(p.type === 'user' && p.emailAddress) ||
(p.type === 'anyone') ||
(p.type === 'domain')
);
// Get highest permission level
const roleHierarchy = ['reader', 'commenter', 'writer', 'fileOrganizer', 'organizer', 'owner'];
let highestRole = null;
let highestRoleIndex = -1;
for (const permission of userPermissions) {
const roleIndex = roleHierarchy.indexOf(permission.role || 'reader');
if (roleIndex > highestRoleIndex) {
highestRoleIndex = roleIndex;
highestRole = permission.role;
}
}
return highestRole || null;
}
/**
* Check if user has required permission level
*/
private checkPermissionLevel(
userRole: string | null,
requiredPermission: 'read' | 'write' | 'comment',
capabilities?: any
): boolean {
if (!userRole) return false;
// Check capabilities if available
if (capabilities) {
switch (requiredPermission) {
case 'read':
return capabilities.canDownload !== false; // Default to true if not specified
case 'write':
return capabilities.canEdit === true;
case 'comment':
return capabilities.canComment === true;
}
}
// Fallback to role-based checking
const rolePermissions = {
'reader': ['read'],
'commenter': ['read', 'comment'],
'writer': ['read', 'comment', 'write'],
'fileOrganizer': ['read', 'comment', 'write'],
'organizer': ['read', 'comment', 'write'],
'owner': ['read', 'comment', 'write']
};
const userPermissions = rolePermissions[userRole as keyof typeof rolePermissions] || [];
return userPermissions.includes(requiredPermission);
}
/**
* Helper methods for mapping Google API objects to our interfaces
*/
private mapGoogleUserToUser(user: drive_v3.Schema$User): any {
return {
displayName: user.displayName || 'Unknown User',
emailAddress: user.emailAddress || '',
...(user.photoLink && { photoLink: user.photoLink }),
me: user.me || false
};
}
private mapGooglePermissionToPermission(permission: drive_v3.Schema$Permission): Permission {
return {
id: permission.id!,
type: (permission.type as any) || 'user',
role: (permission.role as any) || 'reader',
...(permission.emailAddress && { emailAddress: permission.emailAddress }),
...(permission.domain && { domain: permission.domain }),
...(permission.displayName && { displayName: permission.displayName })
};
}
private mapGoogleCapabilitiesToFileCapabilities(capabilities: any): FileCapabilities {
return {
canAddChildren: capabilities.canAddChildren || false,
canChangeCopyRequiresWriterPermission: capabilities.canChangeCopyRequiresWriterPermission || false,
canChangeViewersCanCopyContent: capabilities.canChangeViewersCanCopyContent || false,
canComment: capabilities.canComment || false,
canCopy: capabilities.canCopy || false,
canDelete: capabilities.canDelete || false,
canDownload: capabilities.canDownload || false,
canEdit: capabilities.canEdit || false,
canListChildren: capabilities.canListChildren || false,
canMoveItemIntoTeamDrive: capabilities.canMoveItemIntoTeamDrive || false,
canMoveItemOutOfTeamDrive: capabilities.canMoveItemOutOfTeamDrive || false,
canMoveItemWithinTeamDrive: capabilities.canMoveItemWithinTeamDrive || false,
canReadRevisions: capabilities.canReadRevisions || false,
canRemoveChildren: capabilities.canRemoveChildren || false,
canRename: capabilities.canRename || false,
canShare: capabilities.canShare || false,
canTrash: capabilities.canTrash || false,
canUntrash: capabilities.canUntrash || false
};
}
}
/**
* Interfaces for permission and metadata handling
*/
export interface PermissionCheckResult {
hasPermission: boolean;
reason: string;
canAccess: boolean;
effectiveRole: string | null;
capabilities?: FileCapabilities;
}
export interface ComprehensiveFileMetadata {
basicMetadata: any; // Using FileMetadata from drive.ts
extendedMetadata: ExtendedFileMetadata;
sharingInfo: SharingInformation;
revisions: FileRevision[];
}
export interface ExtendedFileMetadata {
viewedByMeTime?: Date;
shared: boolean;
ownedByMe: boolean;
viewersCanCopyContent?: boolean;
copyRequiresWriterPermission?: boolean;
hasAugmentedPermissions?: boolean;
permissionIds: string[];
originalFilename?: string;
fullFileExtension?: string;
fileExtension?: string;
md5Checksum?: string;
sha1Checksum?: string;
sha256Checksum?: string;
headRevisionId?: string;
isAppAuthorized?: boolean;
exportLinks?: { [key: string]: string };
driveId?: string;
teamDriveId?: string;
folderColorRgb?: string;
quotaBytesUsed?: number;
}
export interface SharingInformation {
isPublic: boolean;
publicRole?: 'reader' | 'writer' | 'commenter';
domainSharing: DomainSharing[];
userSharing: UserSharing[];
groupSharing: GroupSharing[];
totalSharedUsers: number;
hasExpiringPermissions: boolean;
}
export interface DomainSharing {
domain: string;
role: 'reader' | 'writer' | 'commenter';
}
export interface UserSharing {
emailAddress: string;
displayName: string;
role: 'reader' | 'writer' | 'commenter';
expirationTime?: Date;
}
export interface GroupSharing {
emailAddress: string;
displayName: string;
role: 'reader' | 'writer' | 'commenter';
}
export interface FileRevision {
id: string;
modifiedTime: Date;
lastModifyingUser: any; // Using User from drive.ts
size?: number;
originalFilename?: string;
md5Checksum?: string;
keepForever: boolean;
published: boolean;
}
export interface SharedDriveInfo {
id: string;
name: string;
colorRgb?: string;
backgroundImageFile?: any;
capabilities?: any;
createdTime: Date;
hidden: boolean;
restrictions?: any;
}