Add traverse access level for folder navigation without file access
When a permission is set on a nested folder, traverse is automatically granted on all ancestor folders so users can navigate to it. Traverse only shows subfolders in listings — files are hidden. This prevents orphaned permissions where a user has access to a nested folder but can't reach it. Hierarchy: traverse < view < edit < admin
This commit is contained in:
@@ -97,7 +97,7 @@ export const storageRoutes: FastifyPluginAsync = async (app) => {
|
||||
}
|
||||
|
||||
if (!roleId && !userId) throw new ValidationError('Either roleId or userId is required')
|
||||
if (!accessLevel || !['view', 'edit', 'admin'].includes(accessLevel)) throw new ValidationError('accessLevel must be view, edit, or admin')
|
||||
if (!accessLevel || !['traverse', 'view', 'edit', 'admin'].includes(accessLevel)) throw new ValidationError('accessLevel must be traverse, view, edit, or admin')
|
||||
|
||||
const perm = await StoragePermissionService.setPermission(app.db, id, roleId, userId, accessLevel)
|
||||
return reply.status(201).send(perm)
|
||||
@@ -139,8 +139,9 @@ export const storageRoutes: FastifyPluginAsync = async (app) => {
|
||||
app.get('/storage/folders/:folderId/files', { preHandler: [app.authenticate, app.requirePermission('files.view')] }, async (request, reply) => {
|
||||
const { folderId } = request.params as { folderId: string }
|
||||
|
||||
const hasAccess = await StoragePermissionService.canAccess(app.db, folderId, request.user.id)
|
||||
if (!hasAccess) return reply.status(403).send({ error: { message: 'Access denied', statusCode: 403 } })
|
||||
// traverse only allows navigating folders, not viewing files
|
||||
const accessLevel = await StoragePermissionService.getAccessLevel(app.db, folderId, request.user.id)
|
||||
if (!accessLevel || accessLevel === 'traverse') return reply.status(403).send({ error: { message: 'Access denied', statusCode: 403 } })
|
||||
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const result = await StorageFileService.listByFolder(app.db, folderId, params)
|
||||
|
||||
@@ -88,9 +88,9 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
|
||||
resources.push(...children)
|
||||
}
|
||||
} else if (resolved.type === 'folder' && resolved.folder) {
|
||||
// Check access
|
||||
const hasAccess = await StoragePermissionService.canAccess(app.db, resolved.folder.id, request.user.id)
|
||||
if (!hasAccess) return reply.status(403).send('Access Denied')
|
||||
// Check access — traverse or higher lets you see the folder
|
||||
const accessLevel = await StoragePermissionService.getAccessLevel(app.db, resolved.folder.id, request.user.id)
|
||||
if (!accessLevel) return reply.status(403).send('Access Denied')
|
||||
|
||||
resources.push({
|
||||
href: basePath.slice(0, -1) + '/',
|
||||
@@ -100,12 +100,14 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
|
||||
createdAt: resolved.folder.createdAt ? new Date(resolved.folder.createdAt) : undefined,
|
||||
})
|
||||
if (depth !== '0') {
|
||||
const children = await WebDavService.listChildren(app.db, resolved.folder.id, basePath)
|
||||
// traverse: show subfolders only, no files. view+: show everything
|
||||
const children = await WebDavService.listChildren(app.db, resolved.folder.id, basePath, accessLevel === 'traverse')
|
||||
resources.push(...children)
|
||||
}
|
||||
} else if (resolved.type === 'file' && resolved.file && resolved.folder) {
|
||||
const hasAccess = await StoragePermissionService.canAccess(app.db, resolved.folder.id, request.user.id)
|
||||
if (!hasAccess) return reply.status(403).send('Access Denied')
|
||||
// File access requires at least view (not traverse)
|
||||
const accessLevel = await StoragePermissionService.getAccessLevel(app.db, resolved.folder.id, request.user.id)
|
||||
if (!accessLevel || accessLevel === 'traverse') return reply.status(403).send('Access Denied')
|
||||
|
||||
resources.push({
|
||||
href: `${WEBDAV_PREFIX}${resourcePath}`,
|
||||
@@ -159,8 +161,8 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
|
||||
const resolved = await WebDavService.resolvePath(app.db, resourcePath)
|
||||
|
||||
if (resolved.type === 'file' && resolved.file && resolved.folder) {
|
||||
const hasAccess = await StoragePermissionService.canAccess(app.db, resolved.folder.id, request.user.id)
|
||||
if (!hasAccess) return reply.status(403).send('Access Denied')
|
||||
const accessLevel = await StoragePermissionService.getAccessLevel(app.db, resolved.folder.id, request.user.id)
|
||||
if (!accessLevel || accessLevel === 'traverse') return reply.status(403).send('Access Denied')
|
||||
|
||||
const data = await app.storage.get(resolved.file.path)
|
||||
return reply
|
||||
|
||||
Reference in New Issue
Block a user