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:
Nicola Murino
2019-12-25 18:20:19 +01:00
parent f8fd5c067c
commit 489101668c
20 changed files with 1166 additions and 273 deletions

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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

View File

@@ -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,