Harden storage permissions and WebDAV security
Permission service:
- Add hasAccess() with explicit minLevel param, deprecate canAccess()
- Cycle protection + depth limit (50) on all parent traversal
- Pick highest access level across multiple roles (was using first match)
- isPublic only grants view on directly requested folder, not inherited
- Sanitize file extension from content-type
- Clean up orphaned traverse perms when removing permissions
- Add getPermissionById() for authz checks on permission deletion
Storage routes:
- All write ops require edit via hasAccess() — traverse can no longer
create folders, upload files, rename, toggle isPublic, or delete
- Permission delete requires admin access on the folder
- Permission list requires admin access on the folder
- Folder children listing filtered by user access
- File search results filtered by user access (was returning all)
- Signed URL requires view (was using canAccess which allows traverse)
WebDAV:
- 100MB upload size limit (was unbounded — OOM risk)
- PROPFIND root filters folders by user access (was listing all)
- COPY uses hasAccess('view') not canAccess (traverse bypass)
- All writes use hasAccess('edit') consistently
- MKCOL at root requires files.delete permission
- Lock ownership enforced on UNLOCK (was allowing any user)
- Lock conflict check on LOCK (423 if locked by another user)
- Lock enforcement on PUT and DELETE (423 if locked by another)
- Max 100 locks per user, periodic expired lock cleanup
- Path traversal protection: reject .. and null bytes in segments
- Brute-force protection: 10 failed attempts per IP, 5min lockout
This commit is contained in:
@@ -15,10 +15,10 @@ export const storageRoutes: FastifyPluginAsync = async (app) => {
|
||||
const { name, parentId, isPublic } = request.body as { name?: string; parentId?: string; isPublic?: boolean }
|
||||
if (!name?.trim()) throw new ValidationError('Folder name is required')
|
||||
|
||||
// Check parent access if creating subfolder
|
||||
// Check parent access if creating subfolder — require at least edit (not traverse or view)
|
||||
if (parentId) {
|
||||
const accessLevel = await StoragePermissionService.getAccessLevel(app.db, parentId, request.user.id)
|
||||
if (!accessLevel || accessLevel === 'view') {
|
||||
const hasEdit = await StoragePermissionService.hasAccess(app.db, parentId, request.user.id, 'edit')
|
||||
if (!hasEdit) {
|
||||
return reply.status(403).send({ error: { message: 'No edit access to parent folder', statusCode: 403 } })
|
||||
}
|
||||
}
|
||||
@@ -29,10 +29,16 @@ export const storageRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
app.get('/storage/folders', { preHandler: [app.authenticate, app.requirePermission('files.view')] }, async (request, reply) => {
|
||||
const { parentId } = request.query as { parentId?: string }
|
||||
const folders = parentId
|
||||
const allChildren = parentId
|
||||
? await StorageFolderService.listChildren(app.db, parentId)
|
||||
: await StorageFolderService.listChildren(app.db, null)
|
||||
return reply.send({ data: folders })
|
||||
// Filter to only folders the user can access (at least traverse)
|
||||
const accessible = []
|
||||
for (const folder of allChildren) {
|
||||
const level = await StoragePermissionService.getAccessLevel(app.db, folder.id, request.user.id)
|
||||
if (level) accessible.push(folder)
|
||||
}
|
||||
return reply.send({ data: accessible })
|
||||
})
|
||||
|
||||
app.get('/storage/folders/tree', { preHandler: [app.authenticate, app.requirePermission('files.view')] }, async (request, reply) => {
|
||||
@@ -56,8 +62,8 @@ export const storageRoutes: FastifyPluginAsync = async (app) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const { name, isPublic } = request.body as { name?: string; isPublic?: boolean }
|
||||
|
||||
const accessLevel = await StoragePermissionService.getAccessLevel(app.db, id, request.user.id)
|
||||
if (!accessLevel || accessLevel === 'view') {
|
||||
const hasEdit = await StoragePermissionService.hasAccess(app.db, id, request.user.id, 'edit')
|
||||
if (!hasEdit) {
|
||||
return reply.status(403).send({ error: { message: 'No edit access', statusCode: 403 } })
|
||||
}
|
||||
|
||||
@@ -83,6 +89,9 @@ export const storageRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
app.get('/storage/folders/:id/permissions', { preHandler: [app.authenticate, app.requirePermission('files.view')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
// Only folder admins can view permission details
|
||||
const hasAdmin = await StoragePermissionService.hasAccess(app.db, id, request.user.id, 'admin')
|
||||
if (!hasAdmin) return reply.status(403).send({ error: { message: 'Admin access required', statusCode: 403 } })
|
||||
const permissions = await StoragePermissionService.listPermissions(app.db, id)
|
||||
return reply.send({ data: permissions })
|
||||
})
|
||||
@@ -105,9 +114,18 @@ export const storageRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
app.delete('/storage/folder-permissions/:id', { preHandler: [app.authenticate, app.requirePermission('files.delete')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const perm = await StoragePermissionService.removePermission(app.db, id)
|
||||
// Look up the permission to find which folder it belongs to
|
||||
const existing = await StoragePermissionService.listPermissions(app.db, id)
|
||||
// listPermissions takes folderId, we need to find by perm id — use removePermission which fetches first
|
||||
const perm = await StoragePermissionService.getPermissionById(app.db, id)
|
||||
if (!perm) return reply.status(404).send({ error: { message: 'Permission not found', statusCode: 404 } })
|
||||
return reply.send(perm)
|
||||
|
||||
// Verify admin access on the folder this permission belongs to
|
||||
const hasAdmin = await StoragePermissionService.hasAccess(app.db, perm.folderId, request.user.id, 'admin')
|
||||
if (!hasAdmin) return reply.status(403).send({ error: { message: 'Admin access required', statusCode: 403 } })
|
||||
|
||||
const deleted = await StoragePermissionService.removePermission(app.db, id)
|
||||
return reply.send(deleted)
|
||||
})
|
||||
|
||||
// --- Files ---
|
||||
@@ -115,8 +133,8 @@ export const storageRoutes: FastifyPluginAsync = async (app) => {
|
||||
app.post('/storage/folders/:folderId/files', { preHandler: [app.authenticate, app.requirePermission('files.upload')] }, async (request, reply) => {
|
||||
const { folderId } = request.params as { folderId: string }
|
||||
|
||||
const accessLevel = await StoragePermissionService.getAccessLevel(app.db, folderId, request.user.id)
|
||||
if (!accessLevel || accessLevel === 'view') {
|
||||
const hasEdit = await StoragePermissionService.hasAccess(app.db, folderId, request.user.id, 'edit')
|
||||
if (!hasEdit) {
|
||||
return reply.status(403).send({ error: { message: 'No edit access to this folder', statusCode: 403 } })
|
||||
}
|
||||
|
||||
@@ -153,8 +171,8 @@ export const storageRoutes: FastifyPluginAsync = async (app) => {
|
||||
const file = await StorageFileService.getById(app.db, id)
|
||||
if (!file) return reply.status(404).send({ error: { message: 'File not found', statusCode: 404 } })
|
||||
|
||||
const accessLevel = await StoragePermissionService.getAccessLevel(app.db, file.folderId, request.user.id)
|
||||
if (!accessLevel || accessLevel === 'view') {
|
||||
const hasEdit = await StoragePermissionService.hasAccess(app.db, file.folderId, request.user.id, 'edit')
|
||||
if (!hasEdit) {
|
||||
return reply.status(403).send({ error: { message: 'No edit access', statusCode: 403 } })
|
||||
}
|
||||
|
||||
@@ -167,8 +185,8 @@ export const storageRoutes: FastifyPluginAsync = async (app) => {
|
||||
const file = await StorageFileService.getById(app.db, id)
|
||||
if (!file) return reply.status(404).send({ error: { message: 'File not found', statusCode: 404 } })
|
||||
|
||||
const hasAccess = await StoragePermissionService.canAccess(app.db, file.folderId, request.user.id)
|
||||
if (!hasAccess) return reply.status(403).send({ error: { message: 'Access denied', statusCode: 403 } })
|
||||
const hasView = await StoragePermissionService.hasAccess(app.db, file.folderId, request.user.id, 'view')
|
||||
if (!hasView) return reply.status(403).send({ error: { message: 'Access denied', statusCode: 403 } })
|
||||
|
||||
const token = app.jwt.sign({ path: file.path, purpose: 'file-access' } as any, { expiresIn: '15m' })
|
||||
const signedUrl = `/v1/files/s/${file.path}?token=${token}`
|
||||
@@ -181,6 +199,14 @@ export const storageRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const result = await StorageFileService.search(app.db, q, params)
|
||||
return reply.send(result)
|
||||
|
||||
// Filter results to only files the user has at least view access to
|
||||
const filtered = []
|
||||
for (const file of result.data) {
|
||||
const hasView = await StoragePermissionService.hasAccess(app.db, (file as any).folderId, request.user.id, 'view')
|
||||
if (hasView) filtered.push(file)
|
||||
}
|
||||
|
||||
return reply.send({ data: filtered, pagination: { ...result.pagination, total: filtered.length } })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,10 +12,20 @@ const DAV_METHODS = 'OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL
|
||||
const WEBDAV_PREFIX = '/webdav'
|
||||
|
||||
export const webdavRoutes: FastifyPluginAsync = async (app) => {
|
||||
// Parse all request bodies as raw buffers for WebDAV
|
||||
// Parse all request bodies as raw buffers for WebDAV, with size limit
|
||||
const MAX_UPLOAD_SIZE = 100 * 1024 * 1024 // 100MB
|
||||
app.addContentTypeParser('*', function (_request, payload, done) {
|
||||
const chunks: Buffer[] = []
|
||||
payload.on('data', (chunk: Buffer) => chunks.push(chunk))
|
||||
let totalSize = 0
|
||||
payload.on('data', (chunk: Buffer) => {
|
||||
totalSize += chunk.length
|
||||
if (totalSize > MAX_UPLOAD_SIZE) {
|
||||
payload.destroy()
|
||||
done(new Error('Payload too large'), undefined)
|
||||
return
|
||||
}
|
||||
chunks.push(chunk)
|
||||
})
|
||||
payload.on('end', () => done(null, Buffer.concat(chunks)))
|
||||
payload.on('error', done)
|
||||
})
|
||||
@@ -85,7 +95,19 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
|
||||
})
|
||||
if (depth !== '0') {
|
||||
const children = await WebDavService.listChildren(app.db, null, `${WEBDAV_PREFIX}/`)
|
||||
resources.push(...children)
|
||||
// Filter to only folders the user has access to
|
||||
const filtered = []
|
||||
for (const child of children) {
|
||||
if (!child.isCollection) continue // root has no files
|
||||
// Extract folder name from href to resolve access
|
||||
const folderName = decodeURIComponent(child.href.replace(`${WEBDAV_PREFIX}/`, '').replace(/\/$/, ''))
|
||||
const resolved = await WebDavService.resolvePath(app.db, '/' + folderName)
|
||||
if (resolved.type === 'folder' && resolved.folder) {
|
||||
const level = await StoragePermissionService.getAccessLevel(app.db, resolved.folder.id, request.user.id)
|
||||
if (level) filtered.push(child)
|
||||
}
|
||||
}
|
||||
resources.push(...filtered)
|
||||
}
|
||||
} else if (resolved.type === 'folder' && resolved.folder) {
|
||||
// Check access — traverse or higher lets you see the folder
|
||||
@@ -144,7 +166,17 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
if (depth !== '0') {
|
||||
const children = await WebDavService.listChildren(app.db, null, `${WEBDAV_PREFIX}/`)
|
||||
resources.push(...children)
|
||||
const filtered = []
|
||||
for (const child of children) {
|
||||
if (!child.isCollection) continue
|
||||
const folderName = decodeURIComponent(child.href.replace(`${WEBDAV_PREFIX}/`, '').replace(/\/$/, ''))
|
||||
const childResolved = await WebDavService.resolvePath(app.db, '/' + folderName)
|
||||
if (childResolved.type === 'folder' && childResolved.folder) {
|
||||
const level = await StoragePermissionService.getAccessLevel(app.db, childResolved.folder.id, request.user.id)
|
||||
if (level) filtered.push(child)
|
||||
}
|
||||
}
|
||||
resources.push(...filtered)
|
||||
}
|
||||
|
||||
const xml = buildMultistatus(resources)
|
||||
@@ -189,15 +221,19 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
if (!name) return reply.status(400).send('Invalid path')
|
||||
|
||||
// Check if resource is locked by another user
|
||||
const lockCheck = checkLock(resourcePath, request.user.id)
|
||||
if (lockCheck.byOther) return reply.status(423).send('Resource is locked by another user')
|
||||
|
||||
// Resolve parent to find the folder
|
||||
const parentResolved = await WebDavService.resolvePath(app.db, parentPath)
|
||||
if (parentResolved.type !== 'folder' || !parentResolved.folder) {
|
||||
return reply.status(409).send('Parent folder not found')
|
||||
}
|
||||
|
||||
// Check edit permission
|
||||
const accessLevel = await StoragePermissionService.getAccessLevel(app.db, parentResolved.folder.id, request.user.id)
|
||||
if (!accessLevel || accessLevel === 'view') {
|
||||
// Check edit permission — traverse and view are not enough
|
||||
const hasEdit = await StoragePermissionService.hasAccess(app.db, parentResolved.folder.id, request.user.id, 'edit')
|
||||
if (!hasEdit) {
|
||||
return reply.status(403).send('No edit access')
|
||||
}
|
||||
|
||||
@@ -227,11 +263,16 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
|
||||
// --- DELETE ---
|
||||
app.delete('/*', { preHandler: auth }, async (request, reply) => {
|
||||
const resourcePath = getResourcePath(request)
|
||||
|
||||
// Check if resource is locked by another user
|
||||
const lockCheck = checkLock(resourcePath, request.user.id)
|
||||
if (lockCheck.byOther) return reply.status(423).send('Resource is locked by another user')
|
||||
|
||||
const resolved = await WebDavService.resolvePath(app.db, resourcePath)
|
||||
|
||||
if (resolved.type === 'file' && resolved.file && resolved.folder) {
|
||||
const accessLevel = await StoragePermissionService.getAccessLevel(app.db, resolved.folder.id, request.user.id)
|
||||
if (!accessLevel || accessLevel === 'view') return reply.status(403).send('No edit access')
|
||||
const hasEdit = await StoragePermissionService.hasAccess(app.db, resolved.folder.id, request.user.id, 'edit')
|
||||
if (!hasEdit) return reply.status(403).send('No edit access')
|
||||
await StorageFileService.delete(app.db, app.storage, resolved.file.id)
|
||||
return reply.status(204).send('')
|
||||
}
|
||||
@@ -268,12 +309,16 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
|
||||
if (parentResolved.type !== 'folder' || !parentResolved.folder) {
|
||||
return reply.status(409).send('Parent folder not found')
|
||||
}
|
||||
// Check edit permission on parent
|
||||
const accessLevel = await StoragePermissionService.getAccessLevel(app.db, parentResolved.folder.id, request.user.id)
|
||||
if (!accessLevel || accessLevel === 'view') {
|
||||
const hasEdit = await StoragePermissionService.hasAccess(app.db, parentResolved.folder.id, request.user.id, 'edit')
|
||||
if (!hasEdit) {
|
||||
return reply.status(403).send('No edit access to parent folder')
|
||||
}
|
||||
parentFolderId = parentResolved.folder.id
|
||||
} else {
|
||||
// Creating top-level folder requires files.admin permission
|
||||
if (!request.permissions.has('files.delete')) {
|
||||
return reply.status(403).send('Admin permission required to create top-level folders')
|
||||
}
|
||||
}
|
||||
|
||||
await StorageFolderService.create(app.db, {
|
||||
@@ -308,9 +353,9 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.status(404).send('Source not found (only file copy supported)')
|
||||
}
|
||||
|
||||
// Check read access on source
|
||||
const srcAccess = await StoragePermissionService.canAccess(app.db, resolved.folder.id, request.user.id)
|
||||
if (!srcAccess) return reply.status(403).send('No access to source')
|
||||
// Check view access on source (traverse is not enough to read file data)
|
||||
const hasSrcView = await StoragePermissionService.hasAccess(app.db, resolved.folder.id, request.user.id, 'view')
|
||||
if (!hasSrcView) return reply.status(403).send('No access to source')
|
||||
|
||||
// Resolve destination parent
|
||||
const { parentPath, name: destName } = WebDavService.parseParentAndName(destPath)
|
||||
@@ -320,8 +365,8 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
|
||||
}
|
||||
|
||||
// Check edit access on destination
|
||||
const destAccess = await StoragePermissionService.getAccessLevel(app.db, destParent.folder.id, request.user.id)
|
||||
if (!destAccess || destAccess === 'view') return reply.status(403).send('No edit access to destination')
|
||||
const hasDestEdit = await StoragePermissionService.hasAccess(app.db, destParent.folder.id, request.user.id, 'edit')
|
||||
if (!hasDestEdit) return reply.status(403).send('No edit access to destination')
|
||||
|
||||
// Copy: read source file data and upload to destination
|
||||
const fileData = await app.storage.get(resolved.file.path)
|
||||
@@ -366,8 +411,8 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
if (resolved.type === 'file' && resolved.file && resolved.folder) {
|
||||
// Check edit access on source folder
|
||||
const srcAccess = await StoragePermissionService.getAccessLevel(app.db, resolved.folder.id, request.user.id)
|
||||
if (!srcAccess || srcAccess === 'view') return reply.status(403).send('No edit access to source')
|
||||
const hasSrcEdit = await StoragePermissionService.hasAccess(app.db, resolved.folder.id, request.user.id, 'edit')
|
||||
if (!hasSrcEdit) return reply.status(403).send('No edit access to source')
|
||||
|
||||
// Resolve destination
|
||||
const { parentPath, name: destName } = WebDavService.parseParentAndName(destPath)
|
||||
@@ -375,8 +420,8 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
|
||||
if (destParent.type !== 'folder' || !destParent.folder) {
|
||||
return reply.status(409).send('Destination parent not found')
|
||||
}
|
||||
const destAccess = await StoragePermissionService.getAccessLevel(app.db, destParent.folder.id, request.user.id)
|
||||
if (!destAccess || destAccess === 'view') return reply.status(403).send('No edit access to destination')
|
||||
const hasDestEdit = await StoragePermissionService.hasAccess(app.db, destParent.folder.id, request.user.id, 'edit')
|
||||
if (!hasDestEdit) return reply.status(403).send('No edit access to destination')
|
||||
|
||||
// Move: copy data then delete source
|
||||
const overwrite = (request.headers['overwrite'] as string)?.toUpperCase() !== 'F'
|
||||
@@ -412,8 +457,8 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
|
||||
if (destParent.type === 'folder' || destParent.type === 'root') {
|
||||
const newParentId = destParent.type === 'folder' ? destParent.folder?.id : undefined
|
||||
if (newParentId) {
|
||||
const destAccess = await StoragePermissionService.getAccessLevel(app.db, newParentId, request.user.id)
|
||||
if (!destAccess || destAccess === 'view') return reply.status(403).send('No edit access to destination')
|
||||
const hasDestEdit = await StoragePermissionService.hasAccess(app.db, newParentId, request.user.id, 'edit')
|
||||
if (!hasDestEdit) return reply.status(403).send('No edit access to destination')
|
||||
}
|
||||
|
||||
await StorageFolderService.update(app.db, resolved.folder.id, { name: destName })
|
||||
@@ -428,6 +473,23 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
|
||||
},
|
||||
})
|
||||
|
||||
// --- Lock helpers ---
|
||||
const MAX_LOCKS_PER_USER = 100
|
||||
const LOCK_TIMEOUT = 300 // 5 minutes
|
||||
|
||||
function cleanExpiredLocks() {
|
||||
const now = Date.now()
|
||||
for (const [path, lock] of locks) {
|
||||
if (lock.expires < now) locks.delete(path)
|
||||
}
|
||||
}
|
||||
|
||||
function checkLock(resourcePath: string, userId: string): { locked: boolean; byOther: boolean } {
|
||||
const lock = locks.get(resourcePath)
|
||||
if (!lock || lock.expires < Date.now()) return { locked: false, byOther: false }
|
||||
return { locked: true, byOther: lock.owner !== userId }
|
||||
}
|
||||
|
||||
// --- LOCK ---
|
||||
app.route({
|
||||
method: 'LOCK' as any,
|
||||
@@ -435,22 +497,27 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
|
||||
preHandler: auth,
|
||||
handler: async (request, reply) => {
|
||||
const resourcePath = getResourcePath(request)
|
||||
const timeout = 300 // 5 minutes
|
||||
const token = `opaquelocktoken:${randomUUID()}`
|
||||
cleanExpiredLocks()
|
||||
|
||||
// Clean expired locks
|
||||
const now = Date.now()
|
||||
for (const [path, lock] of locks) {
|
||||
if (lock.expires < now) locks.delete(path)
|
||||
// Check if locked by another user
|
||||
const existing = locks.get(resourcePath)
|
||||
if (existing && existing.expires > Date.now() && existing.owner !== request.user.id) {
|
||||
return reply.status(423).send('Resource is locked by another user')
|
||||
}
|
||||
|
||||
locks.set(resourcePath, {
|
||||
token,
|
||||
owner: request.user.id,
|
||||
expires: now + timeout * 1000,
|
||||
})
|
||||
// Cap locks per user
|
||||
let userLockCount = 0
|
||||
for (const lock of locks.values()) {
|
||||
if (lock.owner === request.user.id && lock.expires > Date.now()) userLockCount++
|
||||
}
|
||||
if (userLockCount >= MAX_LOCKS_PER_USER) {
|
||||
return reply.status(400).send('Too many active locks')
|
||||
}
|
||||
|
||||
const xml = buildLockResponse(token, request.user.id, timeout)
|
||||
const token = `opaquelocktoken:${randomUUID()}`
|
||||
locks.set(resourcePath, { token, owner: request.user.id, expires: Date.now() + LOCK_TIMEOUT * 1000 })
|
||||
|
||||
const xml = buildLockResponse(token, request.user.id, LOCK_TIMEOUT)
|
||||
return reply
|
||||
.header('Content-Type', 'application/xml; charset=utf-8')
|
||||
.header('Lock-Token', `<${token}>`)
|
||||
@@ -459,15 +526,15 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
|
||||
},
|
||||
})
|
||||
|
||||
// LOCK on root
|
||||
app.route({
|
||||
method: 'LOCK' as any,
|
||||
url: '/',
|
||||
preHandler: auth,
|
||||
handler: async (request, reply) => {
|
||||
cleanExpiredLocks()
|
||||
const token = `opaquelocktoken:${randomUUID()}`
|
||||
const xml = buildLockResponse(token, request.user.id, 300)
|
||||
locks.set('/', { token, owner: request.user.id, expires: Date.now() + 300000 })
|
||||
locks.set('/', { token, owner: request.user.id, expires: Date.now() + LOCK_TIMEOUT * 1000 })
|
||||
const xml = buildLockResponse(token, request.user.id, LOCK_TIMEOUT)
|
||||
return reply
|
||||
.header('Content-Type', 'application/xml; charset=utf-8')
|
||||
.header('Lock-Token', `<${token}>`)
|
||||
@@ -483,6 +550,11 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
|
||||
preHandler: auth,
|
||||
handler: async (request, reply) => {
|
||||
const resourcePath = getResourcePath(request)
|
||||
const lock = locks.get(resourcePath)
|
||||
// Only the lock owner can unlock
|
||||
if (lock && lock.owner !== request.user.id) {
|
||||
return reply.status(403).send('Not the lock owner')
|
||||
}
|
||||
locks.delete(resourcePath)
|
||||
return reply.status(204).send('')
|
||||
},
|
||||
@@ -492,7 +564,11 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
|
||||
method: 'UNLOCK' as any,
|
||||
url: '/',
|
||||
preHandler: auth,
|
||||
handler: async (_request, reply) => {
|
||||
handler: async (request, reply) => {
|
||||
const lock = locks.get('/')
|
||||
if (lock && lock.owner !== request.user.id) {
|
||||
return reply.status(403).send('Not the lock owner')
|
||||
}
|
||||
locks.delete('/')
|
||||
return reply.status(204).send('')
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user