Skip to content

feat(permissions): implement fine-grained permission control#2145

Open
Lanfei wants to merge 7 commits intoOpenListTeam:mainfrom
Lanfei:feat/fine-grained-permissions
Open

feat(permissions): implement fine-grained permission control#2145
Lanfei wants to merge 7 commits intoOpenListTeam:mainfrom
Lanfei:feat/fine-grained-permissions

Conversation

@Lanfei
Copy link
Contributor

@Lanfei Lanfei commented Feb 18, 2026

Description / 描述

This PR implements a fine-grained, per-user read/write permission system at the meta level, allowing administrators to restrict specific users from accessing or modifying
certain paths. It also refactors existing permission check functions for clarity and adds comprehensive test coverage.

本 PR 在 meta
层面实现了细粒度的用户读/写权限系统,允许管理员限制特定用户对某些路径的访问或修改权限。同时对现有权限检查函数进行了重命名重构以提升可读性,并添加了完整的测试覆盖。

Key changes / 主要变更:

  • Add ReadUsers/WriteUsers fields to the Meta model with sub-directory inheritance flags
    Meta 模型中新增 ReadUsers/WriteUsers 字段,支持子目录继承标志
  • Implement CanRead and CanWrite permission check functions in server/common
    server/common 中实现 CanReadCanWrite 权限检查函数
  • Filter file list results based on user read permissions
    根据用户读权限对文件列表结果进行过滤
  • Add permission checks across all file operations (FTP, HTTP handlers, WebDAV)
    在所有文件操作(FTP、HTTP 处理器、WebDAV)中添加权限检查
  • Rename functions for clarity: User.CanWrite()User.CanCreateFilesOrFolders(), common.CanWrite()common.CanWriteContentBypassUserPerms(), common.IsApply()
    common.MetaCoversPath()
    重命名函数以提升可读性:User.CanWrite()User.CanCreateFilesOrFolders()common.CanWrite()common.CanWriteContentBypassUserPerms()common.IsApply()
    common.MetaCoversPath()

Note / 注意: Batch and recursive operations (FsMove, FsCopy, FsRemove, FsRecursiveMove, FsBatchRename, FsRegexRename) currently check parent directory
permissions only. Individual item permission checks are not performed for performance reasons.
批量与递归操作(FsMoveFsCopyFsRemoveFsRecursiveMoveFsBatchRenameFsRegexRename)当前仅检查父目录权限,出于性能考量,不对单个子项逐一检查权限。

Motivation and Context / 背景

The existing permission system only supports coarse-grained flags (e.g., global write permission). There was no way to restrict specific users from reading or writing to
particular paths without affecting all users. This PR addresses that gap by introducing a per-user allowlist at the meta level.

现有权限系统仅支持粗粒度的标志位(如全局写权限),无法在不影响其他用户的情况下限制特定用户对特定路径的读写访问。本 PR 通过在 meta 层面引入基于用户的白名单机制来填补这一空白。

Relates to #1328
Closes #1257
Closes #1267
Closes #346
Closes #1753

How Has This Been Tested? / 测试

  • Added TestCanRead, TestCanWrite, TestCanAccessWithReadPermissions, and TestWritePermissionCombinations to validate the three-layer permission system
    新增 TestCanReadTestCanWriteTestCanAccessWithReadPermissionsTestWritePermissionCombinations,验证三层权限体系
  • Test scenarios cover: nil user/meta, sub-path inheritance, user whitelists, and root-level restrictions
    测试场景覆盖:nil 用户/meta、子路径继承、用户白名单及根路径限制
  • Added 43 test scenarios for MetaCoversPath, CanWriteContentBypassUserPerms, getReadme, getHeader, isEncrypt, and whetherHide
    MetaCoversPathCanWriteContentBypassUserPermsgetReadmegetHeaderisEncryptwhetherHide 新增 43 个测试场景
  • Added list_test.go and fsread_test.go for coverage of list filtering and read handler logic
    新增 list_test.gofsread_test.go,覆盖列表过滤与读取处理器逻辑

