Skip to main content
Glama

Google Drive MCP Server

by ducla5
permission-manager.ts17.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; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ducla5/gdriver-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server