mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-09 16:25:15 +03:00
add per directory permissions
we can now have permissions such as these ones
{"/":["*"],"/somedir":["list","download"]}
The old permissions are automatically converted to the new structure,
no database migration is needed
This commit is contained in:
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
databaseVersion = 2
|
||||
databaseVersion = 3
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -33,6 +33,28 @@ type boltDatabaseVersion struct {
|
||||
Version int
|
||||
}
|
||||
|
||||
type compatUserV2 struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password,omitempty"`
|
||||
PublicKeys []string `json:"public_keys,omitempty"`
|
||||
HomeDir string `json:"home_dir"`
|
||||
UID int `json:"uid"`
|
||||
GID int `json:"gid"`
|
||||
MaxSessions int `json:"max_sessions"`
|
||||
QuotaSize int64 `json:"quota_size"`
|
||||
QuotaFiles int `json:"quota_files"`
|
||||
Permissions []string `json:"permissions"`
|
||||
UsedQuotaSize int64 `json:"used_quota_size"`
|
||||
UsedQuotaFiles int `json:"used_quota_files"`
|
||||
LastQuotaUpdate int64 `json:"last_quota_update"`
|
||||
UploadBandwidth int64 `json:"upload_bandwidth"`
|
||||
DownloadBandwidth int64 `json:"download_bandwidth"`
|
||||
ExpirationDate int64 `json:"expiration_date"`
|
||||
LastLogin int64 `json:"last_login"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
func initializeBoltProvider(basePath string) error {
|
||||
var err error
|
||||
logSender = BoltDataProviderName
|
||||
@@ -376,27 +398,91 @@ func checkBoltDatabaseVersion(dbHandle *bolt.DB) error {
|
||||
return nil
|
||||
}
|
||||
if dbVersion.Version == 1 {
|
||||
providerLog(logger.LevelInfo, "update bolt database version: 1 -> 2")
|
||||
usernames, err := getBoltAvailableUsernames(dbHandle)
|
||||
err = updateDatabaseFrom1To2(dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, u := range usernames {
|
||||
user, err := provider.userExists(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Status = 1
|
||||
err = provider.updateUser(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
providerLog(logger.LevelInfo, "user %#v updated, \"status\" setted to 1", user.Username)
|
||||
}
|
||||
return updateBoltDatabaseVersion(dbHandle, 2)
|
||||
return updateDatabaseFrom2To3(dbHandle)
|
||||
} else if dbVersion.Version == 2 {
|
||||
return updateDatabaseFrom2To3(dbHandle)
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateDatabaseFrom1To2(dbHandle *bolt.DB) error {
|
||||
providerLog(logger.LevelInfo, "updating bolt database version: 1 -> 2")
|
||||
usernames, err := getBoltAvailableUsernames(dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, u := range usernames {
|
||||
user, err := provider.userExists(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Status = 1
|
||||
err = provider.updateUser(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
providerLog(logger.LevelInfo, "user %#v updated, \"status\" setted to 1", user.Username)
|
||||
}
|
||||
return updateBoltDatabaseVersion(dbHandle, 2)
|
||||
}
|
||||
|
||||
func updateDatabaseFrom2To3(dbHandle *bolt.DB) error {
|
||||
providerLog(logger.LevelInfo, "updating bolt database version: 2 -> 3")
|
||||
users := []User{}
|
||||
err := dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket, _, err := getBuckets(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var compatUser compatUserV2
|
||||
err = json.Unmarshal(v, &compatUser)
|
||||
if err == nil {
|
||||
user := User{}
|
||||
user.ID = compatUser.ID
|
||||
user.Username = compatUser.Username
|
||||
user.Password = compatUser.Password
|
||||
user.PublicKeys = compatUser.PublicKeys
|
||||
user.HomeDir = compatUser.HomeDir
|
||||
user.UID = compatUser.UID
|
||||
user.GID = compatUser.GID
|
||||
user.MaxSessions = compatUser.MaxSessions
|
||||
user.QuotaSize = compatUser.QuotaSize
|
||||
user.QuotaFiles = compatUser.QuotaFiles
|
||||
user.Permissions = make(map[string][]string)
|
||||
user.Permissions["/"] = compatUser.Permissions
|
||||
user.UsedQuotaSize = compatUser.UsedQuotaSize
|
||||
user.UsedQuotaFiles = compatUser.UsedQuotaFiles
|
||||
user.LastQuotaUpdate = compatUser.LastQuotaUpdate
|
||||
user.UploadBandwidth = compatUser.UploadBandwidth
|
||||
user.DownloadBandwidth = compatUser.DownloadBandwidth
|
||||
user.ExpirationDate = compatUser.ExpirationDate
|
||||
user.LastLogin = compatUser.LastLogin
|
||||
user.Status = compatUser.Status
|
||||
users = append(users, user)
|
||||
}
|
||||
}
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
err = provider.updateUser(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
providerLog(logger.LevelInfo, "user %#v updated, \"permissions\" setted to %+v", user.Username, user.Permissions)
|
||||
}
|
||||
|
||||
return updateBoltDatabaseVersion(dbHandle, 3)
|
||||
}
|
||||
|
||||
func getBoltAvailableUsernames(dbHandle *bolt.DB) ([]string, error) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -343,14 +344,33 @@ func buildUserHomeDir(user *User) {
|
||||
}
|
||||
|
||||
func validatePermissions(user *User) error {
|
||||
for _, p := range user.Permissions {
|
||||
if !utils.IsStringInSlice(p, ValidPerms) {
|
||||
return &ValidationError{err: fmt.Sprintf("Invalid permission: %v", p)}
|
||||
permissions := make(map[string][]string)
|
||||
if _, ok := user.Permissions["/"]; !ok {
|
||||
return &ValidationError{err: fmt.Sprintf("Permissions for the root dir \"/\" must be set")}
|
||||
}
|
||||
for dir, perms := range user.Permissions {
|
||||
if len(perms) == 0 {
|
||||
return &ValidationError{err: fmt.Sprintf("No permissions granted for the directory: %#v", dir)}
|
||||
}
|
||||
for _, p := range perms {
|
||||
if !utils.IsStringInSlice(p, ValidPerms) {
|
||||
return &ValidationError{err: fmt.Sprintf("Invalid permission: %#v", p)}
|
||||
}
|
||||
}
|
||||
cleanedDir := filepath.ToSlash(path.Clean(dir))
|
||||
if cleanedDir != "/" {
|
||||
cleanedDir = strings.TrimSuffix(cleanedDir, "/")
|
||||
}
|
||||
if !path.IsAbs(cleanedDir) {
|
||||
return &ValidationError{err: fmt.Sprintf("Cannot set permissions for non absolute path: %#v", dir)}
|
||||
}
|
||||
if utils.IsStringInSlice(PermAny, perms) {
|
||||
permissions[cleanedDir] = []string{PermAny}
|
||||
} else {
|
||||
permissions[cleanedDir] = perms
|
||||
}
|
||||
}
|
||||
if utils.IsStringInSlice(PermAny, user.Permissions) {
|
||||
user.Permissions = []string{PermAny}
|
||||
}
|
||||
user.Permissions = permissions
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -265,10 +265,18 @@ func getUserFromDbRow(row *sql.Row, rows *sql.Rows) (User, error) {
|
||||
}
|
||||
}
|
||||
if permissions.Valid {
|
||||
var list []string
|
||||
err = json.Unmarshal([]byte(permissions.String), &list)
|
||||
perms := make(map[string][]string)
|
||||
err = json.Unmarshal([]byte(permissions.String), &perms)
|
||||
if err == nil {
|
||||
user.Permissions = list
|
||||
user.Permissions = perms
|
||||
} else {
|
||||
// compatibility layer: until version 0.9.4 permissions were a string list
|
||||
var list []string
|
||||
err = json.Unmarshal([]byte(permissions.String), &list)
|
||||
if err == nil {
|
||||
perms["/"] = list
|
||||
user.Permissions = perms
|
||||
}
|
||||
}
|
||||
}
|
||||
return user, err
|
||||
|
||||
@@ -3,8 +3,10 @@ package dataprovider
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
)
|
||||
@@ -68,7 +70,7 @@ type User struct {
|
||||
// Maximum number of files allowed. 0 means unlimited
|
||||
QuotaFiles int `json:"quota_files"`
|
||||
// List of the granted permissions
|
||||
Permissions []string `json:"permissions"`
|
||||
Permissions map[string][]string `json:"permissions"`
|
||||
// Used quota as bytes
|
||||
UsedQuotaSize int64 `json:"used_quota_size"`
|
||||
// Used quota as number of files
|
||||
@@ -83,21 +85,59 @@ type User struct {
|
||||
LastLogin int64 `json:"last_login"`
|
||||
}
|
||||
|
||||
// GetPermissionsForPath returns the permissions for the given path
|
||||
func (u *User) GetPermissionsForPath(p string) []string {
|
||||
permissions := []string{}
|
||||
if perms, ok := u.Permissions["/"]; ok {
|
||||
// if only root permissions are defined returns them unconditionally
|
||||
if len(u.Permissions) == 1 {
|
||||
return perms
|
||||
}
|
||||
// fallback permissions
|
||||
permissions = perms
|
||||
}
|
||||
relPath := u.GetRelativePath(p)
|
||||
if len(relPath) == 0 {
|
||||
relPath = "/"
|
||||
}
|
||||
dirsForPath := []string{relPath}
|
||||
for {
|
||||
if relPath == "/" {
|
||||
break
|
||||
}
|
||||
relPath = path.Dir(relPath)
|
||||
dirsForPath = append(dirsForPath, relPath)
|
||||
}
|
||||
// dirsForPath contains all the dirs for a given path in reverse order
|
||||
// for example if the path is: /1/2/3/4 it contains:
|
||||
// [ "/1/2/3/4", "/1/2/3", "/1/2", "/1", "/" ]
|
||||
// so the first match is the one we are interested to
|
||||
for _, val := range dirsForPath {
|
||||
if perms, ok := u.Permissions[val]; ok {
|
||||
permissions = perms
|
||||
break
|
||||
}
|
||||
}
|
||||
return permissions
|
||||
}
|
||||
|
||||
// HasPerm returns true if the user has the given permission or any permission
|
||||
func (u *User) HasPerm(permission string) bool {
|
||||
if utils.IsStringInSlice(PermAny, u.Permissions) {
|
||||
func (u *User) HasPerm(permission, path string) bool {
|
||||
perms := u.GetPermissionsForPath(path)
|
||||
if utils.IsStringInSlice(PermAny, perms) {
|
||||
return true
|
||||
}
|
||||
return utils.IsStringInSlice(permission, u.Permissions)
|
||||
return utils.IsStringInSlice(permission, perms)
|
||||
}
|
||||
|
||||
// HasPerms return true if the user has all the given permissions
|
||||
func (u *User) HasPerms(permissions []string) bool {
|
||||
if utils.IsStringInSlice(PermAny, u.Permissions) {
|
||||
func (u *User) HasPerms(permissions []string, path string) bool {
|
||||
perms := u.GetPermissionsForPath(path)
|
||||
if utils.IsStringInSlice(PermAny, perms) {
|
||||
return true
|
||||
}
|
||||
for _, permission := range permissions {
|
||||
if !utils.IsStringInSlice(permission, u.Permissions) {
|
||||
if !utils.IsStringInSlice(permission, perms) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -143,10 +183,13 @@ func (u *User) HasQuotaRestrictions() bool {
|
||||
// GetRelativePath returns the path for a file relative to the user's home dir.
|
||||
// This is the path as seen by SFTP users
|
||||
func (u *User) GetRelativePath(path string) string {
|
||||
rel, err := filepath.Rel(u.GetHomeDir(), path)
|
||||
rel, err := filepath.Rel(u.GetHomeDir(), filepath.Clean(path))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if rel == "." || strings.HasPrefix(rel, "..") {
|
||||
rel = ""
|
||||
}
|
||||
return "/" + filepath.ToSlash(rel)
|
||||
}
|
||||
|
||||
@@ -168,12 +211,28 @@ func (u *User) GetQuotaSummary() string {
|
||||
|
||||
// GetPermissionsAsString returns the user's permissions as comma separated string
|
||||
func (u *User) GetPermissionsAsString() string {
|
||||
var result string
|
||||
for _, p := range u.Permissions {
|
||||
if len(result) > 0 {
|
||||
result += ", "
|
||||
result := ""
|
||||
for dir, perms := range u.Permissions {
|
||||
var dirPerms string
|
||||
for _, p := range perms {
|
||||
if len(dirPerms) > 0 {
|
||||
dirPerms += ", "
|
||||
}
|
||||
dirPerms += p
|
||||
}
|
||||
dp := fmt.Sprintf("%#v: %#v", dir, dirPerms)
|
||||
if dir == "/" {
|
||||
if len(result) > 0 {
|
||||
result = dp + ", " + result
|
||||
} else {
|
||||
result = dp
|
||||
}
|
||||
} else {
|
||||
if len(result) > 0 {
|
||||
result += ", "
|
||||
}
|
||||
result += dp
|
||||
}
|
||||
result += p
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -230,8 +289,12 @@ func (u *User) GetExpirationDateAsString() string {
|
||||
func (u *User) getACopy() User {
|
||||
pubKeys := make([]string, len(u.PublicKeys))
|
||||
copy(pubKeys, u.PublicKeys)
|
||||
permissions := make([]string, len(u.Permissions))
|
||||
copy(permissions, u.Permissions)
|
||||
permissions := make(map[string][]string)
|
||||
for k, v := range u.Permissions {
|
||||
perms := make([]string, len(v))
|
||||
copy(perms, v)
|
||||
permissions[k] = perms
|
||||
}
|
||||
return User{
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
|
||||
Reference in New Issue
Block a user