Checklist / 检查清单

@Lanfei
Copy link
Contributor Author

Lanfei commented Mar 13, 2026

Hi @xrgzs @PIKACHUIM, this PR has been open for about 3 weeks now. CI is all green and there are no merge conflicts. The companion frontend PR (OpenList-Frontend#386) and docs PR (OpenList-Docs#310) are also ready. Could you help schedule a review? Happy to make any adjustments based on your feedback. Thanks!

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces meta-level, per-user read/write allowlists and wires the new permission checks through core file operations (HTTP handlers, FTP, WebDAV), including filtering list results by read permission and expanding automated test coverage for the new permission logic.

Changes:

  • Extend Meta with ReadUsers/WriteUsers (+ sub-path inheritance flags) and add common.CanRead / common.CanWrite checks.
  • Apply permission checks across file operations (list/read/write/manage) including WebDAV/FTP and list filtering.
  • Add/expand tests for meta path coverage and permission combinations.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
internal/model/meta.go Adds per-user read/write allowlists to Meta (JSON-serialized).
server/common/check.go Introduces CanRead/CanWrite, refactors path coverage helper(s).
internal/fs/list.go Filters directory listings based on per-user read permission.
server/handles/fsread.go Adjusts list response fields and write/tool availability logic.
server/handles/fsmanage.go Adds parent-meta permission checks to manage operations.
server/webdav.go Updates WebDAV auth/middleware context values for permission checks.
server/webdav/webdav.go Adds CanAccess/CanWrite enforcement for WebDAV operations.
server/webdav/file.go Adds permission gates to WebDAV copy/move helpers.
server/middlewares/fsup.go Refactors upload middleware permission checks.
server/ftp/fsup.go / server/ftp/fsmanage.go / server/ftp/fsread.go Adds/adjusts FTP permission checks using new helpers.
server/handles/archive.go / server/handles/offline_download.go Adds meta permission checks for archive/decompress and offline download creation.
server/common/check_test.go / internal/fs/list_test.go / server/handles/fsread_test.go Adds/expands tests for the new permission model and helpers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Lanfei and others added 7 commits March 17, 2026 15:11
- User.CanWrite() → User.CanCreateFilesOrFolders()
- common.CanWrite() → common.CanWriteContentBypassUserPerms()
- common.IsApply() → common.MetaCoversPath()

Improves code readability by making function names more descriptive.
The new MetaCoversPath name clearly indicates it checks if a meta rule
covers a specific path. It better conveys that it's a query function
rather than an action, and the applyToSubFolder parameter is more
explicit than applySub.

Also adds comprehensive test coverage:
- 10 tests for MetaCoversPath core logic
- 6 tests for CanWriteContent
UserPerms
- 7 tests for getReadme
- 5 tests for getHeader
- 6 tests for isEncrypt
- 9 tests for whetherHide

Total: 43 test scenarios covering all path matching and permission
inheritance logic. Tests verify both normal behavior and bug fixes
for Readme/Header information leakage and write permission bypass.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ite operations

Add per-user read and write permission controls at the meta level to enable
more granular access control beyond the existing permission flags.

Key changes:
- Add ReadUsers/WriteUsers fields to Meta model with sub-directory inheritance flags
- Implement CanRead and CanWrite permission check functions in server/common
- Filter file list results based on user read permissions
- Add permission checks across all file operations (FTP, HTTP handlers, WebDAV)
- Simplify error handling pattern for MetaNotFound errors throughout codebase

This allows administrators to restrict specific users from accessing or modifying
certain paths, providing finer control over file system permissions.

Note: Batch and recursive operations (FsMove, FsCopy, FsRemove, FsRecursiveMove,
FsBatchRename, FsRegexRename) currently check parent directory permissions only.
Individual item permission checks are not performed for performance reasons.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…combined permission checks

Add TestCanRead, TestCanWrite, TestCanAccessWithReadPermissions, and
TestWritePermissionCombinations to validate the three-layer permission
system including nil user/meta, sub-path inheritance, user whitelists,
and root-level restrictions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bearer-token and OPTIONS auth paths do not set MetaPassKey in context,
causing a panic when handlers perform a forced type assertion on nil.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously, CanRead/CanWrite returned false for nil user, causing
filterReadableObjs to return an empty list when fs.List is called from
internal contexts without a user (e.g. context.Background()). A nil user
represents an internal/system call and should bypass per-user restrictions,
consistent with how whetherHide already handles nil user.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous check only skipped names that resolved to "/", but did not
prevent traversal to sibling directories (e.g. "../secret"), which could
bypass the CanWrite permission check that is only applied to req.Dir.

Replace with a post-join prefix check to ensure each resolved path stays
within reqPath.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
For guest users, the WebDAV password input serves as the meta folder
password (consistent with FTP anonymous/guest handling). For authenticated
users, MetaPassKey is set to empty string since their login password is
not the meta folder password.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Lanfei Lanfei force-pushed the feat/fine-grained-permissions branch from 9dc7cf7 to 814e995 Compare March 17, 2026 07:11
@Lanfei
Copy link
Contributor Author

Lanfei commented Mar 17, 2026

@xrgzs @PIKACHUIM All Copilot review comments have been addressed. Could you please take a look when you have a chance? Thanks!

@xrgzs
Copy link
Member

xrgzs commented Mar 17, 2026

@Lanfei Can it smoothly migrate from the old version to the new version? If not, please refine the migration code.

@Lanfei
Copy link
Contributor Author

Lanfei commented Mar 17, 2026

@xrgzs Yes, migration from older versions is smooth — no manual migration patch is needed.

GORM's AutoMigrate (called at startup in internal/db/db.go) will automatically ADD COLUMN for the four new fields on the metas table: read_users, read_users_sub, write_users, write_users_sub.

For existing rows, read_users and write_users will be NULL, which GORM's JSON serializer deserializes as a nil slice. Since CanRead/CanWrite only restrict access when len(ReadUsers) > 0 / len(WriteUsers) > 0, existing metas with no value set will behave exactly as before — no access restrictions are added. The two new bool columns default to false, which is also the safe/permissive default.

In short: upgrading applies additive schema changes only, and the zero-value semantics preserve backward-compatible behavior for all existing data.

@liliang1112
Copy link

liliang1112 commented Mar 20, 2026

@Lanfei @xrgzs @PIKACHUIM Could you please take a look at the approval when you have time? This feature is very important to me. Thank you!

@Lanfei
Copy link
Contributor Author

Lanfei commented Mar 22, 2026

@Lanfei @xrgzs @PIKACHUIM Could you please take a look at the approval when you have time? This feature is very important to me. Thank you!

Yeah, I am waiting for the approval as well :P

@liliang1112
Copy link

@xrgzs @PIKACHUIM I see that the author has already solved the above issues. Could you please help arrange for a review. Thank you!

@PIKACHUIM
Copy link
Member

@liliang1112 @Lanfei Thank you for your contributions.

We are deeply moved that you are implementing the features we have planned.
However, the team members are currently quite busy, so we may need some time to test these features.

At first glance, this feature appears to be a major update / breaking change.
If possible, we would like to merge this PR into the 5.0 version branch (Next).

We previously designed a simple plan for reference:
Since this plan is still in its early stages, you are not required to follow the structure below.

OpenList File System Architecture

File Configuration

graph LR
A[Permission Config]
    A --> B(Permission Group A)
    A --> C(Permission Group B)
    A --> D(.......)
    B --> B1(Encrypt Content: bool)
    B --> B2(Encrypt Name: bool)
    B --> B3(Hide Directory: bool)
    B --> B4(Compress File: bool)
    B --> B5(Split Volume: bool)
    B --> B6(File Permission: 10bits)
    B --> B7(File Owner: number)
    B --> B8(Volume Size: number)
    B --> B9(Access Password: string)
    B --> BA(Encryption Password: string)
Loading

Encrypted paths must not overlap; traversal scan is required when adding new paths
If file content encryption is enabled, a password must be provided; filename-only encryption does not require a password
A default permission is needed: default is no encryption and public access, but it can be modified

File Configuration Page

graph LR
A[Path Config]
    A --> B(/path1/) --> B1(Permission Group A)
    A --> C(/path2/) --> C1(Permission Group A)
    A --> D(/path3/pic.jpg) --> D1(Permission Group B)
    A --> E(/path4/file.mp4) --> E1(Permission Group B)
    A --> F(...............)
Loading

Encryption Execution Flow

graph LR
A(Upload /path2/file.mp4)
    A --> B1(Match /path1/) --> C1(Miss)
    A --> B2(Match /path2/) --> C2(Hit) --> D1(Execute Encryption)
    F(file.mp4) --> G1(Encrypt File) --> J1(file.mp4.u8BMxMpD.enc)
    F(file.mp4) --> G2(Encrypt Name) --> J2(ZmlsZS5tcDQ=.b64)
    F(file.mp4) --> G3(Compress & Split) --> J3(file.mp4.f001.zip)
    X1[ZmlsZS5tcDQ=.b64] -->X4[.zip] -->X2[.f001] -->X3[.u8BMxMpD.enc] 
    Y1[Encrypted Filename] --> Y4[Compression] --> Y2[Split Volume] --> Y3[Encryption] 
Loading

Compression + Encryption suffix: ZEC, Encryption only: ENC, Compression only: ZIP
PS: If no rules match, the file is uploaded as-is without encryption or compression.

File Access Flow

graph LR
A1(/path2/file.mp4.enc)
    A1 --> B1(Suffix is ENC) --> C1(Display as file.mp4)
    B1 --> B11(Match /path1/) --> C11(Miss)
    B1 --> B12(Match /path2/) --> C12(Hit) --> D11(Decrypt)
    
A2(/path2/ZmlsZS5tcDQ=.b64.u8BMxMpD.enc)
    A2 --> B2(Suffix is ENC) --> C2(Decrypt Filename) -->D2(Display as file.mp4)
    B2 --> B21(Match /path1/) --> C21(Miss)
    B2 --> B22(Match /path2/) --> C22(Hit) --> D21(Decrypt)
Loading

PS: If no rules match and the file is encrypted, a password must be entered.

@PIKACHUIM PIKACHUIM self-requested a review March 23, 2026 03:37
@PIKACHUIM
Copy link
Member

PIKACHUIM commented Mar 23, 2026

Regarding the file system architecture design provided in our previous reply, we would like to add the following:
We aim to build a unified file system permission and configuration management system.
Therefore, we envision the overall structure as follows:

a. Different default read/write permissions can be set for individual users
b. Different default permissions can be configured for different directories
Thus, we hope you can review the design structure we submitted, but there is no need to strictly follow our design, as we have not yet reached a unified and definitive conclusion internally. If possible, we would like to discuss the feature planning with you.

[文件系统Opfs Layout]

graph LR
A[文件系统Opfs Layout]
    A --> B(网盘驱动Driver)
    B --> B1[驱动A]
    B --> B2[驱动B]
Loading

[网盘驱动File Driver]

graph LR
A[网盘驱动File Driver]
     A --> B(配置信息Config)
     A --> C(单个文件Module)
     C --> C1[文件A]
     C --> C2[文件B]
Loading

[单个文件File Module]

graph LR
A[单个文件File Module]
    A --> B(文件路径Path: string)
    A --> C(...........)
    A --> D(文件权限Mask(8进制): number)
    A --> E(所有用户User: string)
Loading

[File Mask]

graph LR
A[文件权限FileMask 16B]
    A --> A0(0000类属性) -->B0(加密文件,加密名称,压缩文件,保留标识)
    A --> A1(0000所有者) -->B1(允许下载,允许写入,允许删除,保留标识)
    A --> A2(0000用户组) -->B2(允许下载,允许写入,允许删除,保留标识)
    A --> A3(0000其他人) -->B3(允许下载,允许写入,允许删除,保留标识)
Loading

@PIKACHUIM PIKACHUIM requested a review from jyxjjj March 23, 2026 03:49
@Lanfei
Copy link
Contributor Author

Lanfei commented Mar 23, 2026

@PIKACHUIM Thank you for the detailed response. I’m really excited to hear about the 5.0 direction.

Given the current situation, I’d like to suggest a conditional approach:

  • If 5.0 introduces a fully new file/permission architecture and the release is still some time away, we could merge this PR first. It is a solid and practical solution built on the current system, and fine-grained permission control is already widely requested by users (while still respecting maintainers’ bandwidth).
  • If the 5.0 architecture is close to the current model and the release plan is already relatively clear, then I’m happy to retarget this work to the 5.0 branch instead.

What do you think?

@jyxjjj
Copy link
Member

jyxjjj commented Mar 23, 2026

Sounds good—merge now, redesign later on the 5.0 branch.

Copy link
Member

@PIKACHUIM PIKACHUIM left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code is fine, but rigorous testing is necessary.

@liliang1112
Copy link

@PIKACHUIM @xrgzs Thank you for your detailed reply. Could you please arrange for a merger

@jyxjjj
Copy link
Member

jyxjjj commented Mar 23, 2026

Pika may not be in a position to conduct a review right now.
And KirCute and I have been quite busy recently, so we unable to thoroughly review changes of this scale right now.
Given the scope of the changes, it would be helpful if the submitter could break them down more carefully into smaller, more reviewable pieces.
That would make it much easier for us to review them properly.
I took a quick look, but not very carefully, and didn’t really find any issues.

Comment on lines -97 to 103
if !user.CanWrite() && !common.CanWrite(meta, reqPath) && req.Refresh {
common.ErrorStrResp(c, "Refresh without permission", 403)
return
}
objs, err := fs.List(c.Request.Context(), reqPath, &fs.ListArgs{
Refresh: req.Refresh,
WithStorageDetails: !user.IsGuest() && !setting.GetBool(conf.HideStorageDetails),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guests can refresh the cache without authorization. Although the refresh icon existed on the frontend before, it previously required a password when clicked. But I consider this a bug rather than a feature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review.

I’m not sure Write is the right boundary for refresh. Refresh is closer to list/read behavior, and there is a non-obvious case here: a user may not have write permission but may still be allowed to copy/move/rename/delete in some combinations. If refresh is blocked by write, the list can remain stale after those operations, which feels unexpected.

Do you have a suggested approach here? Happy to follow your preference.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We either preserve the previous state or directly granularize the permissions. The latter would also help solve many of the issues that 5.0 will have to address.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Lanfei

Regarding cache issues, different users hold differing opinions. Some users believe the cache should be refreshable by guests so they can view the latest file changes. Others argue that allowing regular users to refresh the cache is insecure, and only admin should have this permission.

I suppose we should revert it to the previous state. Only users with write permissions could refresh the cache.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We either preserve the previous state or directly granularize the permissions. The latter would also help solve many of the issues that 5.0 will have to address.

In OpenList v5.0, I think the situation will become more complex. Considering that features such as audio/video transcoding and media/doc library will be added, user file permissions will grow more complicated, making it necessary to implement atomic permission control.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Module: User User and authentication related issue/PR

Projects

None yet

6 participants