Files
alist/internal/op/storage.go
千石 00120cba27 feat: enhance permission control and label management (#9215)
* 标签管理

* pr检查优化

* feat(role): Implement role management functionality

- Add role management routes in `server/router.go` for listing, getting, creating, updating, and deleting roles
- Introduce `initRoles()` in `internal/bootstrap/data/data.go` for initializing roles during bootstrap
- Create `internal/op/role.go` to handle role operations including caching and singleflight
- Implement role handler functions in `server/handles/role.go` for API responses
- Define database operations for roles in `internal/db/role.go`
- Extend `internal/db/db.go` for role model auto-migration
- Design `internal/model/role.go` to represent role structure with ID, name, description, base path, and permissions
- Initialize default roles (`admin` and `guest`) in `internal/bootstrap/data/role.go` during startup

* refactor(user roles): Support multiple roles for users

- Change the `Role` field type from `int` to `[]int` in `drivers/alist_v3/types.go` and `drivers/quqi/types.go`.
- Update the `Role` field in `internal/model/user.go` to use a new `Roles` type with JSON and database support.
- Modify `IsGuest` and `IsAdmin` methods to check for roles using `Contains` method.
- Update `GetUserByRole` method in `internal/db/user.go` to handle multiple roles.
- Add `roles.go` to define a new `Roles` type with JSON marshalling and scanning capabilities.
- Adjust code in `server/handles/user.go` to compare roles with `utils.SliceEqual`.
- Change role initialization for users in `internal/bootstrap/data/dev.go` and `internal/bootstrap/data/user.go`.
- Update `Role` handling in `server/handles/task.go`, `server/handles/ssologin.go`, and `server/handles/ldap_login.go`.

* feat(user/role): Add path limit check for user and role permissions

- Add new permission bit for checking path limits in `user.go`
- Implement `CheckPathLimit` method in `User` struct to validate path access
- Modify `JoinPath` method in `User` to enforce path limit checks
- Update `role.go` to include path limit logic in `Role` struct
- Document new permission bit in `Role` and `User` comments for clarity

* feat(permission): Add role-based permission handling

- Introduce `role_perm.go` for managing user permissions based on roles.
- Implement `HasPermission` and `MergeRolePermissions` functions.
- Update `webdav.go` to utilize role-based permissions instead of direct user checks.
- Modify `fsup.go` to integrate `CanAccessWithRoles` function.
- Refactor `fsread.go` to use `common.HasPermission` for permission validation.
- Adjust `fsmanage.go` for role-based access control checks.
- Enhance `ftp.go` and `sftp.go` to manage FTP access via roles.
- Update `fsbatch.go` to employ `MergeRolePermissions` for batch operations.
- Replace direct user permission checks with role-based permission handling across various modules.

* refactor(user): Replace integer role values with role IDs

- Change `GetAdmin()` and `GetGuest()` functions to retrieve role by name and use role ID.
- Add patch for version `v3.45.2` to convert legacy integer roles to role IDs.
- Update `dev.go` and `user.go` to use role IDs instead of integer values for roles.
- Remove redundant code in `role.go` related to guest role creation.
- Modify `ssologin.go` and `ldap_login.go` to set user roles to nil instead of using integer roles.
- Introduce `convert_roles.go` to handle conversion of legacy roles and ensure role existence in the database.

* feat(role_perm): implement support for multiple base paths for roles

- Modify role permission checks to support multiple base paths
- Update role creation and update functions to handle multiple base paths
- Add migration script to convert old base_path to base_paths
- Define new Paths type for handling multiple paths in the model
- Adjust role model to replace BasePath with BasePaths
- Update existing patches to handle roles with multiple base paths
- Update bootstrap data to reflect the new base_paths field

* feat(role): Restrict modifications to default roles (admin and guest)

- Add validation to prevent changes to "admin" and "guest" roles in `UpdateRole` and `DeleteRole` functions.
- Introduce `ErrChangeDefaultRole` error in `internal/errs/role.go` to standardize error messaging.
- Update role-related API handlers in `server/handles/role.go` to enforce the new restriction.
- Enhance comments in `internal/bootstrap/data/role.go` to clarify the significance of default roles.
- Ensure consistent error responses for unauthorized role modifications across the application.

* 🔄 **refactor(role): Enhance role permission handling**

- Replaced `BasePaths` with `PermissionPaths` in `Role` struct for better permission granularity.
- Introduced JSON serialization for `PermissionPaths` using `RawPermission` field in `Role` struct.
- Implemented `BeforeSave` and `AfterFind` GORM hooks for handling `PermissionPaths` serialization.
- Refactored permission calculation logic in `role_perm.go` to work with `PermissionPaths`.
- Updated role creation logic to initialize `PermissionPaths` for `admin` and `guest` roles.
- Removed deprecated `CheckPathLimit` method from `Role` struct.

* fix(model/user/role): update permission settings for admin and role

- Change `RawPermission` field in `role.go` to hide JSON representation
- Update `Permission` field in `user.go` to `0xFFFF` for full access
- Modify `PermissionScopes` in `role.go` to `0xFFFF` for enhanced permissions

* 🔒 feat(role-permissions): Enhance role-based access control

- Introduce `canReadPathByRole` function in `role_perm.go` to verify path access based on user roles
- Modify `CanAccessWithRoles` to include role-based path read check
- Add `RoleNames` and `Permissions` to `UserResp` struct in `auth.go` for enhanced user role and permission details
- Implement role details aggregation in `auth.go` to populate `RoleNames` and `Permissions`
- Update `User` struct in `user.go` to include `RolesDetail` for more detailed role information
- Enhance middleware in `auth.go` to load and verify detailed role information for users
- Move `guest` user initialization logic in `user.go` to improve code organization and avoid repetition

* 🔒 fix(permissions): Add permission checks for archive operations

- Add `MergeRolePermissions` and `HasPermission` checks to validate user access for reading archives
- Ensure users have `PermReadArchives` before proceeding with `GetNearestMeta` in specific archive paths
- Implement permission checks for decompress operations, requiring `PermDecompress` for source paths
- Return `PermissionDenied` errors with 403 status if user lacks necessary permissions

* 🔒 fix(server): Add permission check for offline download

- Add permission merging logic for user roles
- Check user has permission for offline download addition
- Return error response with "permission denied" if check fails

*  feat(role-permission): Implement path-based role permission checks

- Add `CheckPathLimitWithRoles` function to validate access based on `PermPathLimit` permission.
- Integrate `CheckPathLimitWithRoles` in `offline_download` to enforce path-based access control.
- Apply `CheckPathLimitWithRoles` across file system management operations (e.g., creation, movement, deletion).
- Ensure `CheckPathLimitWithRoles` is invoked for batch operations and archive-related actions.
- Update error handling to return `PermissionDenied` if the path validation fails.
- Import `errs` package in `offline_download` for consistent error responses.

*  feat(role-permission): Implement path-based role permission checks

- Add `CheckPathLimitWithRoles` function to validate access based on `PermPathLimit` permission.
- Integrate `CheckPathLimitWithRoles` in `offline_download` to enforce path-based access control.
- Apply `CheckPathLimitWithRoles` across file system management operations (e.g., creation, movement, deletion).
- Ensure `CheckPathLimitWithRoles` is invoked for batch operations and archive-related actions.
- Update error handling to return `PermissionDenied` if the path validation fails.
- Import `errs` package in `offline_download` for consistent error responses.

* ♻️ refactor(access-control): Update access control logic to use role-based checks

- Remove deprecated logic from `CanAccess` function in `check.go`, replacing it with `CanAccessWithRoles` for improved role-based access control.
- Modify calls in `search.go` to use `CanAccessWithRoles` for more precise handling of permissions.
- Update `fsread.go` to utilize `CanAccessWithRoles`, ensuring accurate access validation based on user roles.
- Simplify import statements in `check.go` by removing unused packages to clean up the codebase.

*  feat(fs): Improve visibility logic for hidden files

- Import `server/common` package to handle permissions more robustly
- Update `whetherHide` function to use `MergeRolePermissions` for user-specific path permissions
- Replace direct user checks with `HasPermission` for `PermSeeHides`
- Enhance logic to ensure `nil` user cases are handled explicitly

* 标签管理

* feat(db/auth/user): Enhance role handling and clean permission paths

- Comment out role modification checks in `server/handles/user.go` to allow flexible role changes.
- Improve permission path handling in `server/handles/auth.go` by normalizing and deduplicating paths.
- Introduce `addedPaths` map in `CurrentUser` to prevent duplicate permissions.

* feat(storage/db): Implement role permissions path prefix update

- Add `UpdateRolePermissionsPathPrefix` function in `role.go` to update role permissions paths.
- Modify `storage.go` to call the new function when the mount path is renamed.
- Introduce path cleaning and prefix matching logic for accurate path updates.
- Ensure roles are updated only if their permission scopes are modified.
- Handle potential errors with informative messages during database operations.

* feat(role-migration): Implement role conversion and introduce NEWGENERAL role

- Add `NEWGENERAL` to the roles enumeration in `user.go`
- Create new file `convert_role.go` for migrating legacy roles to new model
- Implement `ConvertLegacyRoles` function to handle role conversion with permission scopes
- Add `convert_role.go` patch to `all.go` under version `v3.46.0`

* feat(role/auth): Add role retrieval by user ID and update path prefixes

- Add `GetRolesByUserID` function for efficient role retrieval by user ID
- Implement `UpdateUserBasePathPrefix` to update user base paths
- Modify `UpdateRolePermissionsPathPrefix` to return modified role IDs
- Update `auth.go` middleware to use the new role retrieval function
- Refresh role and user caches upon path prefix updates to maintain consistency

---------

Co-authored-by: Leslie-Xy <540049476@qq.com>
2025-07-26 09:51:59 +08:00

380 lines
11 KiB
Go

package op
import (
"context"
"fmt"
"runtime"
"sort"
"strings"
"time"
"github.com/alist-org/alist/v3/internal/db"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/generic_sync"
"github.com/alist-org/alist/v3/pkg/utils"
mapset "github.com/deckarep/golang-set/v2"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
// Although the driver type is stored,
// there is a storage in each driver,
// so it should actually be a storage, just wrapped by the driver
var storagesMap generic_sync.MapOf[string, driver.Driver]
func GetAllStorages() []driver.Driver {
return storagesMap.Values()
}
func HasStorage(mountPath string) bool {
return storagesMap.Has(utils.FixAndCleanPath(mountPath))
}
func GetStorageByMountPath(mountPath string) (driver.Driver, error) {
mountPath = utils.FixAndCleanPath(mountPath)
storageDriver, ok := storagesMap.Load(mountPath)
if !ok {
return nil, errors.Errorf("no mount path for an storage is: %s", mountPath)
}
return storageDriver, nil
}
// CreateStorage Save the storage to database so storage can get an id
// then instantiate corresponding driver and save it in memory
func CreateStorage(ctx context.Context, storage model.Storage) (uint, error) {
storage.Modified = time.Now()
storage.MountPath = utils.FixAndCleanPath(storage.MountPath)
var err error
// check driver first
driverName := storage.Driver
driverNew, err := GetDriver(driverName)
if err != nil {
return 0, errors.WithMessage(err, "failed get driver new")
}
storageDriver := driverNew()
// insert storage to database
err = db.CreateStorage(&storage)
if err != nil {
return storage.ID, errors.WithMessage(err, "failed create storage in database")
}
// already has an id
err = initStorage(ctx, storage, storageDriver)
go callStorageHooks("add", storageDriver)
if err != nil {
return storage.ID, errors.Wrap(err, "failed init storage but storage is already created")
}
log.Debugf("storage %+v is created", storageDriver)
return storage.ID, nil
}
// LoadStorage load exist storage in db to memory
func LoadStorage(ctx context.Context, storage model.Storage) error {
storage.MountPath = utils.FixAndCleanPath(storage.MountPath)
// check driver first
driverName := storage.Driver
driverNew, err := GetDriver(driverName)
if err != nil {
return errors.WithMessage(err, "failed get driver new")
}
storageDriver := driverNew()
err = initStorage(ctx, storage, storageDriver)
go callStorageHooks("add", storageDriver)
log.Debugf("storage %+v is created", storageDriver)
return err
}
func getCurrentGoroutineStack() string {
buf := make([]byte, 1<<16)
n := runtime.Stack(buf, false)
return string(buf[:n])
}
// initStorage initialize the driver and store to storagesMap
func initStorage(ctx context.Context, storage model.Storage, storageDriver driver.Driver) (err error) {
storageDriver.SetStorage(storage)
driverStorage := storageDriver.GetStorage()
defer func() {
if err := recover(); err != nil {
errInfo := fmt.Sprintf("[panic] err: %v\nstack: %s\n", err, getCurrentGoroutineStack())
log.Errorf("panic init storage: %s", errInfo)
driverStorage.SetStatus(errInfo)
MustSaveDriverStorage(storageDriver)
storagesMap.Store(driverStorage.MountPath, storageDriver)
}
}()
// Unmarshal Addition
err = utils.Json.UnmarshalFromString(driverStorage.Addition, storageDriver.GetAddition())
if err == nil {
if ref, ok := storageDriver.(driver.Reference); ok {
if strings.HasPrefix(driverStorage.Remark, "ref:/") {
refMountPath := driverStorage.Remark
i := strings.Index(refMountPath, "\n")
if i > 0 {
refMountPath = refMountPath[4:i]
} else {
refMountPath = refMountPath[4:]
}
var refStorage driver.Driver
refStorage, err = GetStorageByMountPath(refMountPath)
if err != nil {
err = fmt.Errorf("ref: %w", err)
} else {
err = ref.InitReference(refStorage)
if err != nil && errs.IsNotSupportError(err) {
err = fmt.Errorf("ref: storage is not %s", storageDriver.Config().Name)
}
}
}
}
}
if err == nil {
err = storageDriver.Init(ctx)
}
storagesMap.Store(driverStorage.MountPath, storageDriver)
if err != nil {
driverStorage.SetStatus(err.Error())
err = errors.Wrap(err, "failed init storage")
} else {
driverStorage.SetStatus(WORK)
}
MustSaveDriverStorage(storageDriver)
return err
}
func EnableStorage(ctx context.Context, id uint) error {
storage, err := db.GetStorageById(id)
if err != nil {
return errors.WithMessage(err, "failed get storage")
}
if !storage.Disabled {
return errors.Errorf("this storage have enabled")
}
storage.Disabled = false
err = db.UpdateStorage(storage)
if err != nil {
return errors.WithMessage(err, "failed update storage in db")
}
err = LoadStorage(ctx, *storage)
if err != nil {
return errors.WithMessage(err, "failed load storage")
}
return nil
}
func DisableStorage(ctx context.Context, id uint) error {
storage, err := db.GetStorageById(id)
if err != nil {
return errors.WithMessage(err, "failed get storage")
}
if storage.Disabled {
return errors.Errorf("this storage have disabled")
}
storageDriver, err := GetStorageByMountPath(storage.MountPath)
if err != nil {
return errors.WithMessage(err, "failed get storage driver")
}
// drop the storage in the driver
if err := storageDriver.Drop(ctx); err != nil {
return errors.Wrap(err, "failed drop storage")
}
// delete the storage in the memory
storage.Disabled = true
storage.SetStatus(DISABLED)
err = db.UpdateStorage(storage)
if err != nil {
return errors.WithMessage(err, "failed update storage in db")
}
storagesMap.Delete(storage.MountPath)
go callStorageHooks("del", storageDriver)
return nil
}
// UpdateStorage update storage
// get old storage first
// drop the storage then reinitialize
func UpdateStorage(ctx context.Context, storage model.Storage) error {
oldStorage, err := db.GetStorageById(storage.ID)
if err != nil {
return errors.WithMessage(err, "failed get old storage")
}
if oldStorage.Driver != storage.Driver {
return errors.Errorf("driver cannot be changed")
}
storage.Modified = time.Now()
storage.MountPath = utils.FixAndCleanPath(storage.MountPath)
err = db.UpdateStorage(&storage)
if err != nil {
return errors.WithMessage(err, "failed update storage in database")
}
if storage.Disabled {
return nil
}
storageDriver, err := GetStorageByMountPath(oldStorage.MountPath)
if oldStorage.MountPath != storage.MountPath {
// mount path renamed, need to drop the storage
storagesMap.Delete(oldStorage.MountPath)
modifiedRoleIDs, err := db.UpdateRolePermissionsPathPrefix(oldStorage.MountPath, storage.MountPath)
if err != nil {
return errors.WithMessage(err, "failed to update role permissions")
}
for _, id := range modifiedRoleIDs {
roleCache.Del(fmt.Sprint(id))
}
modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldStorage.MountPath, storage.MountPath)
if err != nil {
return errors.WithMessage(err, "failed to update user base path")
}
for _, name := range modifiedUsernames {
userCache.Del(name)
}
}
if err != nil {
return errors.WithMessage(err, "failed get storage driver")
}
err = storageDriver.Drop(ctx)
if err != nil {
return errors.Wrapf(err, "failed drop storage")
}
err = initStorage(ctx, storage, storageDriver)
go callStorageHooks("update", storageDriver)
log.Debugf("storage %+v is update", storageDriver)
return err
}
func DeleteStorageById(ctx context.Context, id uint) error {
storage, err := db.GetStorageById(id)
if err != nil {
return errors.WithMessage(err, "failed get storage")
}
if !storage.Disabled {
storageDriver, err := GetStorageByMountPath(storage.MountPath)
if err != nil {
return errors.WithMessage(err, "failed get storage driver")
}
// drop the storage in the driver
if err := storageDriver.Drop(ctx); err != nil {
return errors.Wrapf(err, "failed drop storage")
}
// delete the storage in the memory
storagesMap.Delete(storage.MountPath)
go callStorageHooks("del", storageDriver)
}
// delete the storage in the database
if err := db.DeleteStorageById(id); err != nil {
return errors.WithMessage(err, "failed delete storage in database")
}
return nil
}
// MustSaveDriverStorage call from specific driver
func MustSaveDriverStorage(driver driver.Driver) {
err := saveDriverStorage(driver)
if err != nil {
log.Errorf("failed save driver storage: %s", err)
}
}
func saveDriverStorage(driver driver.Driver) error {
storage := driver.GetStorage()
addition := driver.GetAddition()
str, err := utils.Json.MarshalToString(addition)
if err != nil {
return errors.Wrap(err, "error while marshal addition")
}
storage.Addition = str
err = db.UpdateStorage(storage)
if err != nil {
return errors.WithMessage(err, "failed update storage in database")
}
return nil
}
// getStoragesByPath get storage by longest match path, contains balance storage.
// for example, there is /a/b,/a/c,/a/d/e,/a/d/e.balance
// getStoragesByPath(/a/d/e/f) => /a/d/e,/a/d/e.balance
func getStoragesByPath(path string) []driver.Driver {
storages := make([]driver.Driver, 0)
curSlashCount := 0
storagesMap.Range(func(mountPath string, value driver.Driver) bool {
mountPath = utils.GetActualMountPath(mountPath)
// is this path
if utils.IsSubPath(mountPath, path) {
slashCount := strings.Count(utils.PathAddSeparatorSuffix(mountPath), "/")
// not the longest match
if slashCount > curSlashCount {
storages = storages[:0]
curSlashCount = slashCount
}
if slashCount == curSlashCount {
storages = append(storages, value)
}
}
return true
})
// make sure the order is the same for same input
sort.Slice(storages, func(i, j int) bool {
return storages[i].GetStorage().MountPath < storages[j].GetStorage().MountPath
})
return storages
}
// GetStorageVirtualFilesByPath Obtain the virtual file generated by the storage according to the path
// for example, there are: /a/b,/a/c,/a/d/e,/a/b.balance1,/av
// GetStorageVirtualFilesByPath(/a) => b,c,d
func GetStorageVirtualFilesByPath(prefix string) []model.Obj {
files := make([]model.Obj, 0)
storages := storagesMap.Values()
sort.Slice(storages, func(i, j int) bool {
if storages[i].GetStorage().Order == storages[j].GetStorage().Order {
return storages[i].GetStorage().MountPath < storages[j].GetStorage().MountPath
}
return storages[i].GetStorage().Order < storages[j].GetStorage().Order
})
prefix = utils.FixAndCleanPath(prefix)
set := mapset.NewSet[string]()
for _, v := range storages {
mountPath := utils.GetActualMountPath(v.GetStorage().MountPath)
// Exclude prefix itself and non prefix
if len(prefix) >= len(mountPath) || !utils.IsSubPath(prefix, mountPath) {
continue
}
name := strings.SplitN(strings.TrimPrefix(mountPath[len(prefix):], "/"), "/", 2)[0]
if set.Add(name) {
files = append(files, &model.Object{
Name: name,
Size: 0,
Modified: v.GetStorage().Modified,
IsFolder: true,
})
}
}
return files
}
var balanceMap generic_sync.MapOf[string, int]
// GetBalancedStorage get storage by path
func GetBalancedStorage(path string) driver.Driver {
path = utils.FixAndCleanPath(path)
storages := getStoragesByPath(path)
storageNum := len(storages)
switch storageNum {
case 0:
return nil
case 1:
return storages[0]
default:
virtualPath := utils.GetActualMountPath(storages[0].GetStorage().MountPath)
i, _ := balanceMap.LoadOrStore(virtualPath, 0)
i = (i + 1) % storageNum
balanceMap.Store(virtualPath, i)
return storages[i]
}
}