Loading content...
Sharing is what transforms personal file storage into a collaboration platform. Users need to share files with colleagues, partners, and the public—with precisely controlled access levels. The challenge is building a permission system that is:
This page explores the architecture of enterprise-grade sharing and permission systems.
By the end of this page, you'll understand: (1) Permission models and access control lists, (2) Inheritance in folder hierarchies, (3) Shareable links and their security, (4) Performance optimization for permission checks, and (5) Enterprise features like groups and external sharing.
The foundation of any sharing system is its permission model—the rules that define who can do what to which resources.
Core Permission Levels:
Permission Hierarchy (each level includes all lower levels):
┌─────────────────────────────────────────────────────────────┐
│ OWNER │
│ ├── Can delete file/folder permanently │
│ ├── Can change owner │
│ ├── Can modify all permissions │
│ └── All Editor permissions │
├─────────────────────────────────────────────────────────────┤
│ EDITOR │
│ ├── Can modify file content │
│ ├── Can add/remove files in folder │
│ ├── Can create subfolders │
│ ├── Can share with others (if allowed by owner settings) │
│ └── All Viewer permissions │
├─────────────────────────────────────────────────────────────┤
│ COMMENTER (optional level) │
│ ├── Can add comments │
│ ├── Can suggest edits (tracked changes) │
│ └── All Viewer permissions │
├─────────────────────────────────────────────────────────────┤
│ VIEWER │
│ ├── Can view file content │
│ ├── Can download file │
│ └── Can see file metadata │
└─────────────────────────────────────────────────────────────┘
12345678910111213141516171819202122232425262728293031323334353637383940414243
// Core permission data modelenum PermissionLevel { VIEWER = 'viewer', COMMENTER = 'commenter', EDITOR = 'editor', OWNER = 'owner',} interface Permission { id: string; resourceId: string; // File or folder ID resourceType: 'file' | 'folder'; // Who has the permission granteeType: 'user' | 'group' | 'link' | 'anyone'; granteeId?: string; // User/group ID (null for 'anyone') // What permission they have level: PermissionLevel; // Optional restrictions expiresAt?: Date; // Auto-revoke after date downloadsRemaining?: number; // For limited download links // Metadata createdAt: Date; createdBy: string; inherited: boolean; // From parent folder? inheritedFrom?: string; // Which ancestor?} // Capabilities computed from permission levelconst CAPABILITIES = { [PermissionLevel.VIEWER]: ['view', 'download', 'metadata'], [PermissionLevel.COMMENTER]: ['view', 'download', 'metadata', 'comment', 'suggest'], [PermissionLevel.EDITOR]: ['view', 'download', 'metadata', 'comment', 'suggest', 'edit', 'share'], [PermissionLevel.OWNER]: ['view', 'download', 'metadata', 'comment', 'suggest', 'edit', 'share', 'delete', 'manage_permissions', 'transfer_ownership'],}; // Check if user can perform actionfunction hasCapability(permission: Permission, capability: string): boolean { return CAPABILITIES[permission.level].includes(capability);}| Capability | Viewer | Commenter | Editor | Owner |
|---|---|---|---|---|
| View content | ✓ | ✓ | ✓ | ✓ |
| Download | ✓ | ✓ | ✓ | ✓ |
| Comment | — | ✓ | ✓ | ✓ |
| Suggest edits | — | ✓ | ✓ | ✓ |
| Edit content | — | — | ✓ | ✓ |
| Add/remove files | — | — | ✓ | ✓ |
| Share with others | — | — | ✓* | ✓ |
| Manage permissions | — | — | — | ✓ |
| Delete permanently | — | — | — | ✓ |
| Transfer ownership | — | — | — | ✓ |
Whether editors can share is often configurable by the owner. Settings like 'Editors cannot change permissions and share' restrict editors to just editing. This prevents permission escalation where an editor shares with more people than the owner intended.
When you share a folder, all files within it inherit those permissions. This inheritance makes managing access to large folder structures practical.
Inheritance Model:
/Shared Projects/ ← Alice: Owner, Bob: Editor
├── Project A/ ← (Inherits: Alice: Owner, Bob: Editor)
│ ├── design.fig ← (Inherits permissions)
│ ├── spec.docx ← (Inherits permissions)
│ └── Private/ ← (Inherits) + Carol: Viewer (direct)
│ └── notes.txt ← (Inherits from Private)
└── Project B/ ← (Inherits) + David: Editor (direct)
└── report.pdf ← (Inherits from Project B)
Effective Permissions for report.pdf:
- Alice: Owner (from /Shared Projects/)
- Bob: Editor (from /Shared Projects/)
- David: Editor (from /Project B/)
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
// Resolve effective permissions for a resourceclass PermissionResolver { // Get all effective permissions for a file/folder async getEffectivePermissions(resourceId: string): Promise<EffectivePermission[]> { const resource = await this.getResource(resourceId); // Get ancestry path: [root, ..., parent, self] const ancestry = await this.getAncestry(resource.path); // Collect permissions from all ancestors const permissionMap = new Map<string, EffectivePermission>(); for (const ancestor of ancestry) { const permissions = await this.getDirectPermissions(ancestor.id); for (const perm of permissions) { const key = `${perm.granteeType}:${perm.granteeId}`; const existing = permissionMap.get(key); if (!existing || this.isHigherLevel(perm.level, existing.level)) { permissionMap.set(key, { ...perm, inherited: ancestor.id !== resourceId, inheritedFrom: ancestor.id !== resourceId ? ancestor.path : undefined, }); } } } return Array.from(permissionMap.values()); } // Check if user can perform action on resource async checkAccess( userId: string, resourceId: string, capability: string ): Promise<boolean> { // Get user's groups const groupIds = await this.getUserGroups(userId); // Get all effective permissions const permissions = await this.getEffectivePermissions(resourceId); // Find best matching permission for (const perm of permissions) { const matches = (perm.granteeType === 'anyone') || (perm.granteeType === 'user' && perm.granteeId === userId) || (perm.granteeType === 'group' && groupIds.includes(perm.granteeId!)); if (matches && hasCapability(perm, capability)) { // Check expiration if (perm.expiresAt && perm.expiresAt < new Date()) { continue; // Expired, try next } return true; } } return false; } private isHigherLevel(a: PermissionLevel, b: PermissionLevel): boolean { const order = ['viewer', 'commenter', 'editor', 'owner']; return order.indexOf(a) > order.indexOf(b); }}When a user moves a file from a folder they own to a shared folder, the file's inherited permissions change. Users are often surprised that moving a file grants access to others. Good UX warns users: 'Moving to /Team Folder will give 5 people access to this file. Continue?'
Shareable links enable sharing with anyone—no account required. They're essential for sharing with external partners, customers, or public audiences. But they're also the biggest security risk in file sharing systems.
Link Types:
1. Anyone with link (public):
https://storage.example.com/s/abc123def456
- No authentication required
- Link = access token
- Very convenient, but link leak = access leak
2. Anyone within organization:
https://storage.example.com/s/abc123def456
- Must be logged in to organization account
- Link alone isn't enough
- Good for internal sharing
3. Specific people only:
https://storage.example.com/s/abc123def456
- Must be logged in as one of the specific invited users
- Link is just a reference, not an access token
- Most secure, least convenient
4. Password-protected:
https://storage.example.com/s/abc123def456?p=hunter2
- Anyone with link AND password
- Adds friction, adds security
- Password can be shared separately from link
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
// Shareable link implementationinterface ShareLink { id: string; token: string; // The secret part of the URL resourceId: string; resourceType: 'file' | 'folder'; // Access control accessLevel: 'view' | 'comment' | 'edit'; requiresAuth: 'none' | 'any_account' | 'org_account' | 'specific_users'; allowedUsers?: string[]; // If specific_users password?: string; // Bcrypt hash if password protected // Restrictions expiresAt?: Date; maxDownloads?: number; currentDownloads: number; allowDownload: boolean; // Tracking createdAt: Date; createdBy: string; lastAccessedAt?: Date; accessCount: number;} class ShareLinkService { // Generate new share link async createLink( resourceId: string, options: CreateLinkOptions ): Promise<ShareLink> { // Generate cryptographically secure token const token = crypto.randomBytes(32).toString('base64url'); const link: ShareLink = { id: generateId(), token, resourceId, resourceType: options.type, accessLevel: options.accessLevel || 'view', requiresAuth: options.requiresAuth || 'none', allowedUsers: options.allowedUsers, password: options.password ? await bcrypt.hash(options.password, 12) : undefined, expiresAt: options.expiresAt, maxDownloads: options.maxDownloads, currentDownloads: 0, allowDownload: options.allowDownload !== false, createdAt: new Date(), createdBy: this.currentUser.id, accessCount: 0, }; await this.db.shareLinks.create(link); return link; } // Validate access via link async validateLinkAccess( token: string, password?: string, user?: User ): Promise<LinkValidation> { const link = await this.db.shareLinks.findByToken(token); if (!link) { return { valid: false, reason: 'Link not found' }; } // Check expiration if (link.expiresAt && link.expiresAt < new Date()) { return { valid: false, reason: 'Link expired' }; } // Check download limit if (link.maxDownloads && link.currentDownloads >= link.maxDownloads) { return { valid: false, reason: 'Download limit reached' }; } // Check password if (link.password) { if (!password) { return { valid: false, reason: 'Password required', needsPassword: true }; } const passwordValid = await bcrypt.compare(password, link.password); if (!passwordValid) { return { valid: false, reason: 'Invalid password' }; } } // Check authentication requirement if (link.requiresAuth === 'any_account' && !user) { return { valid: false, reason: 'Sign-in required', needsAuth: true }; } if (link.requiresAuth === 'org_account') { const resource = await this.getResource(link.resourceId); if (!user || user.orgId !== resource.orgId) { return { valid: false, reason: 'Organization sign-in required', needsAuth: true }; } } if (link.requiresAuth === 'specific_users') { if (!user || !link.allowedUsers!.includes(user.id)) { return { valid: false, reason: 'You do not have access' }; } } // Track access await this.trackAccess(link, user); return { valid: true, link, accessLevel: link.accessLevel }; }}Link tokens must be cryptographically random and long enough to prevent guessing. A 256-bit (32 byte) token has 2^256 possibilities—practically impossible to guess. Hash tokens before storing (like passwords) if you need to revoke a link knowing only the URL. Never log full tokens.
Enterprise sharing requires managing permissions for large numbers of users. Groups allow granting access to many users at once, and managing access centrally.
Group Types:
1. User-Created Groups:
- Created by any user
- Membership managed by creator
- e.g., "Project Alpha Team", "External Contractors"
2. Organization Groups:
- Created by admins
- Often synced from directory (LDAP, SCIM)
- e.g., "engineering@company.com", "executives@company.com"
3. Dynamic Groups:
- Membership computed from rules
- Auto-update as org structure changes
- e.g., "All users in Engineering department"
4. Nested Groups:
- Groups can contain other groups
- Complex hierarchies possible
- e.g., "Engineering" contains "Backend", "Frontend", "DevOps"
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
// Group management and resolutioninterface Group { id: string; name: string; description?: string; type: 'user_created' | 'org_managed' | 'dynamic'; // Membership members: string[]; // Direct member user IDs memberGroups: string[]; // Nested group IDs // Dynamic group rules dynamicRules?: { field: string; // e.g., 'department' operator: 'equals' | 'contains' | 'startsWith'; value: string; // e.g., 'Engineering' }[]; // Metadata orgId: string; createdBy: string; createdAt: Date; lastSyncedAt?: Date; // For directory-synced groups} class GroupResolver { // Get all groups a user belongs to (including nested) async getUserGroups(userId: string): Promise<string[]> { const directGroups = await this.db.groups.findMemberOf(userId); const allGroups = new Set<string>(); // BFS to resolve nested groups const queue = [...directGroups.map(g => g.id)]; while (queue.length > 0) { const groupId = queue.shift()!; if (allGroups.has(groupId)) continue; allGroups.add(groupId); // Find groups that contain this group as a member const parentGroups = await this.db.groups.findContaining(groupId); queue.push(...parentGroups.map(g => g.id)); } return Array.from(allGroups); } // Get all effective members of a group (including nested) async getGroupMembers(groupId: string): Promise<string[]> { const group = await this.db.groups.findById(groupId); const allMembers = new Set<string>(); // Direct members group.members.forEach(m => allMembers.add(m)); // Nested group members for (const nestedId of group.memberGroups) { const nestedMembers = await this.getGroupMembers(nestedId); nestedMembers.forEach(m => allMembers.add(m)); } // Dynamic members if (group.dynamicRules) { const dynamicMembers = await this.resolveDynamicMembers(group.dynamicRules); dynamicMembers.forEach(m => allMembers.add(m)); } return Array.from(allMembers); } // Cache group membership for performance private membershipCache = new LRUCache<string, string[]>({ max: 10000, ttl: 60 * 1000, // 1 minute cache });}If Group A contains Group B, and Group B contains Group A, you have an infinite loop. Prevent cycles during group creation/update by detecting transitive membership. If adding B to A would create a cycle (A is already in B's expanded membership), reject the operation.
Every file operation requires an access check. These checks must be fast—adding 100ms to every file read would make the system unusable. Production systems employ sophisticated caching and denormalization.
| Scenario | Target Latency | Strategy |
|---|---|---|
| Cached hit | <1 ms | In-memory cache (Redis, local) |
| Cache miss, owner check | <5 ms | Single DB query |
| Cache miss, with groups | <20 ms | Query + group resolution |
| Cache miss, deep inheritance | <50 ms | Path traversal + caching |
| Full permission refresh | <200 ms | Async background job |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
// High-performance permission cachinginterface CachedPermission { userId: string; resourceId: string; effectiveLevel: PermissionLevel | null; // null = no access source: 'owner' | 'direct' | 'group' | 'inherited' | 'link'; expiresAt: Date; computedAt: Date;} class PermissionCache { private redis: RedisClient; private localCache: LRUCache<string, CachedPermission>; // Fast path: check cache first async checkAccess( userId: string, resourceId: string, capability: string ): Promise<boolean> { const key = `perm:${userId}:${resourceId}`; // Level 1: Local in-memory cache (fastest, ~0.1ms) const local = this.localCache.get(key); if (local && local.expiresAt > new Date()) { return hasCapability(local, capability); } // Level 2: Distributed cache (fast, ~1ms) const cached = await this.redis.get(key); if (cached) { const parsed = JSON.parse(cached); this.localCache.set(key, parsed); // Populate local return hasCapability(parsed, capability); } // Level 3: Compute and cache (~10-50ms) const computed = await this.computePermission(userId, resourceId); await this.cachePermission(key, computed); return hasCapability(computed, capability); } // Invalidate cache when permissions change async onPermissionChange(resourceId: string): Promise<void> { // Get all users with permissions on this resource const affectedUsers = await this.getAffectedUsers(resourceId); // Invalidate their cached permissions const keys = affectedUsers.map(u => `perm:${u}:${resourceId}`); await this.redis.del(...keys); // Also invalidate for all descendants (if folder) const resource = await this.getResource(resourceId); if (resource.type === 'folder') { await this.invalidateDescendants(resourceId); } } async onGroupMembershipChange(groupId: string, userId: string): Promise<void> { // Get all resources shared with this group const resources = await this.getGroupSharedResources(groupId); // Invalidate user's cached permissions for those resources const keys = resources.map(r => `perm:${userId}:${r.id}`); await this.redis.del(...keys); } // Batch permission check for file listing async checkAccessBatch( userId: string, resourceIds: string[] ): Promise<Map<string, boolean>> { // Multi-get from Redis const keys = resourceIds.map(r => `perm:${userId}:${r}`); const cached = await this.redis.mget(...keys); const results = new Map<string, boolean>(); const missing: string[] = []; cached.forEach((value, index) => { if (value) { results.set(resourceIds[index], hasCapability(JSON.parse(value), 'view')); } else { missing.push(resourceIds[index]); } }); // Batch compute missing if (missing.length > 0) { const computed = await this.computePermissionBatch(userId, missing); for (const [resourceId, perm] of computed) { results.set(resourceId, hasCapability(perm, 'view')); } } return results; }}When listing a folder's contents, don't check permissions file-by-file. Batch the check: 'Which of these 100 files can user X view?' One query returns all accessible files. This is critical for folder listings to be fast.
Enterprise organizations often need to share with external partners—vendors, customers, consultants—who aren't part of the organization. This creates unique security and compliance challenges.
| Capability | Internal User | External Guest |
|---|---|---|
| View shared files | ✓ | ✓ |
| Edit shared files | ✓ | If granted |
| Download | ✓ | If allowed |
| Create share links | ✓ | Usually no |
| Reshare with others | ✓ | Usually no |
| Access admin features | ✓ | Never |
| Visible in directory | ✓ | No |
| Activity fully audited | ✓ | ✓ (enhanced) |
External users may retain copies of files after access revoked. Downloads, screenshots, email attachments—data leaves your control. For highly sensitive data, consider not allowing external sharing at all. Technical controls can limit but not eliminate this risk.
Enterprise customers require detailed audit trails for compliance with regulations like SOX, HIPAA, GDPR, and industry-specific requirements. The sharing system must log every access and permission change.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
// Comprehensive audit logginginterface AuditEvent { id: string; timestamp: Date; // Actor actorId: string; actorType: 'user' | 'admin' | 'system' | 'api_key'; actorEmail: string; // Action action: AuditAction; resourceId: string; resourcePath: string; resourceType: 'file' | 'folder' | 'link' | 'permission'; // Context ipAddress: string; userAgent: string; deviceId?: string; sessionId: string; // Specifics details: Record<string, any>; // Outcome success: boolean; failureReason?: string;} type AuditAction = | 'file.view' | 'file.download' | 'file.upload' | 'file.edit' | 'file.delete' | 'file.restore' | 'file.move' | 'file.copy' | 'permission.grant' | 'permission.revoke' | 'permission.modify' | 'link.create' | 'link.access' | 'link.revoke' | 'share.external' | 'admin.export' | 'admin.audit_view'; // Audit log queries for complianceclass AuditService { // Who accessed this file? async getFileAccessHistory( fileId: string, since: Date ): Promise<AuditEvent[]> { return this.db.auditEvents .where({ resourceId: fileId, action: { $in: ['file.view', 'file.download'] } }) .where({ timestamp: { $gte: since } }) .orderBy('timestamp', 'desc') .limit(1000); } // What has this user accessed? async getUserActivityReport( userId: string, since: Date ): Promise<AuditEvent[]> { return this.db.auditEvents .where({ actorId: userId }) .where({ timestamp: { $gte: since } }) .orderBy('timestamp', 'desc') .limit(10000); } // Permission change history async getPermissionChanges( resourceId: string ): Promise<AuditEvent[]> { return this.db.auditEvents .where({ resourceId }) .where({ action: { $regex: /^permission\./ } }) .orderBy('timestamp', 'desc'); } // External access report (for compliance) async getExternalAccessReport( orgId: string, since: Date ): Promise<ExternalAccessSummary[]> { const events = await this.db.auditEvents .where({ action: 'link.access', actorType: 'user', timestamp: { $gte: since } }) .join('users', 'actorId', 'id') .where('users.orgId', '!=', orgId); // Not from our org return this.summarizeByUser(events); }}Compliance often mandates long audit log retention (7 years for SOX, HIPAA). This creates massive data volumes. Solutions: (1) Tiered storage (hot/warm/cold), (2) Summarized aggregates for old logs, (3) Separate audit log database optimized for write-heavy workloads. Never delete audit logs without legal/compliance review.
Sharing and permissions transform personal storage into a collaboration platform. This page covered the complete architecture of enterprise-grade access control:
Module Complete:
Congratulations! You've now completed the comprehensive module on designing cloud file storage systems like Google Drive and Dropbox. You've learned:
These concepts form the foundation for designing any cloud storage or collaboration system.
You now have a complete understanding of cloud file storage system design. From low-level chunking algorithms to high-level permission models, you can design, discuss, and critique systems like Dropbox, Google Drive, or any file collaboration platform. Apply these patterns to your own system designs!