add groups support
Using groups simplifies the administration of multiple accounts by letting you assign settings once to a group, instead of multiple times to each individual user. Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
857b6cc10a
commit
504cd3efda
53 changed files with 6986 additions and 1076 deletions
|
@ -27,6 +27,7 @@ Several storage backends are supported: local filesystem, encrypted local filesy
|
|||
- Partial authentication. You can configure multi-step authentication requiring, for example, the user password after successful public key authentication.
|
||||
- Per-user authentication methods.
|
||||
- [Two-factor authentication](./docs/howto/two-factor-authentication.md) based on time-based one time passwords (RFC 6238) which works with Authy, Google Authenticator and other compatible apps.
|
||||
- Simplified user administrations using [groups](./docs/groups.md).
|
||||
- Custom authentication via external programs/HTTP API.
|
||||
- Web Client and Web Admin user interfaces support [OpenID Connect](https://openid.net/connect/) authentication and so they can be integrated with identity providers such as [Keycloak](https://www.keycloak.org/). You can find more details [here](./docs/oidc.md).
|
||||
- [Data At Rest Encryption](./docs/dare.md).
|
||||
|
|
|
@ -58,7 +58,6 @@ Please take a look at the usage below to customize the options.`,
|
|||
func init() {
|
||||
addConfigFlags(revertProviderCmd)
|
||||
revertProviderCmd.Flags().IntVar(&revertProviderTargetVersion, "to-version", 15, `15 means the version supported in v2.2.x`)
|
||||
revertProviderCmd.MarkFlagRequired("to-version") //nolint:errcheck
|
||||
|
||||
rootCmd.AddCommand(revertProviderCmd)
|
||||
}
|
||||
|
|
|
@ -140,6 +140,11 @@ Command-line flags should be specified in the Subsystem declaration.
|
|||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
err = user.LoadAndApplyGroupSettings()
|
||||
if err != nil {
|
||||
logger.Error(logSender, connectionID, "unable to apply group settings for user %#v: %v", username, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = sftpd.ServeSubSystemConnection(&user, connectionID, os.Stdin, os.Stdout)
|
||||
if err != nil && err != io.EOF {
|
||||
logger.Warn(logSender, connectionID, "serving subsystem finished with error: %v", err)
|
||||
|
|
|
@ -29,6 +29,7 @@ const (
|
|||
|
||||
const (
|
||||
actionObjectUser = "user"
|
||||
actionObjectGroup = "group"
|
||||
actionObjectAdmin = "admin"
|
||||
actionObjectAPIKey = "api_key"
|
||||
actionObjectShare = "share"
|
||||
|
|
|
@ -32,6 +32,7 @@ const (
|
|||
PermAdminCloseConnections = "close_conns"
|
||||
PermAdminViewServerStatus = "view_status"
|
||||
PermAdminManageAdmins = "manage_admins"
|
||||
PermAdminManageGroups = "manage_groups"
|
||||
PermAdminManageAPIKeys = "manage_apikeys"
|
||||
PermAdminQuotaScans = "quota_scans"
|
||||
PermAdminManageSystem = "manage_system"
|
||||
|
@ -45,10 +46,10 @@ const (
|
|||
var (
|
||||
emailRegex = regexp.MustCompile("^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$")
|
||||
validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers,
|
||||
PermAdminViewUsers, PermAdminViewConnections, PermAdminCloseConnections, PermAdminViewServerStatus,
|
||||
PermAdminManageAdmins, PermAdminManageAPIKeys, PermAdminQuotaScans, PermAdminManageSystem,
|
||||
PermAdminManageDefender, PermAdminViewDefender, PermAdminRetentionChecks, PermAdminMetadataChecks,
|
||||
PermAdminViewEvents}
|
||||
PermAdminViewUsers, PermAdminManageGroups, PermAdminViewConnections, PermAdminCloseConnections,
|
||||
PermAdminViewServerStatus, PermAdminManageAdmins, PermAdminManageAPIKeys, PermAdminQuotaScans,
|
||||
PermAdminManageSystem, PermAdminManageDefender, PermAdminViewDefender, PermAdminRetentionChecks,
|
||||
PermAdminMetadataChecks, PermAdminViewEvents}
|
||||
)
|
||||
|
||||
// AdminTOTPConfig defines the time-based one time password configuration
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -62,7 +62,11 @@ func (cache *usersCache) updateLastLogin(username string) {
|
|||
|
||||
// swapWebDAVUser updates an existing cached user with the specified one
|
||||
// preserving the lock fs if possible
|
||||
func (cache *usersCache) swap(user *User) {
|
||||
// FIXME: this could be racy in rare cases
|
||||
func (cache *usersCache) swap(userRef *User) {
|
||||
user := userRef.getACopy()
|
||||
err := user.LoadAndApplyGroupSettings()
|
||||
|
||||
cache.Lock()
|
||||
defer cache.Unlock()
|
||||
|
||||
|
@ -74,11 +78,17 @@ func (cache *usersCache) swap(user *User) {
|
|||
delete(cache.users, user.Username)
|
||||
return
|
||||
}
|
||||
if cachedUser.User.isFsEqual(user) {
|
||||
if err != nil {
|
||||
providerLog(logger.LevelDebug, "unable to load group settings, for user %#v, removing from cache, err :%v",
|
||||
user.Username, err)
|
||||
delete(cache.users, user.Username)
|
||||
return
|
||||
}
|
||||
if cachedUser.User.isFsEqual(&user) {
|
||||
// the updated user has the same fs as the cached one, we can preserve the lock filesystem
|
||||
providerLog(logger.LevelDebug, "current password and fs unchanged for for user %#v, swap cached one",
|
||||
user.Username)
|
||||
cachedUser.User = *user
|
||||
cachedUser.User = user
|
||||
cache.users[user.Username] = cachedUser
|
||||
} else {
|
||||
// filesystem changed, the cached user is no longer valid
|
||||
|
|
File diff suppressed because it is too large
Load diff
228
dataprovider/group.go
Normal file
228
dataprovider/group.go
Normal file
|
@ -0,0 +1,228 @@
|
|||
package dataprovider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sftpgo/sdk"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/logger"
|
||||
"github.com/drakkan/sftpgo/v2/plugin"
|
||||
"github.com/drakkan/sftpgo/v2/util"
|
||||
"github.com/drakkan/sftpgo/v2/vfs"
|
||||
)
|
||||
|
||||
// GroupUserSettings defines the settings to apply to users
|
||||
type GroupUserSettings struct {
|
||||
sdk.BaseGroupUserSettings
|
||||
// Filesystem configuration details
|
||||
FsConfig vfs.Filesystem `json:"filesystem"`
|
||||
}
|
||||
|
||||
// Group defines an SFTPGo group.
|
||||
// Groups are used to easily configure similar users
|
||||
type Group struct {
|
||||
sdk.BaseGroup
|
||||
// settings to apply to users for whom this is a primary group
|
||||
UserSettings GroupUserSettings `json:"user_settings,omitempty"`
|
||||
// Mapping between virtual paths and virtual folders
|
||||
VirtualFolders []vfs.VirtualFolder `json:"virtual_folders,omitempty"`
|
||||
}
|
||||
|
||||
// GetPermissions returns the permissions as list
|
||||
func (g *Group) GetPermissions() []sdk.DirectoryPermissions {
|
||||
result := make([]sdk.DirectoryPermissions, 0, len(g.UserSettings.Permissions))
|
||||
for k, v := range g.UserSettings.Permissions {
|
||||
result = append(result, sdk.DirectoryPermissions{
|
||||
Path: k,
|
||||
Permissions: v,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetAllowedIPAsString returns the allowed IP as comma separated string
|
||||
func (g *Group) GetAllowedIPAsString() string {
|
||||
return strings.Join(g.UserSettings.Filters.AllowedIP, ",")
|
||||
}
|
||||
|
||||
// GetDeniedIPAsString returns the denied IP as comma separated string
|
||||
func (g *Group) GetDeniedIPAsString() string {
|
||||
return strings.Join(g.UserSettings.Filters.DeniedIP, ",")
|
||||
}
|
||||
|
||||
// HasExternalAuth returns true if the external authentication is globally enabled
|
||||
// and it is not disabled for this group
|
||||
func (g *Group) HasExternalAuth() bool {
|
||||
if g.UserSettings.Filters.Hooks.ExternalAuthDisabled {
|
||||
return false
|
||||
}
|
||||
if config.ExternalAuthHook != "" {
|
||||
return true
|
||||
}
|
||||
return plugin.Handler.HasAuthenticators()
|
||||
}
|
||||
|
||||
// SetEmptySecretsIfNil sets the secrets to empty if nil
|
||||
func (g *Group) SetEmptySecretsIfNil() {
|
||||
g.UserSettings.FsConfig.SetEmptySecretsIfNil()
|
||||
for idx := range g.VirtualFolders {
|
||||
vfolder := &g.VirtualFolders[idx]
|
||||
vfolder.FsConfig.SetEmptySecretsIfNil()
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareForRendering prepares a group for rendering.
|
||||
// It hides confidential data and set to nil the empty secrets
|
||||
// so they are not serialized
|
||||
func (g *Group) PrepareForRendering() {
|
||||
g.UserSettings.FsConfig.HideConfidentialData()
|
||||
g.UserSettings.FsConfig.SetNilSecretsIfEmpty()
|
||||
for idx := range g.VirtualFolders {
|
||||
folder := &g.VirtualFolders[idx]
|
||||
folder.PrepareForRendering()
|
||||
}
|
||||
}
|
||||
|
||||
// RenderAsJSON implements the renderer interface used within plugins
|
||||
func (g *Group) RenderAsJSON(reload bool) ([]byte, error) {
|
||||
if reload {
|
||||
group, err := provider.groupExists(g.Name)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "unable to reload group before rendering as json: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
group.PrepareForRendering()
|
||||
return json.Marshal(group)
|
||||
}
|
||||
g.PrepareForRendering()
|
||||
return json.Marshal(g)
|
||||
}
|
||||
|
||||
// GetEncryptionAdditionalData returns the additional data to use for AEAD
|
||||
func (g *Group) GetEncryptionAdditionalData() string {
|
||||
return fmt.Sprintf("group_%v", g.Name)
|
||||
}
|
||||
|
||||
// GetGCSCredentialsFilePath returns the path for GCS credentials
|
||||
func (g *Group) GetGCSCredentialsFilePath() string {
|
||||
return filepath.Join(credentialsDirPath, "groups", fmt.Sprintf("%v_gcs_credentials.json", g.Name))
|
||||
}
|
||||
|
||||
// HasRedactedSecret returns true if the user has a redacted secret
|
||||
func (g *Group) hasRedactedSecret() bool {
|
||||
for idx := range g.VirtualFolders {
|
||||
folder := &g.VirtualFolders[idx]
|
||||
if folder.HasRedactedSecret() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return g.UserSettings.FsConfig.HasRedactedSecret()
|
||||
}
|
||||
|
||||
func (g *Group) validate() error {
|
||||
g.SetEmptySecretsIfNil()
|
||||
if g.Name == "" {
|
||||
return util.NewValidationError("name is mandatory")
|
||||
}
|
||||
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(g.Name) {
|
||||
return util.NewValidationError(fmt.Sprintf("name %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~", g.Name))
|
||||
}
|
||||
if g.hasRedactedSecret() {
|
||||
return util.NewValidationError("cannot save a user with a redacted secret")
|
||||
}
|
||||
vfolders, err := validateAssociatedVirtualFolders(g.VirtualFolders)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
g.VirtualFolders = vfolders
|
||||
return g.validateUserSettings()
|
||||
}
|
||||
|
||||
func (g *Group) validateUserSettings() error {
|
||||
if g.UserSettings.HomeDir != "" {
|
||||
g.UserSettings.HomeDir = filepath.Clean(g.UserSettings.HomeDir)
|
||||
if !filepath.IsAbs(g.UserSettings.HomeDir) {
|
||||
return util.NewValidationError(fmt.Sprintf("home_dir must be an absolute path, actual value: %v",
|
||||
g.UserSettings.HomeDir))
|
||||
}
|
||||
}
|
||||
if err := g.UserSettings.FsConfig.Validate(g); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := saveGCSCredentials(&g.UserSettings.FsConfig, g); err != nil {
|
||||
return err
|
||||
}
|
||||
if g.UserSettings.TotalDataTransfer > 0 {
|
||||
// if a total data transfer is defined we reset the separate upload and download limits
|
||||
g.UserSettings.UploadDataTransfer = 0
|
||||
g.UserSettings.DownloadDataTransfer = 0
|
||||
}
|
||||
if len(g.UserSettings.Permissions) > 0 {
|
||||
permissions, err := validateUserPermissions(g.UserSettings.Permissions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
g.UserSettings.Permissions = permissions
|
||||
}
|
||||
if err := validateBaseFilters(&g.UserSettings.Filters); err != nil {
|
||||
return err
|
||||
}
|
||||
if !g.HasExternalAuth() {
|
||||
g.UserSettings.Filters.ExternalAuthCacheTime = 0
|
||||
}
|
||||
g.UserSettings.Filters.UserType = ""
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Group) getACopy() Group {
|
||||
users := make([]string, len(g.Users))
|
||||
copy(users, g.Users)
|
||||
virtualFolders := make([]vfs.VirtualFolder, 0, len(g.VirtualFolders))
|
||||
for idx := range g.VirtualFolders {
|
||||
vfolder := g.VirtualFolders[idx].GetACopy()
|
||||
virtualFolders = append(virtualFolders, vfolder)
|
||||
}
|
||||
permissions := make(map[string][]string)
|
||||
for k, v := range g.UserSettings.Permissions {
|
||||
perms := make([]string, len(v))
|
||||
copy(perms, v)
|
||||
permissions[k] = perms
|
||||
}
|
||||
|
||||
return Group{
|
||||
BaseGroup: sdk.BaseGroup{
|
||||
ID: g.ID,
|
||||
Name: g.Name,
|
||||
Description: g.Description,
|
||||
CreatedAt: g.CreatedAt,
|
||||
UpdatedAt: g.UpdatedAt,
|
||||
Users: users,
|
||||
},
|
||||
UserSettings: GroupUserSettings{
|
||||
BaseGroupUserSettings: sdk.BaseGroupUserSettings{
|
||||
HomeDir: g.UserSettings.HomeDir,
|
||||
MaxSessions: g.UserSettings.MaxSessions,
|
||||
QuotaSize: g.UserSettings.QuotaSize,
|
||||
QuotaFiles: g.UserSettings.QuotaFiles,
|
||||
Permissions: permissions,
|
||||
UploadBandwidth: g.UserSettings.UploadBandwidth,
|
||||
DownloadBandwidth: g.UserSettings.DownloadBandwidth,
|
||||
UploadDataTransfer: g.UserSettings.UploadDataTransfer,
|
||||
DownloadDataTransfer: g.UserSettings.DownloadDataTransfer,
|
||||
TotalDataTransfer: g.UserSettings.TotalDataTransfer,
|
||||
Filters: copyBaseUserFilters(g.UserSettings.Filters),
|
||||
},
|
||||
FsConfig: g.UserSettings.FsConfig.GetACopy(),
|
||||
},
|
||||
VirtualFolders: virtualFolders,
|
||||
}
|
||||
}
|
||||
|
||||
// GetUsersAsString returns the list of users as comma separated string
|
||||
func (g *Group) GetUsersAsString() string {
|
||||
return strings.Join(g.Users, ",")
|
||||
}
|
|
@ -28,6 +28,10 @@ type memoryProviderHandle struct {
|
|||
usernames []string
|
||||
// map for users, username is the key
|
||||
users map[string]User
|
||||
// slice with ordered group names
|
||||
groupnames []string
|
||||
// map for group, group name is the key
|
||||
groups map[string]Group
|
||||
// map for virtual folders, folder name is the key
|
||||
vfolders map[string]vfs.BaseVirtualFolder
|
||||
// slice with ordered folder names
|
||||
|
@ -64,6 +68,8 @@ func initializeMemoryProvider(basePath string) {
|
|||
isClosed: false,
|
||||
usernames: []string{},
|
||||
users: make(map[string]User),
|
||||
groupnames: []string{},
|
||||
groups: make(map[string]Group),
|
||||
vfolders: make(map[string]vfs.BaseVirtualFolder),
|
||||
vfoldersNames: []string{},
|
||||
admins: make(map[string]Admin),
|
||||
|
@ -299,7 +305,12 @@ func (p *MemoryProvider) addUser(user *User) error {
|
|||
user.LastLogin = 0
|
||||
user.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
user.VirtualFolders = p.joinVirtualFoldersFields(user)
|
||||
user.VirtualFolders = p.joinUserVirtualFoldersFields(user)
|
||||
for idx := range user.Groups {
|
||||
if err = p.addUserFromGroupMapping(user.Username, user.Groups[idx].Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
p.dbHandle.users[user.Username] = user.getACopy()
|
||||
p.dbHandle.usernames = append(p.dbHandle.usernames, user.Username)
|
||||
sort.Strings(p.dbHandle.usernames)
|
||||
|
@ -325,9 +336,19 @@ func (p *MemoryProvider) updateUser(user *User) error {
|
|||
return err
|
||||
}
|
||||
for _, oldFolder := range u.VirtualFolders {
|
||||
p.removeUserFromFolderMapping(oldFolder.Name, u.Username)
|
||||
p.removeRelationFromFolderMapping(oldFolder.Name, u.Username, "")
|
||||
}
|
||||
for idx := range u.Groups {
|
||||
if err = p.removeUserFromGroupMapping(u.Username, u.Groups[idx].Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
user.VirtualFolders = p.joinUserVirtualFoldersFields(user)
|
||||
for idx := range user.Groups {
|
||||
if err = p.addUserFromGroupMapping(user.Username, user.Groups[idx].Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
user.VirtualFolders = p.joinVirtualFoldersFields(user)
|
||||
user.LastQuotaUpdate = u.LastQuotaUpdate
|
||||
user.UsedQuotaSize = u.UsedQuotaSize
|
||||
user.UsedQuotaFiles = u.UsedQuotaFiles
|
||||
|
@ -342,7 +363,7 @@ func (p *MemoryProvider) updateUser(user *User) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) deleteUser(user *User) error {
|
||||
func (p *MemoryProvider) deleteUser(user User) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
|
@ -353,7 +374,12 @@ func (p *MemoryProvider) deleteUser(user *User) error {
|
|||
return err
|
||||
}
|
||||
for _, oldFolder := range u.VirtualFolders {
|
||||
p.removeUserFromFolderMapping(oldFolder.Name, u.Username)
|
||||
p.removeRelationFromFolderMapping(oldFolder.Name, u.Username, "")
|
||||
}
|
||||
for idx := range u.Groups {
|
||||
if err = p.removeUserFromGroupMapping(u.Username, u.Groups[idx].Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
delete(p.dbHandle.users, user.Username)
|
||||
// this could be more efficient
|
||||
|
@ -433,9 +459,21 @@ func (p *MemoryProvider) getUsersForQuotaCheck(toFetch map[string]bool) ([]User,
|
|||
if val, ok := toFetch[username]; ok {
|
||||
u := p.dbHandle.users[username]
|
||||
user := u.getACopy()
|
||||
if len(user.Groups) > 0 {
|
||||
groupMapping := make(map[string]Group)
|
||||
for idx := range user.Groups {
|
||||
group, err := p.groupExistsInternal(user.Groups[idx].Name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
groupMapping[group.Name] = group
|
||||
}
|
||||
user.applyGroupSettings(groupMapping)
|
||||
}
|
||||
if val {
|
||||
p.addVirtualFoldersToUser(&user)
|
||||
}
|
||||
user.SetEmptySecretsIfNil()
|
||||
user.PrepareForRendering()
|
||||
users = append(users, user)
|
||||
}
|
||||
|
@ -512,6 +550,13 @@ func (p *MemoryProvider) userExistsInternal(username string) (User, error) {
|
|||
return User{}, util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username))
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) groupExistsInternal(name string) (Group, error) {
|
||||
if val, ok := p.dbHandle.groups[name]; ok {
|
||||
return val.getACopy(), nil
|
||||
}
|
||||
return Group{}, util.NewRecordNotFoundError(fmt.Sprintf("group %#v does not exist", name))
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) addAdmin(admin *Admin) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
|
@ -558,7 +603,7 @@ func (p *MemoryProvider) updateAdmin(admin *Admin) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) deleteAdmin(admin *Admin) error {
|
||||
func (p *MemoryProvider) deleteAdmin(admin Admin) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
|
@ -680,6 +725,192 @@ func (p *MemoryProvider) updateFolderQuota(name string, filesAdd int, sizeAdd in
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) getGroups(limit, offset int, order string, minimal bool) ([]Group, error) {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
return nil, errMemoryProviderClosed
|
||||
}
|
||||
if limit <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
groups := make([]Group, 0, limit)
|
||||
itNum := 0
|
||||
if order == OrderASC {
|
||||
for _, name := range p.dbHandle.groupnames {
|
||||
itNum++
|
||||
if itNum <= offset {
|
||||
continue
|
||||
}
|
||||
g := p.dbHandle.groups[name]
|
||||
group := g.getACopy()
|
||||
p.addVirtualFoldersToGroup(&group)
|
||||
group.PrepareForRendering()
|
||||
groups = append(groups, group)
|
||||
if len(groups) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i := len(p.dbHandle.groupnames) - 1; i >= 0; i-- {
|
||||
itNum++
|
||||
if itNum <= offset {
|
||||
continue
|
||||
}
|
||||
name := p.dbHandle.groupnames[i]
|
||||
g := p.dbHandle.groups[name]
|
||||
group := g.getACopy()
|
||||
p.addVirtualFoldersToGroup(&group)
|
||||
group.PrepareForRendering()
|
||||
groups = append(groups, group)
|
||||
if len(groups) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) getGroupsWithNames(names []string) ([]Group, error) {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
return nil, errMemoryProviderClosed
|
||||
}
|
||||
groups := make([]Group, 0, len(names))
|
||||
for _, name := range names {
|
||||
if val, ok := p.dbHandle.groups[name]; ok {
|
||||
group := val.getACopy()
|
||||
p.addVirtualFoldersToGroup(&group)
|
||||
groups = append(groups, group)
|
||||
}
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) getUsersInGroups(names []string) ([]string, error) {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
return nil, errMemoryProviderClosed
|
||||
}
|
||||
var users []string
|
||||
for _, name := range names {
|
||||
if val, ok := p.dbHandle.groups[name]; ok {
|
||||
group := val.getACopy()
|
||||
users = append(users, group.Users...)
|
||||
}
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) groupExists(name string) (Group, error) {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
return Group{}, errMemoryProviderClosed
|
||||
}
|
||||
group, err := p.groupExistsInternal(name)
|
||||
if err != nil {
|
||||
return group, err
|
||||
}
|
||||
p.addVirtualFoldersToGroup(&group)
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) addGroup(group *Group) error {
|
||||
if err := group.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
return errMemoryProviderClosed
|
||||
}
|
||||
|
||||
_, err := p.groupExistsInternal(group.Name)
|
||||
if err == nil {
|
||||
return fmt.Errorf("group %#v already exists", group.Name)
|
||||
}
|
||||
group.ID = p.getNextGroupID()
|
||||
group.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
group.VirtualFolders = p.joinGroupVirtualFoldersFields(group)
|
||||
p.dbHandle.groups[group.Name] = group.getACopy()
|
||||
p.dbHandle.groupnames = append(p.dbHandle.groupnames, group.Name)
|
||||
sort.Strings(p.dbHandle.groupnames)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) updateGroup(group *Group) error {
|
||||
if err := group.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
return errMemoryProviderClosed
|
||||
}
|
||||
g, err := p.groupExistsInternal(group.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, oldFolder := range g.VirtualFolders {
|
||||
p.removeRelationFromFolderMapping(oldFolder.Name, "", g.Name)
|
||||
}
|
||||
group.VirtualFolders = p.joinGroupVirtualFoldersFields(group)
|
||||
group.CreatedAt = g.CreatedAt
|
||||
group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
group.ID = g.ID
|
||||
p.dbHandle.groups[group.Name] = group.getACopy()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) deleteGroup(group Group) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
return errMemoryProviderClosed
|
||||
}
|
||||
g, err := p.groupExistsInternal(group.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(g.Users) > 0 {
|
||||
return util.NewValidationError(fmt.Sprintf("the group %#v is referenced, it cannot be removed", group.Name))
|
||||
}
|
||||
for _, oldFolder := range g.VirtualFolders {
|
||||
p.removeRelationFromFolderMapping(oldFolder.Name, "", g.Name)
|
||||
}
|
||||
delete(p.dbHandle.groups, group.Name)
|
||||
// this could be more efficient
|
||||
p.dbHandle.groupnames = make([]string, 0, len(p.dbHandle.groups))
|
||||
for name := range p.dbHandle.groups {
|
||||
p.dbHandle.groupnames = append(p.dbHandle.groupnames, name)
|
||||
}
|
||||
sort.Strings(p.dbHandle.groupnames)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) dumpGroups() ([]Group, error) {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
groups := make([]Group, 0, len(p.dbHandle.groups))
|
||||
var err error
|
||||
if p.dbHandle.isClosed {
|
||||
return groups, errMemoryProviderClosed
|
||||
}
|
||||
for _, name := range p.dbHandle.groupnames {
|
||||
g := p.dbHandle.groups[name]
|
||||
group := g.getACopy()
|
||||
p.addVirtualFoldersToGroup(&group)
|
||||
groups = append(groups, group)
|
||||
}
|
||||
return groups, err
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) getUsedFolderQuota(name string) (int, int64, error) {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
|
@ -694,11 +925,70 @@ func (p *MemoryProvider) getUsedFolderQuota(name string) (int, int64, error) {
|
|||
return folder.UsedQuotaFiles, folder.UsedQuotaSize, err
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) joinVirtualFoldersFields(user *User) []vfs.VirtualFolder {
|
||||
func (p *MemoryProvider) joinGroupVirtualFoldersFields(group *Group) []vfs.VirtualFolder {
|
||||
var folders []vfs.VirtualFolder
|
||||
for idx := range group.VirtualFolders {
|
||||
folder := &group.VirtualFolders[idx]
|
||||
f, err := p.addOrUpdateFolderInternal(&folder.BaseVirtualFolder, "", group.Name, 0, 0, 0)
|
||||
if err == nil {
|
||||
folder.BaseVirtualFolder = f
|
||||
folders = append(folders, *folder)
|
||||
}
|
||||
}
|
||||
return folders
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) addVirtualFoldersToGroup(group *Group) {
|
||||
if len(group.VirtualFolders) > 0 {
|
||||
var folders []vfs.VirtualFolder
|
||||
for idx := range group.VirtualFolders {
|
||||
folder := &group.VirtualFolders[idx]
|
||||
baseFolder, err := p.folderExistsInternal(folder.Name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
folder.BaseVirtualFolder = baseFolder.GetACopy()
|
||||
folders = append(folders, *folder)
|
||||
}
|
||||
group.VirtualFolders = folders
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) addUserFromGroupMapping(username, groupname string) error {
|
||||
g, err := p.groupExistsInternal(groupname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !util.IsStringInSlice(username, g.Users) {
|
||||
g.Users = append(g.Users, username)
|
||||
p.dbHandle.groups[groupname] = g
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) removeUserFromGroupMapping(username, groupname string) error {
|
||||
g, err := p.groupExistsInternal(groupname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if util.IsStringInSlice(username, g.Users) {
|
||||
var users []string
|
||||
for _, u := range g.Users {
|
||||
if u != username {
|
||||
users = append(users, u)
|
||||
}
|
||||
}
|
||||
g.Users = users
|
||||
p.dbHandle.groups[groupname] = g
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) joinUserVirtualFoldersFields(user *User) []vfs.VirtualFolder {
|
||||
var folders []vfs.VirtualFolder
|
||||
for idx := range user.VirtualFolders {
|
||||
folder := &user.VirtualFolders[idx]
|
||||
f, err := p.addOrUpdateFolderInternal(&folder.BaseVirtualFolder, user.Username, 0, 0, 0)
|
||||
f, err := p.addOrUpdateFolderInternal(&folder.BaseVirtualFolder, user.Username, "", 0, 0, 0)
|
||||
if err == nil {
|
||||
folder.BaseVirtualFolder = f
|
||||
folders = append(folders, *folder)
|
||||
|
@ -723,16 +1013,27 @@ func (p *MemoryProvider) addVirtualFoldersToUser(user *User) {
|
|||
}
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) removeUserFromFolderMapping(folderName, username string) {
|
||||
func (p *MemoryProvider) removeRelationFromFolderMapping(folderName, username, groupname string) {
|
||||
folder, err := p.folderExistsInternal(folderName)
|
||||
if err == nil {
|
||||
var usernames []string
|
||||
for _, user := range folder.Users {
|
||||
if user != username {
|
||||
usernames = append(usernames, user)
|
||||
if username != "" {
|
||||
var usernames []string
|
||||
for _, user := range folder.Users {
|
||||
if user != username {
|
||||
usernames = append(usernames, user)
|
||||
}
|
||||
}
|
||||
folder.Users = usernames
|
||||
}
|
||||
if groupname != "" {
|
||||
var groups []string
|
||||
for _, group := range folder.Groups {
|
||||
if group != groupname {
|
||||
groups = append(groups, group)
|
||||
}
|
||||
}
|
||||
folder.Groups = groups
|
||||
}
|
||||
folder.Users = usernames
|
||||
p.dbHandle.vfolders[folder.Name] = folder
|
||||
}
|
||||
}
|
||||
|
@ -745,18 +1046,21 @@ func (p *MemoryProvider) updateFoldersMappingInternal(folder vfs.BaseVirtualFold
|
|||
}
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) addOrUpdateFolderInternal(baseFolder *vfs.BaseVirtualFolder, username string, usedQuotaSize int64,
|
||||
usedQuotaFiles int, lastQuotaUpdate int64) (vfs.BaseVirtualFolder, error,
|
||||
) {
|
||||
func (p *MemoryProvider) addOrUpdateFolderInternal(baseFolder *vfs.BaseVirtualFolder, username, groupname string,
|
||||
usedQuotaSize int64, usedQuotaFiles int, lastQuotaUpdate int64,
|
||||
) (vfs.BaseVirtualFolder, error) {
|
||||
folder, err := p.folderExistsInternal(baseFolder.Name)
|
||||
if err == nil {
|
||||
// exists
|
||||
folder.MappedPath = baseFolder.MappedPath
|
||||
folder.Description = baseFolder.Description
|
||||
folder.FsConfig = baseFolder.FsConfig.GetACopy()
|
||||
if !util.IsStringInSlice(username, folder.Users) {
|
||||
if username != "" && !util.IsStringInSlice(username, folder.Users) {
|
||||
folder.Users = append(folder.Users, username)
|
||||
}
|
||||
if groupname != "" && !util.IsStringInSlice(groupname, folder.Groups) {
|
||||
folder.Groups = append(folder.Groups, groupname)
|
||||
}
|
||||
p.updateFoldersMappingInternal(folder)
|
||||
return folder, nil
|
||||
}
|
||||
|
@ -766,7 +1070,12 @@ func (p *MemoryProvider) addOrUpdateFolderInternal(baseFolder *vfs.BaseVirtualFo
|
|||
folder.UsedQuotaSize = usedQuotaSize
|
||||
folder.UsedQuotaFiles = usedQuotaFiles
|
||||
folder.LastQuotaUpdate = lastQuotaUpdate
|
||||
folder.Users = []string{username}
|
||||
if username != "" {
|
||||
folder.Users = []string{username}
|
||||
}
|
||||
if groupname != "" {
|
||||
folder.Groups = []string{groupname}
|
||||
}
|
||||
p.updateFoldersMappingInternal(folder)
|
||||
return folder, nil
|
||||
}
|
||||
|
@ -780,7 +1089,7 @@ func (p *MemoryProvider) folderExistsInternal(name string) (vfs.BaseVirtualFolde
|
|||
return vfs.BaseVirtualFolder{}, util.NewRecordNotFoundError(fmt.Sprintf("folder %#v does not exist", name))
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) getFolders(limit, offset int, order string) ([]vfs.BaseVirtualFolder, error) {
|
||||
func (p *MemoryProvider) getFolders(limit, offset int, order string, minimal bool) ([]vfs.BaseVirtualFolder, error) {
|
||||
folders := make([]vfs.BaseVirtualFolder, 0, limit)
|
||||
var err error
|
||||
p.dbHandle.Lock()
|
||||
|
@ -902,7 +1211,7 @@ func (p *MemoryProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
func (p *MemoryProvider) deleteFolder(folder vfs.BaseVirtualFolder) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
|
@ -927,6 +1236,20 @@ func (p *MemoryProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
|
|||
p.dbHandle.users[user.Username] = user
|
||||
}
|
||||
}
|
||||
for _, groupname := range folder.Groups {
|
||||
group, err := p.groupExistsInternal(groupname)
|
||||
if err == nil {
|
||||
var folders []vfs.VirtualFolder
|
||||
for idx := range group.VirtualFolders {
|
||||
groupFolder := &group.VirtualFolders[idx]
|
||||
if folder.Name != groupFolder.Name {
|
||||
folders = append(folders, *groupFolder)
|
||||
}
|
||||
}
|
||||
group.VirtualFolders = folders
|
||||
p.dbHandle.groups[group.Name] = group
|
||||
}
|
||||
}
|
||||
delete(p.dbHandle.vfolders, folder.Name)
|
||||
p.dbHandle.vfoldersNames = []string{}
|
||||
for name := range p.dbHandle.vfolders {
|
||||
|
@ -1022,7 +1345,7 @@ func (p *MemoryProvider) updateAPIKey(apiKey *APIKey) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) deleteAPIKey(apiKey *APIKey) error {
|
||||
func (p *MemoryProvider) deleteAPIKey(apiKey APIKey) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
|
@ -1249,7 +1572,7 @@ func (p *MemoryProvider) updateShare(share *Share) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) deleteShare(share *Share) error {
|
||||
func (p *MemoryProvider) deleteShare(share Share) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
|
@ -1430,6 +1753,16 @@ func (p *MemoryProvider) getNextAdminID() int64 {
|
|||
return nextID
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) getNextGroupID() int64 {
|
||||
nextID := int64(1)
|
||||
for _, g := range p.dbHandle.groups {
|
||||
if g.ID >= nextID {
|
||||
nextID = g.ID + 1
|
||||
}
|
||||
}
|
||||
return nextID
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) clear() {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
|
@ -1482,6 +1815,10 @@ func (p *MemoryProvider) reloadConfig() error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := p.restoreGroups(&dump); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.restoreUsers(&dump); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1573,6 +1910,30 @@ func (p *MemoryProvider) restoreAdmins(dump *BackupData) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) restoreGroups(dump *BackupData) error {
|
||||
for _, group := range dump.Groups {
|
||||
group := group // pin
|
||||
group.Name = config.convertName(group.Name)
|
||||
g, err := p.groupExists(group.Name)
|
||||
if err == nil {
|
||||
group.ID = g.ID
|
||||
err = UpdateGroup(&group, g.Users, ActionExecutorSystem, "")
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error updating group %#v: %v", group.Name, err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
group.Users = nil
|
||||
err = AddGroup(&group, ActionExecutorSystem, "")
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error adding group %#v: %v", group.Name, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) restoreFolders(dump *BackupData) error {
|
||||
for _, folder := range dump.Folders {
|
||||
folder := folder // pin
|
||||
|
@ -1580,7 +1941,7 @@ func (p *MemoryProvider) restoreFolders(dump *BackupData) error {
|
|||
f, err := p.getFolderByName(folder.Name)
|
||||
if err == nil {
|
||||
folder.ID = f.ID
|
||||
err = UpdateFolder(&folder, f.Users, ActionExecutorSystem, "")
|
||||
err = UpdateFolder(&folder, f.Users, f.Groups, ActionExecutorSystem, "")
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error updating folder %#v: %v", folder.Name, err)
|
||||
return err
|
||||
|
|
|
@ -24,10 +24,14 @@ import (
|
|||
const (
|
||||
mysqlResetSQL = "DROP TABLE IF EXISTS `{{api_keys}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{folders_mapping}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{users_folders_mapping}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{users_groups_mapping}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{groups_folders_mapping}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{admins}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{folders}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{shares}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{users}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{groups}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{defender_events}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{defender_hosts}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{active_transfers}}` CASCADE;" +
|
||||
|
@ -101,6 +105,53 @@ const (
|
|||
"ALTER TABLE `{{users}}` DROP COLUMN `total_data_transfer`;" +
|
||||
"ALTER TABLE `{{users}}` DROP COLUMN `download_data_transfer`;" +
|
||||
"DROP TABLE `{{active_transfers}}` CASCADE;"
|
||||
mysqlV17SQL = "CREATE TABLE `{{groups}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, " +
|
||||
"`name` varchar(255) NOT NULL UNIQUE, `description` varchar(512) NULL, `created_at` bigint NOT NULL, " +
|
||||
"`updated_at` bigint NOT NULL, `user_settings` longtext NULL);" +
|
||||
"CREATE TABLE `{{groups_folders_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, " +
|
||||
"`group_id` integer NOT NULL, `folder_id` integer NOT NULL, " +
|
||||
"`virtual_path` longtext NOT NULL, `quota_size` bigint NOT NULL, `quota_files` integer NOT NULL);" +
|
||||
"CREATE TABLE `{{users_groups_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, " +
|
||||
"`user_id` integer NOT NULL, `group_id` integer NOT NULL, `group_type` integer NOT NULL);" +
|
||||
"ALTER TABLE `{{folders_mapping}}` DROP FOREIGN KEY `{{prefix}}folders_mapping_folder_id_fk_folders_id`;" +
|
||||
"ALTER TABLE `{{folders_mapping}}` DROP FOREIGN KEY `{{prefix}}folders_mapping_user_id_fk_users_id`;" +
|
||||
"ALTER TABLE `{{folders_mapping}}` DROP INDEX `{{prefix}}unique_mapping`;" +
|
||||
"RENAME TABLE `{{folders_mapping}}` TO `{{users_folders_mapping}}`;" +
|
||||
"ALTER TABLE `{{users_folders_mapping}}` ADD CONSTRAINT `{{prefix}}unique_user_folder_mapping` " +
|
||||
"UNIQUE (`user_id`, `folder_id`);" +
|
||||
"ALTER TABLE `{{users_folders_mapping}}` ADD CONSTRAINT `{{prefix}}users_folders_mapping_user_id_fk_users_id` " +
|
||||
"FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE;" +
|
||||
"ALTER TABLE `{{users_folders_mapping}}` ADD CONSTRAINT `{{prefix}}users_folders_mapping_folder_id_fk_folders_id` " +
|
||||
"FOREIGN KEY (`folder_id`) REFERENCES `{{folders}}` (`id`) ON DELETE CASCADE;" +
|
||||
"ALTER TABLE `{{users_groups_mapping}}` ADD CONSTRAINT `{{prefix}}unique_user_group_mapping` UNIQUE (`user_id`, `group_id`);" +
|
||||
"ALTER TABLE `{{groups_folders_mapping}}` ADD CONSTRAINT `{{prefix}}unique_group_folder_mapping` UNIQUE (`group_id`, `folder_id`);" +
|
||||
"ALTER TABLE `{{users_groups_mapping}}` ADD CONSTRAINT `{{prefix}}users_groups_mapping_group_id_fk_groups_id` " +
|
||||
"FOREIGN KEY (`group_id`) REFERENCES `{{groups}}` (`id`) ON DELETE NO ACTION;" +
|
||||
"ALTER TABLE `{{users_groups_mapping}}` ADD CONSTRAINT `{{prefix}}users_groups_mapping_user_id_fk_users_id` " +
|
||||
"FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE;" +
|
||||
"ALTER TABLE `{{groups_folders_mapping}}` ADD CONSTRAINT `{{prefix}}groups_folders_mapping_folder_id_fk_folders_id` " +
|
||||
"FOREIGN KEY (`folder_id`) REFERENCES `{{folders}}` (`id`) ON DELETE CASCADE;" +
|
||||
"ALTER TABLE `{{groups_folders_mapping}}` ADD CONSTRAINT `{{prefix}}groups_folders_mapping_group_id_fk_groups_id` " +
|
||||
"FOREIGN KEY (`group_id`) REFERENCES `{{groups}}` (`id`) ON DELETE CASCADE;" +
|
||||
"CREATE INDEX `{{prefix}}groups_updated_at_idx` ON `{{groups}}` (`updated_at`);"
|
||||
mysqlV17DownSQL = "ALTER TABLE `{{groups_folders_mapping}}` DROP FOREIGN KEY `{{prefix}}groups_folders_mapping_group_id_fk_groups_id`;" +
|
||||
"ALTER TABLE `{{groups_folders_mapping}}` DROP FOREIGN KEY `{{prefix}}groups_folders_mapping_folder_id_fk_folders_id`;" +
|
||||
"ALTER TABLE `{{users_groups_mapping}}` DROP FOREIGN KEY `{{prefix}}users_groups_mapping_user_id_fk_users_id`;" +
|
||||
"ALTER TABLE `{{users_groups_mapping}}` DROP FOREIGN KEY `{{prefix}}users_groups_mapping_group_id_fk_groups_id`;" +
|
||||
"ALTER TABLE `{{groups_folders_mapping}}` DROP INDEX `{{prefix}}unique_group_folder_mapping`;" +
|
||||
"ALTER TABLE `{{users_groups_mapping}}` DROP INDEX `{{prefix}}unique_user_group_mapping`;" +
|
||||
"DROP TABLE `{{users_groups_mapping}}` CASCADE;" +
|
||||
"DROP TABLE `{{groups_folders_mapping}}` CASCADE;" +
|
||||
"DROP TABLE `{{groups}}` CASCADE;" +
|
||||
"ALTER TABLE `{{users_folders_mapping}}` DROP FOREIGN KEY `{{prefix}}users_folders_mapping_folder_id_fk_folders_id`;" +
|
||||
"ALTER TABLE `{{users_folders_mapping}}` DROP FOREIGN KEY `{{prefix}}users_folders_mapping_user_id_fk_users_id`;" +
|
||||
"ALTER TABLE `{{users_folders_mapping}}` DROP INDEX `{{prefix}}unique_user_folder_mapping`;" +
|
||||
"RENAME TABLE `{{users_folders_mapping}}` TO `{{folders_mapping}}`;" +
|
||||
"ALTER TABLE `{{folders_mapping}}` ADD CONSTRAINT `{{prefix}}unique_mapping` UNIQUE (`user_id`, `folder_id`);" +
|
||||
"ALTER TABLE `{{folders_mapping}}` ADD CONSTRAINT `{{prefix}}folders_mapping_user_id_fk_users_id` " +
|
||||
"FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE;" +
|
||||
"ALTER TABLE `{{folders_mapping}}` ADD CONSTRAINT `{{prefix}}folders_mapping_folder_id_fk_folders_id` " +
|
||||
"FOREIGN KEY (`folder_id`) REFERENCES `{{folders}}` (`id`) ON DELETE CASCADE;"
|
||||
)
|
||||
|
||||
// MySQLProvider defines the auth provider for MySQL/MariaDB database
|
||||
|
@ -243,7 +294,7 @@ func (p *MySQLProvider) updateUser(user *User) error {
|
|||
return sqlCommonUpdateUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) deleteUser(user *User) error {
|
||||
func (p *MySQLProvider) deleteUser(user User) error {
|
||||
return sqlCommonDeleteUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
|
@ -271,8 +322,8 @@ func (p *MySQLProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
|
|||
return sqlCommonDumpFolders(p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) getFolders(limit, offset int, order string) ([]vfs.BaseVirtualFolder, error) {
|
||||
return sqlCommonGetFolders(limit, offset, order, p.dbHandle)
|
||||
func (p *MySQLProvider) getFolders(limit, offset int, order string, minimal bool) ([]vfs.BaseVirtualFolder, error) {
|
||||
return sqlCommonGetFolders(limit, offset, order, minimal, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, error) {
|
||||
|
@ -289,7 +340,7 @@ func (p *MySQLProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
|
|||
return sqlCommonUpdateFolder(folder, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
func (p *MySQLProvider) deleteFolder(folder vfs.BaseVirtualFolder) error {
|
||||
return sqlCommonDeleteFolder(folder, p.dbHandle)
|
||||
}
|
||||
|
||||
|
@ -301,6 +352,38 @@ func (p *MySQLProvider) getUsedFolderQuota(name string) (int, int64, error) {
|
|||
return sqlCommonGetFolderUsedQuota(name, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) getGroups(limit, offset int, order string, minimal bool) ([]Group, error) {
|
||||
return sqlCommonGetGroups(limit, offset, order, minimal, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) getGroupsWithNames(names []string) ([]Group, error) {
|
||||
return sqlCommonGetGroupsWithNames(names, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) getUsersInGroups(names []string) ([]string, error) {
|
||||
return sqlCommonGetUsersInGroups(names, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) groupExists(name string) (Group, error) {
|
||||
return sqlCommonGetGroupByName(name, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) addGroup(group *Group) error {
|
||||
return sqlCommonAddGroup(group, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) updateGroup(group *Group) error {
|
||||
return sqlCommonUpdateGroup(group, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) deleteGroup(group Group) error {
|
||||
return sqlCommonDeleteGroup(group, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) dumpGroups() ([]Group, error) {
|
||||
return sqlCommonDumpGroups(p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) adminExists(username string) (Admin, error) {
|
||||
return sqlCommonGetAdminByUsername(username, p.dbHandle)
|
||||
}
|
||||
|
@ -313,7 +396,7 @@ func (p *MySQLProvider) updateAdmin(admin *Admin) error {
|
|||
return sqlCommonUpdateAdmin(admin, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) deleteAdmin(admin *Admin) error {
|
||||
func (p *MySQLProvider) deleteAdmin(admin Admin) error {
|
||||
return sqlCommonDeleteAdmin(admin, p.dbHandle)
|
||||
}
|
||||
|
||||
|
@ -341,7 +424,7 @@ func (p *MySQLProvider) updateAPIKey(apiKey *APIKey) error {
|
|||
return sqlCommonUpdateAPIKey(apiKey, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) deleteAPIKey(apiKey *APIKey) error {
|
||||
func (p *MySQLProvider) deleteAPIKey(apiKey APIKey) error {
|
||||
return sqlCommonDeleteAPIKey(apiKey, p.dbHandle)
|
||||
}
|
||||
|
||||
|
@ -369,7 +452,7 @@ func (p *MySQLProvider) updateShare(share *Share) error {
|
|||
return sqlCommonUpdateShare(share, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) deleteShare(share *Share) error {
|
||||
func (p *MySQLProvider) deleteShare(share Share) error {
|
||||
return sqlCommonDeleteShare(share, p.dbHandle)
|
||||
}
|
||||
|
||||
|
@ -487,6 +570,8 @@ func (p *MySQLProvider) migrateDatabase() error {
|
|||
return err
|
||||
case version == 15:
|
||||
return updateMySQLDatabaseFromV15(p.dbHandle)
|
||||
case version == 16:
|
||||
return updateMySQLDatabaseFromV16(p.dbHandle)
|
||||
default:
|
||||
if version > sqlDatabaseVersion {
|
||||
providerLog(logger.LevelError, "database version %v is newer than the supported one: %v", version,
|
||||
|
@ -511,27 +596,34 @@ func (p *MySQLProvider) revertDatabase(targetVersion int) error {
|
|||
switch dbVersion.Version {
|
||||
case 16:
|
||||
return downgradeMySQLDatabaseFromV16(p.dbHandle)
|
||||
case 17:
|
||||
return downgradeMySQLDatabaseFromV17(p.dbHandle)
|
||||
default:
|
||||
return fmt.Errorf("database version not handled: %v", dbVersion.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) resetDatabase() error {
|
||||
sql := strings.ReplaceAll(mysqlResetSQL, "{{schema_version}}", sqlTableSchemaVersion)
|
||||
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
|
||||
sql = strings.ReplaceAll(sql, "{{folders}}", sqlTableFolders)
|
||||
sql = strings.ReplaceAll(sql, "{{users}}", sqlTableUsers)
|
||||
sql = strings.ReplaceAll(sql, "{{folders_mapping}}", sqlTableFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{api_keys}}", sqlTableAPIKeys)
|
||||
sql = strings.ReplaceAll(sql, "{{shares}}", sqlTableShares)
|
||||
sql = strings.ReplaceAll(sql, "{{defender_events}}", sqlTableDefenderEvents)
|
||||
sql = strings.ReplaceAll(sql, "{{defender_hosts}}", sqlTableDefenderHosts)
|
||||
sql = strings.ReplaceAll(sql, "{{active_transfers}}", sqlTableActiveTransfers)
|
||||
sql := sqlReplaceAll(mysqlResetSQL)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, strings.Split(sql, ";"), 0)
|
||||
}
|
||||
|
||||
func updateMySQLDatabaseFromV15(dbHandle *sql.DB) error {
|
||||
return updateMySQLDatabaseFrom15To16(dbHandle)
|
||||
if err := updateMySQLDatabaseFrom15To16(dbHandle); err != nil {
|
||||
return err
|
||||
}
|
||||
return updateMySQLDatabaseFromV16(dbHandle)
|
||||
}
|
||||
|
||||
func updateMySQLDatabaseFromV16(dbHandle *sql.DB) error {
|
||||
return updateMySQLDatabaseFrom16To17(dbHandle)
|
||||
}
|
||||
|
||||
func downgradeMySQLDatabaseFromV17(dbHandle *sql.DB) error {
|
||||
if err := downgradeMySQLDatabaseFrom17To16(dbHandle); err != nil {
|
||||
return err
|
||||
}
|
||||
return downgradeMySQLDatabaseFromV16(dbHandle)
|
||||
}
|
||||
|
||||
func downgradeMySQLDatabaseFromV16(dbHandle *sql.DB) error {
|
||||
|
@ -547,6 +639,20 @@ func updateMySQLDatabaseFrom15To16(dbHandle *sql.DB) error {
|
|||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 16)
|
||||
}
|
||||
|
||||
func updateMySQLDatabaseFrom16To17(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database version: 16 -> 17")
|
||||
providerLog(logger.LevelInfo, "updating database version: 16 -> 17")
|
||||
sql := strings.ReplaceAll(mysqlV17SQL, "{{users}}", sqlTableUsers)
|
||||
sql = strings.ReplaceAll(sql, "{{groups}}", sqlTableGroups)
|
||||
sql = strings.ReplaceAll(sql, "{{folders}}", sqlTableFolders)
|
||||
sql = strings.ReplaceAll(sql, "{{folders_mapping}}", sqlTableFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{users_folders_mapping}}", sqlTableUsersFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{users_groups_mapping}}", sqlTableUsersGroupsMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{groups_folders_mapping}}", sqlTableGroupsFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 17)
|
||||
}
|
||||
|
||||
func downgradeMySQLDatabaseFrom16To15(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 16 -> 15")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 16 -> 15")
|
||||
|
@ -554,3 +660,17 @@ func downgradeMySQLDatabaseFrom16To15(dbHandle *sql.DB) error {
|
|||
sql = strings.ReplaceAll(sql, "{{active_transfers}}", sqlTableActiveTransfers)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 15)
|
||||
}
|
||||
|
||||
func downgradeMySQLDatabaseFrom17To16(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 17 -> 16")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 17 -> 16")
|
||||
sql := strings.ReplaceAll(mysqlV17DownSQL, "{{users}}", sqlTableUsers)
|
||||
sql = strings.ReplaceAll(sql, "{{groups}}", sqlTableGroups)
|
||||
sql = strings.ReplaceAll(sql, "{{folders}}", sqlTableFolders)
|
||||
sql = strings.ReplaceAll(sql, "{{folders_mapping}}", sqlTableFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{users_folders_mapping}}", sqlTableUsersFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{users_groups_mapping}}", sqlTableUsersGroupsMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{groups_folders_mapping}}", sqlTableGroupsFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 16)
|
||||
}
|
||||
|
|
|
@ -23,10 +23,14 @@ import (
|
|||
const (
|
||||
pgsqlResetSQL = `DROP TABLE IF EXISTS "{{api_keys}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{folders_mapping}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{users_folders_mapping}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{users_groups_mapping}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{groups_folders_mapping}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{admins}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{folders}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{shares}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{users}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{groups}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{defender_events}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{defender_hosts}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{active_transfers}}" CASCADE;
|
||||
|
@ -113,6 +117,46 @@ ALTER TABLE "{{users}}" DROP COLUMN "upload_data_transfer" CASCADE;
|
|||
ALTER TABLE "{{users}}" DROP COLUMN "total_data_transfer" CASCADE;
|
||||
ALTER TABLE "{{users}}" DROP COLUMN "download_data_transfer" CASCADE;
|
||||
DROP TABLE "{{active_transfers}}" CASCADE;
|
||||
`
|
||||
pgsqlV17SQL = `CREATE TABLE "{{groups}}" ("id" serial NOT NULL PRIMARY KEY, "name" varchar(255) NOT NULL UNIQUE,
|
||||
"description" varchar(512) NULL, "created_at" bigint NOT NULL, "updated_at" bigint NOT NULL, "user_settings" text NULL);
|
||||
CREATE TABLE "{{groups_folders_mapping}}" ("id" serial NOT NULL PRIMARY KEY, "group_id" integer NOT NULL,
|
||||
"folder_id" integer NOT NULL, "virtual_path" text NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL);
|
||||
CREATE TABLE "{{users_groups_mapping}}" ("id" serial NOT NULL PRIMARY KEY, "user_id" integer NOT NULL,
|
||||
"group_id" integer NOT NULL, "group_type" integer NOT NULL);
|
||||
DROP INDEX "{{prefix}}folders_mapping_folder_id_idx";
|
||||
DROP INDEX "{{prefix}}folders_mapping_user_id_idx";
|
||||
ALTER TABLE "{{folders_mapping}}" DROP CONSTRAINT "{{prefix}}unique_mapping";
|
||||
ALTER TABLE "{{folders_mapping}}" RENAME TO "{{users_folders_mapping}}";
|
||||
ALTER TABLE "{{users_folders_mapping}}" ADD CONSTRAINT "{{prefix}}unique_user_folder_mapping" UNIQUE ("user_id", "folder_id");
|
||||
CREATE INDEX "{{prefix}}users_folders_mapping_folder_id_idx" ON "{{users_folders_mapping}}" ("folder_id");
|
||||
CREATE INDEX "{{prefix}}users_folders_mapping_user_id_idx" ON "{{users_folders_mapping}}" ("user_id");
|
||||
ALTER TABLE "{{users_groups_mapping}}" ADD CONSTRAINT "{{prefix}}unique_user_group_mapping" UNIQUE ("user_id", "group_id");
|
||||
ALTER TABLE "{{groups_folders_mapping}}" ADD CONSTRAINT "{{prefix}}unique_group_folder_mapping" UNIQUE ("group_id", "folder_id");
|
||||
CREATE INDEX "{{prefix}}users_groups_mapping_group_id_idx" ON "{{users_groups_mapping}}" ("group_id");
|
||||
ALTER TABLE "{{users_groups_mapping}}" ADD CONSTRAINT "{{prefix}}users_groups_mapping_group_id_fk_groups_id"
|
||||
FOREIGN KEY ("group_id") REFERENCES "{{groups}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||
CREATE INDEX "{{prefix}}users_groups_mapping_user_id_idx" ON "{{users_groups_mapping}}" ("user_id");
|
||||
ALTER TABLE "{{users_groups_mapping}}" ADD CONSTRAINT "{{prefix}}users_groups_mapping_user_id_fk_users_id"
|
||||
FOREIGN KEY ("user_id") REFERENCES "{{users}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;
|
||||
CREATE INDEX "{{prefix}}groups_folders_mapping_folder_id_idx" ON "{{groups_folders_mapping}}" ("folder_id");
|
||||
ALTER TABLE "{{groups_folders_mapping}}" ADD CONSTRAINT "{{prefix}}groups_folders_mapping_folder_id_fk_folders_id"
|
||||
FOREIGN KEY ("folder_id") REFERENCES "{{folders}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;
|
||||
CREATE INDEX "{{prefix}}groups_folders_mapping_group_id_idx" ON "{{groups_folders_mapping}}" ("group_id");
|
||||
ALTER TABLE "{{groups_folders_mapping}}" ADD CONSTRAINT "{{prefix}}groups_folders_mapping_group_id_fk_groups_id"
|
||||
FOREIGN KEY ("group_id") REFERENCES "groups" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;
|
||||
CREATE INDEX "{{prefix}}groups_updated_at_idx" ON "{{groups}}" ("updated_at");
|
||||
`
|
||||
pgsqlV17DownSQL = `DROP TABLE "{{users_groups_mapping}}" CASCADE;
|
||||
DROP TABLE "{{groups_folders_mapping}}" CASCADE;
|
||||
DROP TABLE "{{groups}}" CASCADE;
|
||||
DROP INDEX "{{prefix}}users_folders_mapping_folder_id_idx";
|
||||
DROP INDEX "{{prefix}}users_folders_mapping_user_id_idx";
|
||||
ALTER TABLE "{{users_folders_mapping}}" DROP CONSTRAINT "{{prefix}}unique_user_folder_mapping";
|
||||
ALTER TABLE "{{users_folders_mapping}}" RENAME TO "{{folders_mapping}}";
|
||||
ALTER TABLE "{{folders_mapping}}" ADD CONSTRAINT "{{prefix}}unique_mapping" UNIQUE ("user_id", "folder_id");
|
||||
CREATE INDEX "{{prefix}}folders_mapping_folder_id_idx" ON "{{folders_mapping}}" ("folder_id");
|
||||
CREATE INDEX "{{prefix}}folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id");
|
||||
`
|
||||
)
|
||||
|
||||
|
@ -219,7 +263,7 @@ func (p *PGSQLProvider) updateUser(user *User) error {
|
|||
return sqlCommonUpdateUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) deleteUser(user *User) error {
|
||||
func (p *PGSQLProvider) deleteUser(user User) error {
|
||||
return sqlCommonDeleteUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
|
@ -247,8 +291,8 @@ func (p *PGSQLProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
|
|||
return sqlCommonDumpFolders(p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) getFolders(limit, offset int, order string) ([]vfs.BaseVirtualFolder, error) {
|
||||
return sqlCommonGetFolders(limit, offset, order, p.dbHandle)
|
||||
func (p *PGSQLProvider) getFolders(limit, offset int, order string, minimal bool) ([]vfs.BaseVirtualFolder, error) {
|
||||
return sqlCommonGetFolders(limit, offset, order, minimal, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, error) {
|
||||
|
@ -265,7 +309,7 @@ func (p *PGSQLProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
|
|||
return sqlCommonUpdateFolder(folder, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
func (p *PGSQLProvider) deleteFolder(folder vfs.BaseVirtualFolder) error {
|
||||
return sqlCommonDeleteFolder(folder, p.dbHandle)
|
||||
}
|
||||
|
||||
|
@ -277,6 +321,38 @@ func (p *PGSQLProvider) getUsedFolderQuota(name string) (int, int64, error) {
|
|||
return sqlCommonGetFolderUsedQuota(name, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) getGroups(limit, offset int, order string, minimal bool) ([]Group, error) {
|
||||
return sqlCommonGetGroups(limit, offset, order, minimal, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) getGroupsWithNames(names []string) ([]Group, error) {
|
||||
return sqlCommonGetGroupsWithNames(names, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) getUsersInGroups(names []string) ([]string, error) {
|
||||
return sqlCommonGetUsersInGroups(names, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) groupExists(name string) (Group, error) {
|
||||
return sqlCommonGetGroupByName(name, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) addGroup(group *Group) error {
|
||||
return sqlCommonAddGroup(group, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) updateGroup(group *Group) error {
|
||||
return sqlCommonUpdateGroup(group, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) deleteGroup(group Group) error {
|
||||
return sqlCommonDeleteGroup(group, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) dumpGroups() ([]Group, error) {
|
||||
return sqlCommonDumpGroups(p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) adminExists(username string) (Admin, error) {
|
||||
return sqlCommonGetAdminByUsername(username, p.dbHandle)
|
||||
}
|
||||
|
@ -289,7 +365,7 @@ func (p *PGSQLProvider) updateAdmin(admin *Admin) error {
|
|||
return sqlCommonUpdateAdmin(admin, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) deleteAdmin(admin *Admin) error {
|
||||
func (p *PGSQLProvider) deleteAdmin(admin Admin) error {
|
||||
return sqlCommonDeleteAdmin(admin, p.dbHandle)
|
||||
}
|
||||
|
||||
|
@ -317,7 +393,7 @@ func (p *PGSQLProvider) updateAPIKey(apiKey *APIKey) error {
|
|||
return sqlCommonUpdateAPIKey(apiKey, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) deleteAPIKey(apiKey *APIKey) error {
|
||||
func (p *PGSQLProvider) deleteAPIKey(apiKey APIKey) error {
|
||||
return sqlCommonDeleteAPIKey(apiKey, p.dbHandle)
|
||||
}
|
||||
|
||||
|
@ -345,7 +421,7 @@ func (p *PGSQLProvider) updateShare(share *Share) error {
|
|||
return sqlCommonUpdateShare(share, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) deleteShare(share *Share) error {
|
||||
func (p *PGSQLProvider) deleteShare(share Share) error {
|
||||
return sqlCommonDeleteShare(share, p.dbHandle)
|
||||
}
|
||||
|
||||
|
@ -469,6 +545,8 @@ func (p *PGSQLProvider) migrateDatabase() error {
|
|||
return err
|
||||
case version == 15:
|
||||
return updatePGSQLDatabaseFromV15(p.dbHandle)
|
||||
case version == 16:
|
||||
return updatePGSQLDatabaseFromV16(p.dbHandle)
|
||||
default:
|
||||
if version > sqlDatabaseVersion {
|
||||
providerLog(logger.LevelError, "database version %v is newer than the supported one: %v", version,
|
||||
|
@ -493,27 +571,34 @@ func (p *PGSQLProvider) revertDatabase(targetVersion int) error {
|
|||
switch dbVersion.Version {
|
||||
case 16:
|
||||
return downgradePGSQLDatabaseFromV16(p.dbHandle)
|
||||
case 17:
|
||||
return downgradePGSQLDatabaseFromV17(p.dbHandle)
|
||||
default:
|
||||
return fmt.Errorf("database version not handled: %v", dbVersion.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) resetDatabase() error {
|
||||
sql := strings.ReplaceAll(pgsqlResetSQL, "{{schema_version}}", sqlTableSchemaVersion)
|
||||
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
|
||||
sql = strings.ReplaceAll(sql, "{{folders}}", sqlTableFolders)
|
||||
sql = strings.ReplaceAll(sql, "{{users}}", sqlTableUsers)
|
||||
sql = strings.ReplaceAll(sql, "{{folders_mapping}}", sqlTableFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{api_keys}}", sqlTableAPIKeys)
|
||||
sql = strings.ReplaceAll(sql, "{{shares}}", sqlTableShares)
|
||||
sql = strings.ReplaceAll(sql, "{{defender_events}}", sqlTableDefenderEvents)
|
||||
sql = strings.ReplaceAll(sql, "{{defender_hosts}}", sqlTableDefenderHosts)
|
||||
sql = strings.ReplaceAll(sql, "{{active_transfers}}", sqlTableActiveTransfers)
|
||||
sql := sqlReplaceAll(pgsqlResetSQL)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 0)
|
||||
}
|
||||
|
||||
func updatePGSQLDatabaseFromV15(dbHandle *sql.DB) error {
|
||||
return updatePGSQLDatabaseFrom15To16(dbHandle)
|
||||
if err := updatePGSQLDatabaseFrom15To16(dbHandle); err != nil {
|
||||
return err
|
||||
}
|
||||
return updatePGSQLDatabaseFromV16(dbHandle)
|
||||
}
|
||||
|
||||
func updatePGSQLDatabaseFromV16(dbHandle *sql.DB) error {
|
||||
return updatePGSQLDatabaseFrom16To17(dbHandle)
|
||||
}
|
||||
|
||||
func downgradePGSQLDatabaseFromV17(dbHandle *sql.DB) error {
|
||||
if err := downgradePGSQLDatabaseFrom17To16(dbHandle); err != nil {
|
||||
return err
|
||||
}
|
||||
return downgradePGSQLDatabaseFromV16(dbHandle)
|
||||
}
|
||||
|
||||
func downgradePGSQLDatabaseFromV16(dbHandle *sql.DB) error {
|
||||
|
@ -529,6 +614,25 @@ func updatePGSQLDatabaseFrom15To16(dbHandle *sql.DB) error {
|
|||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 16)
|
||||
}
|
||||
|
||||
func updatePGSQLDatabaseFrom16To17(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database version: 16 -> 17")
|
||||
providerLog(logger.LevelInfo, "updating database version: 16 -> 17")
|
||||
sql := pgsqlV17SQL
|
||||
if config.Driver == CockroachDataProviderName {
|
||||
sql = strings.ReplaceAll(sql, `ALTER TABLE "{{folders_mapping}}" DROP CONSTRAINT "{{prefix}}unique_mapping";`,
|
||||
`DROP INDEX "{{prefix}}unique_mapping" CASCADE;`)
|
||||
}
|
||||
sql = strings.ReplaceAll(sql, "{{groups}}", sqlTableGroups)
|
||||
sql = strings.ReplaceAll(sql, "{{users}}", sqlTableUsers)
|
||||
sql = strings.ReplaceAll(sql, "{{folders}}", sqlTableFolders)
|
||||
sql = strings.ReplaceAll(sql, "{{folders_mapping}}", sqlTableFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{users_folders_mapping}}", sqlTableUsersFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{users_groups_mapping}}", sqlTableUsersGroupsMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{groups_folders_mapping}}", sqlTableGroupsFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 17)
|
||||
}
|
||||
|
||||
func downgradePGSQLDatabaseFrom16To15(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 16 -> 15")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 16 -> 15")
|
||||
|
@ -536,3 +640,22 @@ func downgradePGSQLDatabaseFrom16To15(dbHandle *sql.DB) error {
|
|||
sql = strings.ReplaceAll(sql, "{{active_transfers}}", sqlTableActiveTransfers)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 15)
|
||||
}
|
||||
|
||||
func downgradePGSQLDatabaseFrom17To16(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 17 -> 16")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 17 -> 16")
|
||||
sql := pgsqlV17DownSQL
|
||||
if config.Driver == CockroachDataProviderName {
|
||||
sql = strings.ReplaceAll(sql, `ALTER TABLE "{{users_folders_mapping}}" DROP CONSTRAINT "{{prefix}}unique_user_folder_mapping";`,
|
||||
`DROP INDEX "{{prefix}}unique_user_folder_mapping" CASCADE;`)
|
||||
}
|
||||
sql = strings.ReplaceAll(sql, "{{groups}}", sqlTableGroups)
|
||||
sql = strings.ReplaceAll(sql, "{{users}}", sqlTableUsers)
|
||||
sql = strings.ReplaceAll(sql, "{{folders}}", sqlTableFolders)
|
||||
sql = strings.ReplaceAll(sql, "{{folders_mapping}}", sqlTableFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{users_folders_mapping}}", sqlTableUsersFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{users_groups_mapping}}", sqlTableUsersGroupsMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{groups_folders_mapping}}", sqlTableGroupsFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 16)
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/cockroachdb/cockroach-go/v2/crdb"
|
||||
"github.com/sftpgo/sdk"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/logger"
|
||||
"github.com/drakkan/sftpgo/v2/util"
|
||||
|
@ -18,13 +19,15 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
sqlDatabaseVersion = 16
|
||||
sqlDatabaseVersion = 17
|
||||
defaultSQLQueryTimeout = 10 * time.Second
|
||||
longSQLQueryTimeout = 60 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
errSQLFoldersAssosaction = errors.New("unable to associate virtual folders to user")
|
||||
errSQLFoldersAssociation = errors.New("unable to associate virtual folders to user")
|
||||
errSQLGroupsAssociation = errors.New("unable to associate groups to user")
|
||||
errSQLUsersAssociation = errors.New("unable to associate users to group")
|
||||
errSchemaVersionEmpty = errors.New("we can't determine schema version because the schema_migration table is empty. The SFTPGo database might be corrupted. Consider using the \"resetprovider\" sub-command")
|
||||
)
|
||||
|
||||
|
@ -36,6 +39,25 @@ type sqlScanner interface {
|
|||
Scan(dest ...interface{}) error
|
||||
}
|
||||
|
||||
func sqlReplaceAll(sql string) string {
|
||||
sql = strings.ReplaceAll(sql, "{{schema_version}}", sqlTableSchemaVersion)
|
||||
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
|
||||
sql = strings.ReplaceAll(sql, "{{folders}}", sqlTableFolders)
|
||||
sql = strings.ReplaceAll(sql, "{{users}}", sqlTableUsers)
|
||||
sql = strings.ReplaceAll(sql, "{{groups}}", sqlTableGroups)
|
||||
sql = strings.ReplaceAll(sql, "{{folders_mapping}}", sqlTableFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{users_folders_mapping}}", sqlTableUsersFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{users_groups_mapping}}", sqlTableUsersGroupsMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{groups_folders_mapping}}", sqlTableGroupsFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{api_keys}}", sqlTableAPIKeys)
|
||||
sql = strings.ReplaceAll(sql, "{{shares}}", sqlTableShares)
|
||||
sql = strings.ReplaceAll(sql, "{{defender_events}}", sqlTableDefenderEvents)
|
||||
sql = strings.ReplaceAll(sql, "{{defender_hosts}}", sqlTableDefenderHosts)
|
||||
sql = strings.ReplaceAll(sql, "{{active_transfers}}", sqlTableActiveTransfers)
|
||||
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
|
||||
return sql
|
||||
}
|
||||
|
||||
func sqlCommonGetShareByID(shareID, username string, dbHandle sqlQuerier) (Share, error) {
|
||||
var share Share
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
|
@ -169,7 +191,7 @@ func sqlCommonUpdateShare(share *Share, dbHandle *sql.DB) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func sqlCommonDeleteShare(share *Share, dbHandle *sql.DB) error {
|
||||
func sqlCommonDeleteShare(share Share, dbHandle *sql.DB) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
|
@ -319,7 +341,7 @@ func sqlCommonUpdateAPIKey(apiKey *APIKey, dbHandle *sql.DB) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func sqlCommonDeleteAPIKey(apiKey *APIKey, dbHandle *sql.DB) error {
|
||||
func sqlCommonDeleteAPIKey(apiKey APIKey, dbHandle *sql.DB) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
q := getDeleteAPIKeyQuery()
|
||||
|
@ -499,7 +521,7 @@ func sqlCommonUpdateAdmin(admin *Admin, dbHandle *sql.DB) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func sqlCommonDeleteAdmin(admin *Admin, dbHandle *sql.DB) error {
|
||||
func sqlCommonDeleteAdmin(admin Admin, dbHandle *sql.DB) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
q := getDeleteAdminQuery()
|
||||
|
@ -574,10 +596,264 @@ func sqlCommonDumpAdmins(dbHandle sqlQuerier) ([]Admin, error) {
|
|||
return admins, rows.Err()
|
||||
}
|
||||
|
||||
func sqlCommonGetGroupByName(name string, dbHandle sqlQuerier) (Group, error) {
|
||||
var group Group
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
q := getGroupByNameQuery()
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error preparing database query %#v: %v", q, err)
|
||||
return group, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
row := stmt.QueryRowContext(ctx, name)
|
||||
group, err = getGroupFromDbRow(row)
|
||||
if err != nil {
|
||||
return group, err
|
||||
}
|
||||
group, err = getGroupWithVirtualFolders(ctx, group, dbHandle)
|
||||
if err != nil {
|
||||
return group, err
|
||||
}
|
||||
return getGroupWithUsers(ctx, group, dbHandle)
|
||||
}
|
||||
|
||||
func sqlCommonDumpGroups(dbHandle sqlQuerier) ([]Group, error) {
|
||||
groups := make([]Group, 0, 50)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
q := getDumpGroupsQuery()
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error preparing database query %#v: %v", q, err)
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
rows, err := stmt.QueryContext(ctx)
|
||||
if err != nil {
|
||||
return groups, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
group, err := getGroupFromDbRow(rows)
|
||||
if err != nil {
|
||||
return groups, err
|
||||
}
|
||||
group.PrepareForRendering()
|
||||
groups = append(groups, group)
|
||||
}
|
||||
err = rows.Err()
|
||||
return groups, err
|
||||
}
|
||||
|
||||
func sqlCommonGetUsersInGroups(names []string, dbHandle sqlQuerier) ([]string, error) {
|
||||
if len(names) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
q := getUsersInGroupsQuery(len(names))
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error preparing database query %#v: %v", q, err)
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
args := make([]interface{}, 0, len(names))
|
||||
for _, name := range names {
|
||||
args = append(args, name)
|
||||
}
|
||||
|
||||
usernames := make([]string, 0, len(names))
|
||||
rows, err := stmt.QueryContext(ctx, args...)
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var username string
|
||||
err = rows.Scan(&username)
|
||||
if err != nil {
|
||||
return usernames, err
|
||||
}
|
||||
usernames = append(usernames, username)
|
||||
}
|
||||
}
|
||||
return usernames, rows.Err()
|
||||
}
|
||||
|
||||
func sqlCommonGetGroupsWithNames(names []string, dbHandle sqlQuerier) ([]Group, error) {
|
||||
if len(names) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
q := getGroupsWithNamesQuery(len(names))
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error preparing database query %#v: %v", q, err)
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
args := make([]interface{}, 0, len(names))
|
||||
for _, name := range names {
|
||||
args = append(args, name)
|
||||
}
|
||||
|
||||
groups := make([]Group, 0, len(names))
|
||||
rows, err := stmt.QueryContext(ctx, args...)
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
group, err := getGroupFromDbRow(rows)
|
||||
if err != nil {
|
||||
return groups, err
|
||||
}
|
||||
groups = append(groups, group)
|
||||
}
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return groups, err
|
||||
}
|
||||
return getGroupsWithVirtualFolders(ctx, groups, dbHandle)
|
||||
}
|
||||
|
||||
func sqlCommonGetGroups(limit int, offset int, order string, minimal bool, dbHandle sqlQuerier) ([]Group, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
q := getGroupsQuery(order, minimal)
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error preparing database query %#v: %v", q, err)
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
groups := make([]Group, 0, limit)
|
||||
rows, err := stmt.QueryContext(ctx, limit, offset)
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var group Group
|
||||
if minimal {
|
||||
err = rows.Scan(&group.ID, &group.Name)
|
||||
} else {
|
||||
group, err = getGroupFromDbRow(rows)
|
||||
}
|
||||
if err != nil {
|
||||
return groups, err
|
||||
}
|
||||
groups = append(groups, group)
|
||||
}
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return groups, err
|
||||
}
|
||||
if minimal {
|
||||
return groups, nil
|
||||
}
|
||||
groups, err = getGroupsWithVirtualFolders(ctx, groups, dbHandle)
|
||||
if err != nil {
|
||||
return groups, err
|
||||
}
|
||||
groups, err = getGroupsWithUsers(ctx, groups, dbHandle)
|
||||
if err != nil {
|
||||
return groups, err
|
||||
}
|
||||
for idx := range groups {
|
||||
groups[idx].PrepareForRendering()
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func sqlCommonAddGroup(group *Group, dbHandle *sql.DB) error {
|
||||
if err := group.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
|
||||
q := getAddGroupQuery()
|
||||
stmt, err := tx.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error preparing database query %#v: %v", q, err)
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
settings, err := json.Marshal(group.UserSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = stmt.ExecContext(ctx, group.Name, group.Description, util.GetTimeAsMsSinceEpoch(time.Now()),
|
||||
util.GetTimeAsMsSinceEpoch(time.Now()), string(settings))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return generateGroupVirtualFoldersMapping(ctx, group, tx)
|
||||
})
|
||||
}
|
||||
|
||||
func sqlCommonUpdateGroup(group *Group, dbHandle *sql.DB) error {
|
||||
if err := group.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
|
||||
q := getUpdateGroupQuery()
|
||||
stmt, err := tx.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error preparing database query %#v: %v", q, err)
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
settings, err := json.Marshal(group.UserSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = stmt.ExecContext(ctx, group.Description, settings, util.GetTimeAsMsSinceEpoch(time.Now()), group.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return generateGroupVirtualFoldersMapping(ctx, group, tx)
|
||||
})
|
||||
}
|
||||
|
||||
func sqlCommonDeleteGroup(group Group, dbHandle *sql.DB) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
q := getDeleteGroupQuery()
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error preparing database query %#v: %v", q, err)
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.ExecContext(ctx, group.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
func sqlCommonGetUserByUsername(username string, dbHandle sqlQuerier) (User, error) {
|
||||
var user User
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
q := getUserByUsernameQuery()
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
|
@ -591,7 +867,11 @@ func sqlCommonGetUserByUsername(username string, dbHandle sqlQuerier) (User, err
|
|||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
return getUserWithVirtualFolders(ctx, user, dbHandle)
|
||||
user, err = getUserWithVirtualFolders(ctx, user, dbHandle)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
return getUserWithGroups(ctx, user, dbHandle)
|
||||
}
|
||||
|
||||
func sqlCommonValidateUserAndPass(username, password, ip, protocol string, dbHandle *sql.DB) (User, error) {
|
||||
|
@ -834,7 +1114,10 @@ func sqlCommonAddUser(user *User, dbHandle *sql.DB) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return generateVirtualFoldersMapping(ctx, user, tx)
|
||||
if err := generateUserVirtualFoldersMapping(ctx, user, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
return generateUserGroupMapping(ctx, user, tx)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -893,11 +1176,14 @@ func sqlCommonUpdateUser(user *User, dbHandle *sql.DB) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return generateVirtualFoldersMapping(ctx, user, tx)
|
||||
if err := generateUserVirtualFoldersMapping(ctx, user, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
return generateUserGroupMapping(ctx, user, tx)
|
||||
})
|
||||
}
|
||||
|
||||
func sqlCommonDeleteUser(user *User, dbHandle *sql.DB) error {
|
||||
func sqlCommonDeleteUser(user User, dbHandle *sql.DB) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
q := getDeleteUserQuery()
|
||||
|
@ -943,7 +1229,11 @@ func sqlCommonDumpUsers(dbHandle sqlQuerier) ([]User, error) {
|
|||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
return getUsersWithVirtualFolders(ctx, users, dbHandle)
|
||||
users, err = getUsersWithVirtualFolders(ctx, users, dbHandle)
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
return getUsersWithGroups(ctx, users, dbHandle)
|
||||
}
|
||||
|
||||
func sqlCommonGetRecentlyUpdatedUsers(after int64, dbHandle sqlQuerier) ([]User, error) {
|
||||
|
@ -973,7 +1263,34 @@ func sqlCommonGetRecentlyUpdatedUsers(after int64, dbHandle sqlQuerier) ([]User,
|
|||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
return getUsersWithVirtualFolders(ctx, users, dbHandle)
|
||||
users, err = getUsersWithVirtualFolders(ctx, users, dbHandle)
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
users, err = getUsersWithGroups(ctx, users, dbHandle)
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
var groupNames []string
|
||||
for _, u := range users {
|
||||
for _, g := range u.Groups {
|
||||
groupNames = append(groupNames, g.Name)
|
||||
}
|
||||
}
|
||||
groupNames = util.RemoveDuplicates(groupNames)
|
||||
groups, err := sqlCommonGetGroupsWithNames(groupNames, dbHandle)
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
groupsMapping := make(map[string]Group)
|
||||
for _, g := range groups {
|
||||
groupsMapping[g.Name] = g
|
||||
}
|
||||
for idx := range users {
|
||||
ref := &users[idx]
|
||||
ref.applyGroupSettings(groupsMapping)
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func sqlCommonGetUsersForQuotaCheck(toFetch map[string]bool, dbHandle sqlQuerier) ([]User, error) {
|
||||
|
@ -1021,6 +1338,29 @@ func sqlCommonGetUsersForQuotaCheck(toFetch map[string]bool, dbHandle sqlQuerier
|
|||
return users, err
|
||||
}
|
||||
users = append(users, usersWithFolders...)
|
||||
users, err = getUsersWithGroups(ctx, users, dbHandle)
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
var groupNames []string
|
||||
for _, u := range users {
|
||||
for _, g := range u.Groups {
|
||||
groupNames = append(groupNames, g.Name)
|
||||
}
|
||||
}
|
||||
groupNames = util.RemoveDuplicates(groupNames)
|
||||
groups, err := sqlCommonGetGroupsWithNames(groupNames, dbHandle)
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
groupsMapping := make(map[string]Group)
|
||||
for _, g := range groups {
|
||||
groupsMapping[g.Name] = g
|
||||
}
|
||||
for idx := range users {
|
||||
ref := &users[idx]
|
||||
ref.applyGroupSettings(groupsMapping)
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
|
@ -1188,7 +1528,6 @@ func sqlCommonGetUsers(limit int, offset int, order string, dbHandle sqlQuerier)
|
|||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
u.PrepareForRendering()
|
||||
users = append(users, u)
|
||||
}
|
||||
}
|
||||
|
@ -1196,7 +1535,18 @@ func sqlCommonGetUsers(limit int, offset int, order string, dbHandle sqlQuerier)
|
|||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
return getUsersWithVirtualFolders(ctx, users, dbHandle)
|
||||
users, err = getUsersWithVirtualFolders(ctx, users, dbHandle)
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
users, err = getUsersWithGroups(ctx, users, dbHandle)
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
for idx := range users {
|
||||
users[idx].PrepareForRendering()
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func sqlCommonGetDefenderHosts(from int64, limit int, dbHandle sqlQuerier) ([]DefenderEntry, error) {
|
||||
|
@ -1572,6 +1922,31 @@ func getAdminFromDbRow(row sqlScanner) (Admin, error) {
|
|||
return admin, nil
|
||||
}
|
||||
|
||||
func getGroupFromDbRow(row sqlScanner) (Group, error) {
|
||||
var group Group
|
||||
var userSettings, description sql.NullString
|
||||
|
||||
err := row.Scan(&group.ID, &group.Name, &description, &group.CreatedAt, &group.UpdatedAt, &userSettings)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return group, util.NewRecordNotFoundError(err.Error())
|
||||
}
|
||||
return group, err
|
||||
}
|
||||
if description.Valid {
|
||||
group.Description = description.String
|
||||
}
|
||||
if userSettings.Valid {
|
||||
var settings GroupUserSettings
|
||||
err = json.Unmarshal([]byte(userSettings.String), &settings)
|
||||
if err == nil {
|
||||
group.UserSettings = settings
|
||||
}
|
||||
}
|
||||
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func getUserFromDbRow(row sqlScanner) (User, error) {
|
||||
var user User
|
||||
var permissions sql.NullString
|
||||
|
@ -1701,6 +2076,13 @@ func sqlCommonGetFolderByName(ctx context.Context, name string, dbHandle sqlQuer
|
|||
if len(folders) != 1 {
|
||||
return folder, fmt.Errorf("unable to associate users with folder %#v", name)
|
||||
}
|
||||
folders, err = getVirtualFoldersWithGroups([]vfs.BaseVirtualFolder{folders[0]}, dbHandle)
|
||||
if err != nil {
|
||||
return folder, err
|
||||
}
|
||||
if len(folders) != 1 {
|
||||
return folder, fmt.Errorf("unable to associate groups with folder %#v", name)
|
||||
}
|
||||
return folders[0], nil
|
||||
}
|
||||
|
||||
|
@ -1775,7 +2157,7 @@ func sqlCommonUpdateFolder(folder *vfs.BaseVirtualFolder, dbHandle sqlQuerier) e
|
|||
return err
|
||||
}
|
||||
|
||||
func sqlCommonDeleteFolder(folder *vfs.BaseVirtualFolder, dbHandle sqlQuerier) error {
|
||||
func sqlCommonDeleteFolder(folder vfs.BaseVirtualFolder, dbHandle sqlQuerier) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
q := getDeleteFolderQuery()
|
||||
|
@ -1829,17 +2211,14 @@ func sqlCommonDumpFolders(dbHandle sqlQuerier) ([]vfs.BaseVirtualFolder, error)
|
|||
folders = append(folders, folder)
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return folders, err
|
||||
}
|
||||
return getVirtualFoldersWithUsers(folders, dbHandle)
|
||||
return folders, err
|
||||
}
|
||||
|
||||
func sqlCommonGetFolders(limit, offset int, order string, dbHandle sqlQuerier) ([]vfs.BaseVirtualFolder, error) {
|
||||
func sqlCommonGetFolders(limit, offset int, order string, minimal bool, dbHandle sqlQuerier) ([]vfs.BaseVirtualFolder, error) {
|
||||
folders := make([]vfs.BaseVirtualFolder, 0, limit)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
q := getFoldersQuery(order)
|
||||
q := getFoldersQuery(order, minimal)
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error preparing database query %#v: %v", q, err)
|
||||
|
@ -1854,23 +2233,30 @@ func sqlCommonGetFolders(limit, offset int, order string, dbHandle sqlQuerier) (
|
|||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var folder vfs.BaseVirtualFolder
|
||||
var mappedPath, description, fsConfig sql.NullString
|
||||
err = rows.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles,
|
||||
&folder.LastQuotaUpdate, &folder.Name, &description, &fsConfig)
|
||||
if err != nil {
|
||||
return folders, err
|
||||
}
|
||||
if mappedPath.Valid {
|
||||
folder.MappedPath = mappedPath.String
|
||||
}
|
||||
if description.Valid {
|
||||
folder.Description = description.String
|
||||
}
|
||||
if fsConfig.Valid {
|
||||
var fs vfs.Filesystem
|
||||
err = json.Unmarshal([]byte(fsConfig.String), &fs)
|
||||
if err == nil {
|
||||
folder.FsConfig = fs
|
||||
if minimal {
|
||||
err = rows.Scan(&folder.ID, &folder.Name)
|
||||
if err != nil {
|
||||
return folders, err
|
||||
}
|
||||
} else {
|
||||
var mappedPath, description, fsConfig sql.NullString
|
||||
err = rows.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles,
|
||||
&folder.LastQuotaUpdate, &folder.Name, &description, &fsConfig)
|
||||
if err != nil {
|
||||
return folders, err
|
||||
}
|
||||
if mappedPath.Valid {
|
||||
folder.MappedPath = mappedPath.String
|
||||
}
|
||||
if description.Valid {
|
||||
folder.Description = description.String
|
||||
}
|
||||
if fsConfig.Valid {
|
||||
var fs vfs.Filesystem
|
||||
err = json.Unmarshal([]byte(fsConfig.String), &fs)
|
||||
if err == nil {
|
||||
folder.FsConfig = fs
|
||||
}
|
||||
}
|
||||
}
|
||||
folder.PrepareForRendering()
|
||||
|
@ -1881,11 +2267,18 @@ func sqlCommonGetFolders(limit, offset int, order string, dbHandle sqlQuerier) (
|
|||
if err != nil {
|
||||
return folders, err
|
||||
}
|
||||
return getVirtualFoldersWithUsers(folders, dbHandle)
|
||||
if minimal {
|
||||
return folders, nil
|
||||
}
|
||||
folders, err = getVirtualFoldersWithUsers(folders, dbHandle)
|
||||
if err != nil {
|
||||
return folders, err
|
||||
}
|
||||
return getVirtualFoldersWithGroups(folders, dbHandle)
|
||||
}
|
||||
|
||||
func sqlCommonClearFolderMapping(ctx context.Context, user *User, dbHandle sqlQuerier) error {
|
||||
q := getClearFolderMappingQuery()
|
||||
func sqlCommonClearUserFolderMapping(ctx context.Context, user *User, dbHandle sqlQuerier) error {
|
||||
q := getClearUserFolderMappingQuery()
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error preparing database query %#v: %v", q, err)
|
||||
|
@ -1896,8 +2289,32 @@ func sqlCommonClearFolderMapping(ctx context.Context, user *User, dbHandle sqlQu
|
|||
return err
|
||||
}
|
||||
|
||||
func sqlCommonAddFolderMapping(ctx context.Context, user *User, folder *vfs.VirtualFolder, dbHandle sqlQuerier) error {
|
||||
q := getAddFolderMappingQuery()
|
||||
func sqlCommonClearGroupFolderMapping(ctx context.Context, group *Group, dbHandle sqlQuerier) error {
|
||||
q := getClearGroupFolderMappingQuery()
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error preparing database query %#v: %v", q, err)
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.ExecContext(ctx, group.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
func sqlCommonClearUserGroupMapping(ctx context.Context, user *User, dbHandle sqlQuerier) error {
|
||||
q := getClearUserGroupMappingQuery()
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error preparing database query %#v: %v", q, err)
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.ExecContext(ctx, user.Username)
|
||||
return err
|
||||
}
|
||||
|
||||
func sqlCommonAddUserFolderMapping(ctx context.Context, user *User, folder *vfs.VirtualFolder, dbHandle sqlQuerier) error {
|
||||
q := getAddUserFolderMappingQuery()
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error preparing database query %#v: %v", q, err)
|
||||
|
@ -1908,8 +2325,52 @@ func sqlCommonAddFolderMapping(ctx context.Context, user *User, folder *vfs.Virt
|
|||
return err
|
||||
}
|
||||
|
||||
func generateVirtualFoldersMapping(ctx context.Context, user *User, dbHandle sqlQuerier) error {
|
||||
err := sqlCommonClearFolderMapping(ctx, user, dbHandle)
|
||||
func sqlCommonAddGroupFolderMapping(ctx context.Context, group *Group, folder *vfs.VirtualFolder, dbHandle sqlQuerier) error {
|
||||
q := getAddGroupFolderMappingQuery()
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error preparing database query %#v: %v", q, err)
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.ExecContext(ctx, folder.VirtualPath, folder.QuotaSize, folder.QuotaFiles, folder.Name, group.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
func sqlCommonAddUserGroupMapping(ctx context.Context, username, groupName string, groupType int, dbHandle sqlQuerier) error {
|
||||
q := getAddUserGroupMappingQuery()
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error preparing database query %#v: %v", q, err)
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.ExecContext(ctx, username, groupName, groupType)
|
||||
return err
|
||||
}
|
||||
|
||||
func generateGroupVirtualFoldersMapping(ctx context.Context, group *Group, dbHandle sqlQuerier) error {
|
||||
err := sqlCommonClearGroupFolderMapping(ctx, group, dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for idx := range group.VirtualFolders {
|
||||
vfolder := &group.VirtualFolders[idx]
|
||||
f, err := sqlCommonAddOrUpdateFolder(ctx, &vfolder.BaseVirtualFolder, 0, 0, 0, dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vfolder.BaseVirtualFolder = f
|
||||
err = sqlCommonAddGroupFolderMapping(ctx, group, vfolder, dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func generateUserVirtualFoldersMapping(ctx context.Context, user *User, dbHandle sqlQuerier) error {
|
||||
err := sqlCommonClearUserFolderMapping(ctx, user, dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1920,7 +2381,7 @@ func generateVirtualFoldersMapping(ctx context.Context, user *User, dbHandle sql
|
|||
return err
|
||||
}
|
||||
vfolder.BaseVirtualFolder = f
|
||||
err = sqlCommonAddFolderMapping(ctx, user, vfolder, dbHandle)
|
||||
err = sqlCommonAddUserFolderMapping(ctx, user, vfolder, dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1928,15 +2389,18 @@ func generateVirtualFoldersMapping(ctx context.Context, user *User, dbHandle sql
|
|||
return err
|
||||
}
|
||||
|
||||
func getUserWithVirtualFolders(ctx context.Context, user User, dbHandle sqlQuerier) (User, error) {
|
||||
users, err := getUsersWithVirtualFolders(ctx, []User{user}, dbHandle)
|
||||
func generateUserGroupMapping(ctx context.Context, user *User, dbHandle sqlQuerier) error {
|
||||
err := sqlCommonClearUserGroupMapping(ctx, user, dbHandle)
|
||||
if err != nil {
|
||||
return user, err
|
||||
return err
|
||||
}
|
||||
if len(users) == 0 {
|
||||
return user, errSQLFoldersAssosaction
|
||||
for _, group := range user.Groups {
|
||||
err = sqlCommonAddUserGroupMapping(ctx, user.Username, group.Name, group.Type, dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return users[0], err
|
||||
return err
|
||||
}
|
||||
|
||||
func getDefenderHostsWithScores(ctx context.Context, hosts []DefenderEntry, from int64, idForScores []int64,
|
||||
|
@ -1994,6 +2458,17 @@ func getDefenderHostsWithScores(ctx context.Context, hosts []DefenderEntry, from
|
|||
return result, nil
|
||||
}
|
||||
|
||||
func getUserWithVirtualFolders(ctx context.Context, user User, dbHandle sqlQuerier) (User, error) {
|
||||
users, err := getUsersWithVirtualFolders(ctx, []User{user}, dbHandle)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
if len(users) == 0 {
|
||||
return user, errSQLFoldersAssociation
|
||||
}
|
||||
return users[0], err
|
||||
}
|
||||
|
||||
func getUsersWithVirtualFolders(ctx context.Context, users []User, dbHandle sqlQuerier) ([]User, error) {
|
||||
if len(users) == 0 {
|
||||
return users, nil
|
||||
|
@ -2052,13 +2527,232 @@ func getUsersWithVirtualFolders(ctx context.Context, users []User, dbHandle sqlQ
|
|||
return users, err
|
||||
}
|
||||
|
||||
func getUserWithGroups(ctx context.Context, user User, dbHandle sqlQuerier) (User, error) {
|
||||
users, err := getUsersWithGroups(ctx, []User{user}, dbHandle)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
if len(users) == 0 {
|
||||
return user, errSQLGroupsAssociation
|
||||
}
|
||||
return users[0], err
|
||||
}
|
||||
|
||||
func getUsersWithGroups(ctx context.Context, users []User, dbHandle sqlQuerier) ([]User, error) {
|
||||
if len(users) == 0 {
|
||||
return users, nil
|
||||
}
|
||||
var err error
|
||||
usersGroups := make(map[int64][]sdk.GroupMapping)
|
||||
q := getRelatedGroupsForUsersQuery(users)
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error preparing database query %#v: %v", q, err)
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
rows, err := stmt.QueryContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var group sdk.GroupMapping
|
||||
var userID int64
|
||||
err = rows.Scan(&group.Name, &group.Type, &userID)
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
usersGroups[userID] = append(usersGroups[userID], group)
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
if len(usersGroups) == 0 {
|
||||
return users, err
|
||||
}
|
||||
for idx := range users {
|
||||
ref := &users[idx]
|
||||
ref.Groups = usersGroups[ref.ID]
|
||||
}
|
||||
return users, err
|
||||
}
|
||||
|
||||
func getGroupWithUsers(ctx context.Context, group Group, dbHandle sqlQuerier) (Group, error) {
|
||||
groups, err := getGroupsWithUsers(ctx, []Group{group}, dbHandle)
|
||||
if err != nil {
|
||||
return group, err
|
||||
}
|
||||
if len(groups) == 0 {
|
||||
return group, errSQLUsersAssociation
|
||||
}
|
||||
return groups[0], err
|
||||
}
|
||||
|
||||
func getGroupWithVirtualFolders(ctx context.Context, group Group, dbHandle sqlQuerier) (Group, error) {
|
||||
groups, err := getGroupsWithVirtualFolders(ctx, []Group{group}, dbHandle)
|
||||
if err != nil {
|
||||
return group, err
|
||||
}
|
||||
if len(groups) == 0 {
|
||||
return group, errSQLFoldersAssociation
|
||||
}
|
||||
return groups[0], err
|
||||
}
|
||||
|
||||
func getGroupsWithVirtualFolders(ctx context.Context, groups []Group, dbHandle sqlQuerier) ([]Group, error) {
|
||||
if len(groups) == 0 {
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
q := getRelatedFoldersForGroupsQuery(groups)
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error preparing database query %#v: %v", q, err)
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
rows, err := stmt.QueryContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
groupsVirtualFolders := make(map[int64][]vfs.VirtualFolder)
|
||||
|
||||
for rows.Next() {
|
||||
var groupID int64
|
||||
var folder vfs.VirtualFolder
|
||||
var mappedPath, fsConfig, description sql.NullString
|
||||
err = rows.Scan(&folder.ID, &folder.Name, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles,
|
||||
&folder.LastQuotaUpdate, &folder.VirtualPath, &folder.QuotaSize, &folder.QuotaFiles, &groupID, &fsConfig,
|
||||
&description)
|
||||
if err != nil {
|
||||
return groups, err
|
||||
}
|
||||
if mappedPath.Valid {
|
||||
folder.MappedPath = mappedPath.String
|
||||
}
|
||||
if description.Valid {
|
||||
folder.Description = description.String
|
||||
}
|
||||
if fsConfig.Valid {
|
||||
var fs vfs.Filesystem
|
||||
err = json.Unmarshal([]byte(fsConfig.String), &fs)
|
||||
if err == nil {
|
||||
folder.FsConfig = fs
|
||||
}
|
||||
}
|
||||
groupsVirtualFolders[groupID] = append(groupsVirtualFolders[groupID], folder)
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return groups, err
|
||||
}
|
||||
if len(groupsVirtualFolders) == 0 {
|
||||
return groups, err
|
||||
}
|
||||
for idx := range groups {
|
||||
ref := &groups[idx]
|
||||
ref.VirtualFolders = groupsVirtualFolders[ref.ID]
|
||||
}
|
||||
return groups, err
|
||||
}
|
||||
|
||||
func getGroupsWithUsers(ctx context.Context, groups []Group, dbHandle sqlQuerier) ([]Group, error) {
|
||||
if len(groups) == 0 {
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
q := getRelatedUsersForGroupsQuery(groups)
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error preparing database query %#v: %v", q, err)
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
rows, err := stmt.QueryContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
groupsUsers := make(map[int64][]string)
|
||||
|
||||
for rows.Next() {
|
||||
var username string
|
||||
var groupID int64
|
||||
err = rows.Scan(&groupID, &username)
|
||||
if err != nil {
|
||||
return groups, err
|
||||
}
|
||||
groupsUsers[groupID] = append(groupsUsers[groupID], username)
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return groups, err
|
||||
}
|
||||
if len(groupsUsers) == 0 {
|
||||
return groups, err
|
||||
}
|
||||
for idx := range groups {
|
||||
ref := &groups[idx]
|
||||
ref.Users = groupsUsers[ref.ID]
|
||||
}
|
||||
return groups, err
|
||||
}
|
||||
|
||||
func getVirtualFoldersWithGroups(folders []vfs.BaseVirtualFolder, dbHandle sqlQuerier) ([]vfs.BaseVirtualFolder, error) {
|
||||
if len(folders) == 0 {
|
||||
return folders, nil
|
||||
}
|
||||
var err error
|
||||
vFoldersGroups := make(map[int64][]string)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
q := getRelatedGroupsForFoldersQuery(folders)
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "error preparing database query %#v: %v", q, err)
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
rows, err := stmt.QueryContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var folderID int64
|
||||
err = rows.Scan(&folderID, &name)
|
||||
if err != nil {
|
||||
return folders, err
|
||||
}
|
||||
vFoldersGroups[folderID] = append(vFoldersGroups[folderID], name)
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return folders, err
|
||||
}
|
||||
if len(vFoldersGroups) == 0 {
|
||||
return folders, err
|
||||
}
|
||||
for idx := range folders {
|
||||
ref := &folders[idx]
|
||||
ref.Groups = vFoldersGroups[ref.ID]
|
||||
}
|
||||
return folders, err
|
||||
}
|
||||
|
||||
func getVirtualFoldersWithUsers(folders []vfs.BaseVirtualFolder, dbHandle sqlQuerier) ([]vfs.BaseVirtualFolder, error) {
|
||||
if len(folders) == 0 {
|
||||
return folders, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
vFoldersUsers := make(map[int64][]string)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
q := getRelatedUsersForFoldersQuery(folders)
|
||||
|
@ -2073,6 +2767,8 @@ func getVirtualFoldersWithUsers(folders []vfs.BaseVirtualFolder, dbHandle sqlQue
|
|||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
vFoldersUsers := make(map[int64][]string)
|
||||
for rows.Next() {
|
||||
var username string
|
||||
var folderID int64
|
||||
|
|
|
@ -25,10 +25,14 @@ import (
|
|||
const (
|
||||
sqliteResetSQL = `DROP TABLE IF EXISTS "{{api_keys}}";
|
||||
DROP TABLE IF EXISTS "{{folders_mapping}}";
|
||||
DROP TABLE IF EXISTS "{{users_folders_mapping}}";
|
||||
DROP TABLE IF EXISTS "{{users_groups_mapping}}";
|
||||
DROP TABLE IF EXISTS "{{groups_folders_mapping}}";
|
||||
DROP TABLE IF EXISTS "{{admins}}";
|
||||
DROP TABLE IF EXISTS "{{folders}}";
|
||||
DROP TABLE IF EXISTS "{{shares}}";
|
||||
DROP TABLE IF EXISTS "{{users}}";
|
||||
DROP TABLE IF EXISTS "{{groups}}";
|
||||
DROP TABLE IF EXISTS "{{defender_events}}";
|
||||
DROP TABLE IF EXISTS "{{defender_hosts}}";
|
||||
DROP TABLE IF EXISTS "{{active_transfers}}";
|
||||
|
@ -101,6 +105,49 @@ ALTER TABLE "{{users}}" DROP COLUMN "upload_data_transfer";
|
|||
ALTER TABLE "{{users}}" DROP COLUMN "total_data_transfer";
|
||||
ALTER TABLE "{{users}}" DROP COLUMN "download_data_transfer";
|
||||
DROP TABLE "{{active_transfers}}";
|
||||
`
|
||||
sqliteV17SQL = `CREATE TABLE "{{groups}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(255) NOT NULL UNIQUE,
|
||||
"description" varchar(512) NULL, "created_at" bigint NOT NULL, "updated_at" bigint NOT NULL, "user_settings" text NULL);
|
||||
CREATE TABLE "{{groups_folders_mapping}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"folder_id" integer NOT NULL REFERENCES "{{folders}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
|
||||
"group_id" integer NOT NULL REFERENCES "{{groups}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
|
||||
"virtual_path" text NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL,
|
||||
CONSTRAINT "{{prefix}}unique_group_folder_mapping" UNIQUE ("group_id", "folder_id"));
|
||||
CREATE TABLE "{{users_groups_mapping}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"user_id" integer NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
|
||||
"group_id" integer NOT NULL REFERENCES "groups" ("id") ON DELETE NO ACTION,
|
||||
"group_type" integer NOT NULL, CONSTRAINT "{{prefix}}unique_user_group_mapping" UNIQUE ("user_id", "group_id"));
|
||||
CREATE TABLE "new__folders_mapping" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"user_id" integer NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
|
||||
"folder_id" integer NOT NULL REFERENCES "folders" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
|
||||
"virtual_path" text NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL,
|
||||
CONSTRAINT "{{prefix}}unique_user_folder_mapping" UNIQUE ("user_id", "folder_id"));
|
||||
INSERT INTO "new__folders_mapping" ("id", "virtual_path", "quota_size", "quota_files", "folder_id", "user_id") SELECT "id",
|
||||
"virtual_path", "quota_size", "quota_files", "folder_id", "user_id" FROM "{{folders_mapping}}";
|
||||
DROP TABLE "{{folders_mapping}}";
|
||||
ALTER TABLE "new__folders_mapping" RENAME TO "{{users_folders_mapping}}";
|
||||
CREATE INDEX "{{prefix}}groups_updated_at_idx" ON "{{groups}}" ("updated_at");
|
||||
CREATE INDEX "{{prefix}}users_folders_mapping_folder_id_idx" ON "{{users_folders_mapping}}" ("folder_id");
|
||||
CREATE INDEX "{{prefix}}users_folders_mapping_user_id_idx" ON "{{users_folders_mapping}}" ("user_id");
|
||||
CREATE INDEX "{{prefix}}users_groups_mapping_group_id_idx" ON "{{users_groups_mapping}}" ("group_id");
|
||||
CREATE INDEX "{{prefix}}users_groups_mapping_user_id_idx" ON "{{users_groups_mapping}}" ("user_id");
|
||||
CREATE INDEX "{{prefix}}groups_folders_mapping_folder_id_idx" ON "{{groups_folders_mapping}}" ("folder_id");
|
||||
CREATE INDEX "{{prefix}}groups_folders_mapping_group_id_idx" ON "{{groups_folders_mapping}}" ("group_id");
|
||||
`
|
||||
sqliteV17DownSQL = `DROP TABLE "{{users_groups_mapping}}";
|
||||
DROP TABLE "{{groups_folders_mapping}}";
|
||||
DROP TABLE "{{groups}}";
|
||||
CREATE TABLE "new__folders_mapping" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"user_id" integer NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
|
||||
"folder_id" integer NOT NULL REFERENCES "folders" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
|
||||
"virtual_path" text NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL,
|
||||
CONSTRAINT "{{prefix}}unique_folder_mapping" UNIQUE ("user_id", "folder_id"));
|
||||
INSERT INTO "new__folders_mapping" ("id", "virtual_path", "quota_size", "quota_files", "folder_id", "user_id") SELECT "id",
|
||||
"virtual_path", "quota_size", "quota_files", "folder_id", "user_id" FROM "{{users_folders_mapping}}";
|
||||
DROP TABLE "{{users_folders_mapping}}";
|
||||
ALTER TABLE "new__folders_mapping" RENAME TO "{{folders_mapping}}";
|
||||
CREATE INDEX "{{prefix}}folders_mapping_folder_id_idx" ON "{{folders_mapping}}" ("folder_id");
|
||||
CREATE INDEX "{{prefix}}folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id");
|
||||
`
|
||||
)
|
||||
|
||||
|
@ -193,7 +240,7 @@ func (p *SQLiteProvider) updateUser(user *User) error {
|
|||
return sqlCommonUpdateUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) deleteUser(user *User) error {
|
||||
func (p *SQLiteProvider) deleteUser(user User) error {
|
||||
return sqlCommonDeleteUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
|
@ -222,8 +269,8 @@ func (p *SQLiteProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
|
|||
return sqlCommonDumpFolders(p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) getFolders(limit, offset int, order string) ([]vfs.BaseVirtualFolder, error) {
|
||||
return sqlCommonGetFolders(limit, offset, order, p.dbHandle)
|
||||
func (p *SQLiteProvider) getFolders(limit, offset int, order string, minimal bool) ([]vfs.BaseVirtualFolder, error) {
|
||||
return sqlCommonGetFolders(limit, offset, order, minimal, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, error) {
|
||||
|
@ -240,7 +287,7 @@ func (p *SQLiteProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
|
|||
return sqlCommonUpdateFolder(folder, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
func (p *SQLiteProvider) deleteFolder(folder vfs.BaseVirtualFolder) error {
|
||||
return sqlCommonDeleteFolder(folder, p.dbHandle)
|
||||
}
|
||||
|
||||
|
@ -252,6 +299,38 @@ func (p *SQLiteProvider) getUsedFolderQuota(name string) (int, int64, error) {
|
|||
return sqlCommonGetFolderUsedQuota(name, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) getGroups(limit, offset int, order string, minimal bool) ([]Group, error) {
|
||||
return sqlCommonGetGroups(limit, offset, order, minimal, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) getGroupsWithNames(names []string) ([]Group, error) {
|
||||
return sqlCommonGetGroupsWithNames(names, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) getUsersInGroups(names []string) ([]string, error) {
|
||||
return sqlCommonGetUsersInGroups(names, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) groupExists(name string) (Group, error) {
|
||||
return sqlCommonGetGroupByName(name, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) addGroup(group *Group) error {
|
||||
return sqlCommonAddGroup(group, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) updateGroup(group *Group) error {
|
||||
return sqlCommonUpdateGroup(group, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) deleteGroup(group Group) error {
|
||||
return sqlCommonDeleteGroup(group, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) dumpGroups() ([]Group, error) {
|
||||
return sqlCommonDumpGroups(p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) adminExists(username string) (Admin, error) {
|
||||
return sqlCommonGetAdminByUsername(username, p.dbHandle)
|
||||
}
|
||||
|
@ -264,7 +343,7 @@ func (p *SQLiteProvider) updateAdmin(admin *Admin) error {
|
|||
return sqlCommonUpdateAdmin(admin, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) deleteAdmin(admin *Admin) error {
|
||||
func (p *SQLiteProvider) deleteAdmin(admin Admin) error {
|
||||
return sqlCommonDeleteAdmin(admin, p.dbHandle)
|
||||
}
|
||||
|
||||
|
@ -292,7 +371,7 @@ func (p *SQLiteProvider) updateAPIKey(apiKey *APIKey) error {
|
|||
return sqlCommonUpdateAPIKey(apiKey, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) deleteAPIKey(apiKey *APIKey) error {
|
||||
func (p *SQLiteProvider) deleteAPIKey(apiKey APIKey) error {
|
||||
return sqlCommonDeleteAPIKey(apiKey, p.dbHandle)
|
||||
}
|
||||
|
||||
|
@ -320,7 +399,7 @@ func (p *SQLiteProvider) updateShare(share *Share) error {
|
|||
return sqlCommonUpdateShare(share, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) deleteShare(share *Share) error {
|
||||
func (p *SQLiteProvider) deleteShare(share Share) error {
|
||||
return sqlCommonDeleteShare(share, p.dbHandle)
|
||||
}
|
||||
|
||||
|
@ -438,6 +517,8 @@ func (p *SQLiteProvider) migrateDatabase() error {
|
|||
return err
|
||||
case version == 15:
|
||||
return updateSQLiteDatabaseFromV15(p.dbHandle)
|
||||
case version == 16:
|
||||
return updateSQLiteDatabaseFromV16(p.dbHandle)
|
||||
default:
|
||||
if version > sqlDatabaseVersion {
|
||||
providerLog(logger.LevelError, "database version %v is newer than the supported one: %v", version,
|
||||
|
@ -462,33 +543,40 @@ func (p *SQLiteProvider) revertDatabase(targetVersion int) error {
|
|||
switch dbVersion.Version {
|
||||
case 16:
|
||||
return downgradeSQLiteDatabaseFromV16(p.dbHandle)
|
||||
case 17:
|
||||
return downgradeSQLiteDatabaseFromV17(p.dbHandle)
|
||||
default:
|
||||
return fmt.Errorf("database version not handled: %v", dbVersion.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) resetDatabase() error {
|
||||
sql := strings.ReplaceAll(sqliteResetSQL, "{{schema_version}}", sqlTableSchemaVersion)
|
||||
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
|
||||
sql = strings.ReplaceAll(sql, "{{folders}}", sqlTableFolders)
|
||||
sql = strings.ReplaceAll(sql, "{{users}}", sqlTableUsers)
|
||||
sql = strings.ReplaceAll(sql, "{{folders_mapping}}", sqlTableFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{api_keys}}", sqlTableAPIKeys)
|
||||
sql = strings.ReplaceAll(sql, "{{shares}}", sqlTableShares)
|
||||
sql = strings.ReplaceAll(sql, "{{defender_events}}", sqlTableDefenderEvents)
|
||||
sql = strings.ReplaceAll(sql, "{{defender_hosts}}", sqlTableDefenderHosts)
|
||||
sql = strings.ReplaceAll(sql, "{{active_transfers}}", sqlTableActiveTransfers)
|
||||
sql := sqlReplaceAll(sqliteResetSQL)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 0)
|
||||
}
|
||||
|
||||
func updateSQLiteDatabaseFromV15(dbHandle *sql.DB) error {
|
||||
return updateSQLiteDatabaseFrom15To16(dbHandle)
|
||||
if err := updateSQLiteDatabaseFrom15To16(dbHandle); err != nil {
|
||||
return err
|
||||
}
|
||||
return updateSQLiteDatabaseFromV16(dbHandle)
|
||||
}
|
||||
|
||||
func updateSQLiteDatabaseFromV16(dbHandle *sql.DB) error {
|
||||
return updateSQLiteDatabaseFrom16To17(dbHandle)
|
||||
}
|
||||
|
||||
func downgradeSQLiteDatabaseFromV16(dbHandle *sql.DB) error {
|
||||
return downgradeSQLiteDatabaseFrom16To15(dbHandle)
|
||||
}
|
||||
|
||||
func downgradeSQLiteDatabaseFromV17(dbHandle *sql.DB) error {
|
||||
if err := downgradeSQLiteDatabaseFrom17To16(dbHandle); err != nil {
|
||||
return err
|
||||
}
|
||||
return downgradeSQLiteDatabaseFromV16(dbHandle)
|
||||
}
|
||||
|
||||
func updateSQLiteDatabaseFrom15To16(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database version: 15 -> 16")
|
||||
providerLog(logger.LevelInfo, "updating database version: 15 -> 16")
|
||||
|
@ -498,6 +586,26 @@ func updateSQLiteDatabaseFrom15To16(dbHandle *sql.DB) error {
|
|||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 16)
|
||||
}
|
||||
|
||||
func updateSQLiteDatabaseFrom16To17(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database version: 16 -> 17")
|
||||
providerLog(logger.LevelInfo, "updating database version: 16 -> 17")
|
||||
if err := setPragmaFK(dbHandle, "OFF"); err != nil {
|
||||
return err
|
||||
}
|
||||
sql := strings.ReplaceAll(sqliteV17SQL, "{{users}}", sqlTableUsers)
|
||||
sql = strings.ReplaceAll(sql, "{{groups}}", sqlTableGroups)
|
||||
sql = strings.ReplaceAll(sql, "{{folders}}", sqlTableFolders)
|
||||
sql = strings.ReplaceAll(sql, "{{folders_mapping}}", sqlTableFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{users_folders_mapping}}", sqlTableUsersFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{users_groups_mapping}}", sqlTableUsersGroupsMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{groups_folders_mapping}}", sqlTableGroupsFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
|
||||
if err := sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 17); err != nil {
|
||||
return err
|
||||
}
|
||||
return setPragmaFK(dbHandle, "ON")
|
||||
}
|
||||
|
||||
func downgradeSQLiteDatabaseFrom16To15(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 16 -> 15")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 16 -> 15")
|
||||
|
@ -506,7 +614,27 @@ func downgradeSQLiteDatabaseFrom16To15(dbHandle *sql.DB) error {
|
|||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 15)
|
||||
}
|
||||
|
||||
/*func setPragmaFK(dbHandle *sql.DB, value string) error {
|
||||
func downgradeSQLiteDatabaseFrom17To16(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 17 -> 16")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 17 -> 16")
|
||||
if err := setPragmaFK(dbHandle, "OFF"); err != nil {
|
||||
return err
|
||||
}
|
||||
sql := strings.ReplaceAll(sqliteV17DownSQL, "{{groups}}", sqlTableGroups)
|
||||
sql = strings.ReplaceAll(sql, "{{users}}", sqlTableUsers)
|
||||
sql = strings.ReplaceAll(sql, "{{folders}}", sqlTableFolders)
|
||||
sql = strings.ReplaceAll(sql, "{{folders_mapping}}", sqlTableFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{users_folders_mapping}}", sqlTableUsersFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{users_groups_mapping}}", sqlTableUsersGroupsMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{groups_folders_mapping}}", sqlTableGroupsFoldersMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
|
||||
if err := sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 16); err != nil {
|
||||
return err
|
||||
}
|
||||
return setPragmaFK(dbHandle, "ON")
|
||||
}
|
||||
|
||||
func setPragmaFK(dbHandle *sql.DB, value string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
|
@ -514,4 +642,4 @@ func downgradeSQLiteDatabaseFrom16To15(dbHandle *sql.DB) error {
|
|||
|
||||
_, err := dbHandle.ExecContext(ctx, sql)
|
||||
return err
|
||||
}*/
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ const (
|
|||
selectAPIKeyFields = "key_id,name,api_key,scope,created_at,updated_at,last_use_at,expires_at,description,user_id,admin_id"
|
||||
selectShareFields = "s.share_id,s.name,s.description,s.scope,s.paths,u.username,s.created_at,s.updated_at,s.last_use_at," +
|
||||
"s.expires_at,s.password,s.max_tokens,s.used_tokens,s.allow_from"
|
||||
selectGroupFields = "id,name,description,created_at,updated_at,user_settings"
|
||||
)
|
||||
|
||||
func getSQLPlaceholders() []string {
|
||||
|
@ -105,6 +106,78 @@ func getDefenderEventsCleanupQuery() string {
|
|||
return fmt.Sprintf(`DELETE FROM %v WHERE date_time < %v`, sqlTableDefenderEvents, sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getGroupByNameQuery() string {
|
||||
return fmt.Sprintf(`SELECT %s FROM %s WHERE name = %s`, selectGroupFields, sqlTableGroups, sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getGroupsQuery(order string, minimal bool) string {
|
||||
var fieldSelection string
|
||||
if minimal {
|
||||
fieldSelection = "id,name"
|
||||
} else {
|
||||
fieldSelection = selectGroupFields
|
||||
}
|
||||
return fmt.Sprintf(`SELECT %s FROM %s ORDER BY name %s LIMIT %v OFFSET %v`, fieldSelection, sqlTableGroups,
|
||||
order, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||
}
|
||||
|
||||
func getGroupsWithNamesQuery(numArgs int) string {
|
||||
var sb strings.Builder
|
||||
for idx := 0; idx < numArgs; idx++ {
|
||||
if sb.Len() == 0 {
|
||||
sb.WriteString("(")
|
||||
} else {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(sqlPlaceholders[idx])
|
||||
}
|
||||
if sb.Len() > 0 {
|
||||
sb.WriteString(")")
|
||||
} else {
|
||||
sb.WriteString("('')")
|
||||
}
|
||||
return fmt.Sprintf(`SELECT %s FROM %s WHERE name in %s`, selectGroupFields, sqlTableGroups, sb.String())
|
||||
}
|
||||
|
||||
func getUsersInGroupsQuery(numArgs int) string {
|
||||
var sb strings.Builder
|
||||
for idx := 0; idx < numArgs; idx++ {
|
||||
if sb.Len() == 0 {
|
||||
sb.WriteString("(")
|
||||
} else {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(sqlPlaceholders[idx])
|
||||
}
|
||||
if sb.Len() > 0 {
|
||||
sb.WriteString(")")
|
||||
} else {
|
||||
sb.WriteString("('')")
|
||||
}
|
||||
return fmt.Sprintf(`SELECT username FROM %s WHERE id IN (SELECT user_id from %s WHERE group_id IN (SELECT id FROM %s WHERE name IN (%s)))`,
|
||||
sqlTableUsers, sqlTableUsersGroupsMapping, sqlTableGroups, sb.String())
|
||||
}
|
||||
|
||||
func getDumpGroupsQuery() string {
|
||||
return fmt.Sprintf(`SELECT %s FROM %s`, selectGroupFields, sqlTableGroups)
|
||||
}
|
||||
|
||||
func getAddGroupQuery() string {
|
||||
return fmt.Sprintf(`INSERT INTO %s (name,description,created_at,updated_at,user_settings)
|
||||
VALUES (%v,%v,%v,%v,%v)`, sqlTableGroups, sqlPlaceholders[0], sqlPlaceholders[1],
|
||||
sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4])
|
||||
}
|
||||
|
||||
func getUpdateGroupQuery() string {
|
||||
return fmt.Sprintf(`UPDATE %s SET description=%v,user_settings=%v,updated_at=%v
|
||||
WHERE name = %s`, sqlTableGroups, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2],
|
||||
sqlPlaceholders[3])
|
||||
}
|
||||
|
||||
func getDeleteGroupQuery() string {
|
||||
return fmt.Sprintf(`DELETE FROM %s WHERE name = %s`, sqlTableGroups, sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getAdminByUsernameQuery() string {
|
||||
return fmt.Sprintf(`SELECT %v FROM %v WHERE username = %v`, selectAdminFields, sqlTableAdmins, sqlPlaceholders[0])
|
||||
}
|
||||
|
@ -396,19 +469,48 @@ func getDeleteFolderQuery() string {
|
|||
return fmt.Sprintf(`DELETE FROM %v WHERE id = %v`, sqlTableFolders, sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getClearFolderMappingQuery() string {
|
||||
return fmt.Sprintf(`DELETE FROM %v WHERE user_id = (SELECT id FROM %v WHERE username = %v)`, sqlTableFoldersMapping,
|
||||
func getClearUserGroupMappingQuery() string {
|
||||
return fmt.Sprintf(`DELETE FROM %v WHERE user_id = (SELECT id FROM %v WHERE username = %v)`, sqlTableUsersGroupsMapping,
|
||||
sqlTableUsers, sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getAddFolderMappingQuery() string {
|
||||
func getAddUserGroupMappingQuery() string {
|
||||
return fmt.Sprintf(`INSERT INTO %v (user_id,group_id,group_type) VALUES ((SELECT id FROM %v WHERE username = %v),
|
||||
(SELECT id FROM %v WHERE name = %v),%v)`,
|
||||
sqlTableUsersGroupsMapping, sqlTableUsers, sqlPlaceholders[0], sqlTableGroups, sqlPlaceholders[1], sqlPlaceholders[2])
|
||||
}
|
||||
|
||||
func getClearGroupFolderMappingQuery() string {
|
||||
return fmt.Sprintf(`DELETE FROM %v WHERE group_id = (SELECT id FROM %v WHERE name = %v)`, sqlTableGroupsFoldersMapping,
|
||||
sqlTableGroups, sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getAddGroupFolderMappingQuery() string {
|
||||
return fmt.Sprintf(`INSERT INTO %v (virtual_path,quota_size,quota_files,folder_id,group_id)
|
||||
VALUES (%v,%v,%v,(SELECT id FROM %v WHERE name = %v),(SELECT id FROM %v WHERE name = %v))`,
|
||||
sqlTableGroupsFoldersMapping, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlTableFolders,
|
||||
sqlPlaceholders[3], sqlTableGroups, sqlPlaceholders[4])
|
||||
}
|
||||
|
||||
func getClearUserFolderMappingQuery() string {
|
||||
return fmt.Sprintf(`DELETE FROM %v WHERE user_id = (SELECT id FROM %v WHERE username = %v)`, sqlTableUsersFoldersMapping,
|
||||
sqlTableUsers, sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getAddUserFolderMappingQuery() string {
|
||||
return fmt.Sprintf(`INSERT INTO %v (virtual_path,quota_size,quota_files,folder_id,user_id)
|
||||
VALUES (%v,%v,%v,%v,(SELECT id FROM %v WHERE username = %v))`, sqlTableFoldersMapping, sqlPlaceholders[0],
|
||||
VALUES (%v,%v,%v,%v,(SELECT id FROM %v WHERE username = %v))`, sqlTableUsersFoldersMapping, sqlPlaceholders[0],
|
||||
sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlTableUsers, sqlPlaceholders[4])
|
||||
}
|
||||
|
||||
func getFoldersQuery(order string) string {
|
||||
return fmt.Sprintf(`SELECT %v FROM %v ORDER BY name %v LIMIT %v OFFSET %v`, selectFolderFields, sqlTableFolders,
|
||||
func getFoldersQuery(order string, minimal bool) string {
|
||||
var fieldSelection string
|
||||
if minimal {
|
||||
fieldSelection = "id,name"
|
||||
} else {
|
||||
fieldSelection = selectFolderFields
|
||||
}
|
||||
return fmt.Sprintf(`SELECT %v FROM %v ORDER BY name %v LIMIT %v OFFSET %v`, fieldSelection, sqlTableFolders,
|
||||
order, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||
}
|
||||
|
||||
|
@ -426,6 +528,23 @@ func getQuotaFolderQuery() string {
|
|||
sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getRelatedGroupsForUsersQuery(users []User) string {
|
||||
var sb strings.Builder
|
||||
for _, u := range users {
|
||||
if sb.Len() == 0 {
|
||||
sb.WriteString("(")
|
||||
} else {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(strconv.FormatInt(u.ID, 10))
|
||||
}
|
||||
if sb.Len() > 0 {
|
||||
sb.WriteString(")")
|
||||
}
|
||||
return fmt.Sprintf(`SELECT g.name,ug.group_type,ug.user_id FROM %v g INNER JOIN %v ug ON g.id = ug.group_id WHERE
|
||||
ug.user_id IN %v ORDER BY ug.user_id`, sqlTableGroups, sqlTableUsersGroupsMapping, sb.String())
|
||||
}
|
||||
|
||||
func getRelatedFoldersForUsersQuery(users []User) string {
|
||||
var sb strings.Builder
|
||||
for _, u := range users {
|
||||
|
@ -441,7 +560,7 @@ func getRelatedFoldersForUsersQuery(users []User) string {
|
|||
}
|
||||
return fmt.Sprintf(`SELECT f.id,f.name,f.path,f.used_quota_size,f.used_quota_files,f.last_quota_update,fm.virtual_path,
|
||||
fm.quota_size,fm.quota_files,fm.user_id,f.filesystem,f.description FROM %v f INNER JOIN %v fm ON f.id = fm.folder_id WHERE
|
||||
fm.user_id IN %v ORDER BY fm.user_id`, sqlTableFolders, sqlTableFoldersMapping, sb.String())
|
||||
fm.user_id IN %v ORDER BY fm.user_id`, sqlTableFolders, sqlTableUsersFoldersMapping, sb.String())
|
||||
}
|
||||
|
||||
func getRelatedUsersForFoldersQuery(folders []vfs.BaseVirtualFolder) string {
|
||||
|
@ -458,7 +577,59 @@ func getRelatedUsersForFoldersQuery(folders []vfs.BaseVirtualFolder) string {
|
|||
sb.WriteString(")")
|
||||
}
|
||||
return fmt.Sprintf(`SELECT fm.folder_id,u.username FROM %v fm INNER JOIN %v u ON fm.user_id = u.id
|
||||
WHERE fm.folder_id IN %v ORDER BY fm.folder_id`, sqlTableFoldersMapping, sqlTableUsers, sb.String())
|
||||
WHERE fm.folder_id IN %v ORDER BY fm.folder_id`, sqlTableUsersFoldersMapping, sqlTableUsers, sb.String())
|
||||
}
|
||||
|
||||
func getRelatedGroupsForFoldersQuery(folders []vfs.BaseVirtualFolder) string {
|
||||
var sb strings.Builder
|
||||
for _, f := range folders {
|
||||
if sb.Len() == 0 {
|
||||
sb.WriteString("(")
|
||||
} else {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(strconv.FormatInt(f.ID, 10))
|
||||
}
|
||||
if sb.Len() > 0 {
|
||||
sb.WriteString(")")
|
||||
}
|
||||
return fmt.Sprintf(`SELECT fm.folder_id,g.name FROM %v fm INNER JOIN %v g ON fm.group_id = g.id
|
||||
WHERE fm.folder_id IN %v ORDER BY fm.folder_id`, sqlTableGroupsFoldersMapping, sqlTableGroups, sb.String())
|
||||
}
|
||||
|
||||
func getRelatedUsersForGroupsQuery(groups []Group) string {
|
||||
var sb strings.Builder
|
||||
for _, g := range groups {
|
||||
if sb.Len() == 0 {
|
||||
sb.WriteString("(")
|
||||
} else {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(strconv.FormatInt(g.ID, 10))
|
||||
}
|
||||
if sb.Len() > 0 {
|
||||
sb.WriteString(")")
|
||||
}
|
||||
return fmt.Sprintf(`SELECT um.group_id,u.username FROM %v um INNER JOIN %v u ON um.user_id = u.id
|
||||
WHERE um.group_id IN %v ORDER BY um.group_id`, sqlTableUsersGroupsMapping, sqlTableUsers, sb.String())
|
||||
}
|
||||
|
||||
func getRelatedFoldersForGroupsQuery(groups []Group) string {
|
||||
var sb strings.Builder
|
||||
for _, g := range groups {
|
||||
if sb.Len() == 0 {
|
||||
sb.WriteString("(")
|
||||
} else {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(strconv.FormatInt(g.ID, 10))
|
||||
}
|
||||
if sb.Len() > 0 {
|
||||
sb.WriteString(")")
|
||||
}
|
||||
return fmt.Sprintf(`SELECT f.id,f.name,f.path,f.used_quota_size,f.used_quota_files,f.last_quota_update,fm.virtual_path,
|
||||
fm.quota_size,fm.quota_files,fm.group_id,f.filesystem,f.description FROM %s f INNER JOIN %s fm ON f.id = fm.folder_id WHERE
|
||||
fm.group_id IN %v ORDER BY fm.group_id`, sqlTableFolders, sqlTableGroupsFoldersMapping, sb.String())
|
||||
}
|
||||
|
||||
func getActiveTransfersQuery() string {
|
||||
|
|
|
@ -123,8 +123,12 @@ type User struct {
|
|||
VirtualFolders []vfs.VirtualFolder `json:"virtual_folders,omitempty"`
|
||||
// Filesystem configuration details
|
||||
FsConfig vfs.Filesystem `json:"filesystem"`
|
||||
// groups associated with this user
|
||||
Groups []sdk.GroupMapping `json:"groups,omitempty"`
|
||||
// we store the filesystem here using the base path as key.
|
||||
fsCache map[string]vfs.Fs `json:"-"`
|
||||
// true if group settings are already applied for this user
|
||||
groupSettingsApplied bool `json:"-"`
|
||||
}
|
||||
|
||||
// GetFilesystem returns the base filesystem for this user
|
||||
|
@ -453,6 +457,9 @@ func (u *User) HasBufferedSFTP(name string) bool {
|
|||
|
||||
func (u *User) getForbiddenSFTPSelfUsers(username string) ([]string, error) {
|
||||
sftpUser, err := UserExists(username)
|
||||
if err == nil {
|
||||
err = sftpUser.LoadAndApplyGroupSettings()
|
||||
}
|
||||
if err == nil {
|
||||
// we don't allow local nested SFTP folders
|
||||
var forbiddens []string
|
||||
|
@ -912,30 +919,6 @@ func (u *User) GetAllowedLoginMethods() []string {
|
|||
return allowedMethods
|
||||
}
|
||||
|
||||
// GetFlatFilePatterns returns file patterns as flat list
|
||||
// duplicating a path if it has both allowed and denied patterns
|
||||
func (u *User) GetFlatFilePatterns() []sdk.PatternsFilter {
|
||||
var result []sdk.PatternsFilter
|
||||
|
||||
for _, pattern := range u.Filters.FilePatterns {
|
||||
if len(pattern.AllowedPatterns) > 0 {
|
||||
result = append(result, sdk.PatternsFilter{
|
||||
Path: pattern.Path,
|
||||
AllowedPatterns: pattern.AllowedPatterns,
|
||||
DenyPolicy: pattern.DenyPolicy,
|
||||
})
|
||||
}
|
||||
if len(pattern.DeniedPatterns) > 0 {
|
||||
result = append(result, sdk.PatternsFilter{
|
||||
Path: pattern.Path,
|
||||
DeniedPatterns: pattern.DeniedPatterns,
|
||||
DenyPolicy: pattern.DenyPolicy,
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (u *User) getPatternsFilterForPath(virtualPath string) sdk.PatternsFilter {
|
||||
var filter sdk.PatternsFilter
|
||||
if len(u.Filters.FilePatterns) == 0 {
|
||||
|
@ -1204,7 +1187,7 @@ func (u *User) GetGID() int {
|
|||
|
||||
// GetHomeDir returns the shortest path name equivalent to the user's home directory
|
||||
func (u *User) GetHomeDir() string {
|
||||
return filepath.Clean(u.HomeDir)
|
||||
return u.HomeDir
|
||||
}
|
||||
|
||||
// HasRecentActivity returns true if the last user login is recent and so we can skip some expensive checks
|
||||
|
@ -1448,6 +1431,256 @@ func (u *User) SetEmptySecretsIfNil() {
|
|||
}
|
||||
}
|
||||
|
||||
func (u *User) hasMainDataTransferLimits() bool {
|
||||
return u.UploadDataTransfer > 0 || u.DownloadDataTransfer > 0 || u.TotalDataTransfer > 0
|
||||
}
|
||||
|
||||
// HasPrimaryGroup returns true if the user has the specified primary group
|
||||
func (u *User) HasPrimaryGroup(name string) bool {
|
||||
for _, g := range u.Groups {
|
||||
if g.Name == name {
|
||||
return g.Type == sdk.GroupTypePrimary
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasSecondaryGroup returns true if the user has the specified secondary group
|
||||
func (u *User) HasSecondaryGroup(name string) bool {
|
||||
for _, g := range u.Groups {
|
||||
if g.Name == name {
|
||||
return g.Type == sdk.GroupTypeSecondary
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (u *User) applyGroupSettings(groupsMapping map[string]Group) {
|
||||
if len(u.Groups) == 0 {
|
||||
return
|
||||
}
|
||||
if u.groupSettingsApplied {
|
||||
return
|
||||
}
|
||||
for _, g := range u.Groups {
|
||||
if g.Type == sdk.GroupTypePrimary {
|
||||
if group, ok := groupsMapping[g.Name]; ok {
|
||||
u.mergeWithPrimaryGroup(group)
|
||||
} else {
|
||||
providerLog(logger.LevelError, "mapping not found for user %s, group %s", u.Username, g.Name)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, g := range u.Groups {
|
||||
if g.Type == sdk.GroupTypeSecondary {
|
||||
if group, ok := groupsMapping[g.Name]; ok {
|
||||
u.mergeAdditiveProperties(group, sdk.GroupTypeSecondary)
|
||||
} else {
|
||||
providerLog(logger.LevelError, "mapping not found for user %s, group %s", u.Username, g.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
u.removeDuplicatesAfterGroupMerge()
|
||||
}
|
||||
|
||||
// LoadAndApplyGroupSettings update the user by loading and applying the group settings
|
||||
func (u *User) LoadAndApplyGroupSettings() error {
|
||||
if len(u.Groups) == 0 {
|
||||
return nil
|
||||
}
|
||||
if u.groupSettingsApplied {
|
||||
return nil
|
||||
}
|
||||
names := make([]string, 0, len(u.Groups))
|
||||
var primaryGroupName string
|
||||
for _, g := range u.Groups {
|
||||
if g.Type == sdk.GroupTypePrimary {
|
||||
primaryGroupName = g.Name
|
||||
}
|
||||
names = append(names, g.Name)
|
||||
}
|
||||
groups, err := provider.getGroupsWithNames(names)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get groups: %w", err)
|
||||
}
|
||||
// make sure to always merge with the primary group first
|
||||
for idx, g := range groups {
|
||||
if g.Name == primaryGroupName {
|
||||
u.mergeWithPrimaryGroup(g)
|
||||
lastIdx := len(groups) - 1
|
||||
groups[idx] = groups[lastIdx]
|
||||
groups = groups[:lastIdx]
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, g := range groups {
|
||||
u.mergeAdditiveProperties(g, sdk.GroupTypeSecondary)
|
||||
}
|
||||
u.removeDuplicatesAfterGroupMerge()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) replacePlaceholder(value string) string {
|
||||
if value == "" {
|
||||
return value
|
||||
}
|
||||
return strings.ReplaceAll(value, "%username%", u.Username)
|
||||
}
|
||||
|
||||
func (u *User) replaceFsConfigPlaceholders() {
|
||||
switch u.FsConfig.Provider {
|
||||
case sdk.S3FilesystemProvider:
|
||||
u.FsConfig.S3Config.KeyPrefix = u.replacePlaceholder(u.FsConfig.S3Config.KeyPrefix)
|
||||
case sdk.GCSFilesystemProvider:
|
||||
u.FsConfig.GCSConfig.KeyPrefix = u.replacePlaceholder(u.FsConfig.GCSConfig.KeyPrefix)
|
||||
case sdk.AzureBlobFilesystemProvider:
|
||||
u.FsConfig.AzBlobConfig.KeyPrefix = u.replacePlaceholder(u.FsConfig.AzBlobConfig.KeyPrefix)
|
||||
case sdk.SFTPFilesystemProvider:
|
||||
u.FsConfig.SFTPConfig.Username = u.replacePlaceholder(u.FsConfig.SFTPConfig.Username)
|
||||
u.FsConfig.SFTPConfig.Prefix = u.replacePlaceholder(u.FsConfig.SFTPConfig.Prefix)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) mergeWithPrimaryGroup(group Group) {
|
||||
if group.UserSettings.HomeDir != "" {
|
||||
u.HomeDir = u.replacePlaceholder(group.UserSettings.HomeDir)
|
||||
}
|
||||
if group.UserSettings.FsConfig.Provider != 0 {
|
||||
u.FsConfig = group.UserSettings.FsConfig
|
||||
u.replaceFsConfigPlaceholders()
|
||||
}
|
||||
if u.MaxSessions == 0 {
|
||||
u.MaxSessions = group.UserSettings.MaxSessions
|
||||
}
|
||||
if u.QuotaSize == 0 {
|
||||
u.QuotaSize = group.UserSettings.QuotaSize
|
||||
}
|
||||
if u.QuotaFiles == 0 {
|
||||
u.QuotaFiles = group.UserSettings.QuotaFiles
|
||||
}
|
||||
if u.UploadBandwidth == 0 {
|
||||
u.UploadBandwidth = group.UserSettings.UploadBandwidth
|
||||
}
|
||||
if u.DownloadBandwidth == 0 {
|
||||
u.DownloadBandwidth = group.UserSettings.DownloadBandwidth
|
||||
}
|
||||
if !u.hasMainDataTransferLimits() {
|
||||
u.UploadDataTransfer = group.UserSettings.UploadDataTransfer
|
||||
u.DownloadDataTransfer = group.UserSettings.DownloadDataTransfer
|
||||
u.TotalDataTransfer = group.UserSettings.TotalDataTransfer
|
||||
}
|
||||
u.mergePrimaryGroupFilters(group.UserSettings.Filters)
|
||||
u.mergeAdditiveProperties(group, sdk.GroupTypePrimary)
|
||||
}
|
||||
|
||||
func (u *User) mergePrimaryGroupFilters(filters sdk.BaseUserFilters) {
|
||||
if u.Filters.MaxUploadFileSize == 0 {
|
||||
u.Filters.MaxUploadFileSize = filters.MaxUploadFileSize
|
||||
}
|
||||
if u.Filters.TLSUsername == "" || u.Filters.TLSUsername == sdk.TLSUsernameNone {
|
||||
u.Filters.TLSUsername = filters.TLSUsername
|
||||
}
|
||||
if !u.Filters.Hooks.CheckPasswordDisabled {
|
||||
u.Filters.Hooks.CheckPasswordDisabled = filters.Hooks.CheckPasswordDisabled
|
||||
}
|
||||
if !u.Filters.Hooks.PreLoginDisabled {
|
||||
u.Filters.Hooks.PreLoginDisabled = filters.Hooks.PreLoginDisabled
|
||||
}
|
||||
if !u.Filters.Hooks.ExternalAuthDisabled {
|
||||
u.Filters.Hooks.ExternalAuthDisabled = filters.Hooks.ExternalAuthDisabled
|
||||
}
|
||||
if !u.Filters.DisableFsChecks {
|
||||
u.Filters.DisableFsChecks = filters.DisableFsChecks
|
||||
}
|
||||
if !u.Filters.AllowAPIKeyAuth {
|
||||
u.Filters.AllowAPIKeyAuth = filters.AllowAPIKeyAuth
|
||||
}
|
||||
if u.Filters.ExternalAuthCacheTime == 0 {
|
||||
u.Filters.ExternalAuthCacheTime = filters.ExternalAuthCacheTime
|
||||
}
|
||||
if u.Filters.StartDirectory == "" {
|
||||
u.Filters.StartDirectory = u.replacePlaceholder(filters.StartDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) mergeAdditiveProperties(group Group, groupType int) {
|
||||
u.mergeVirtualFolders(group, groupType)
|
||||
u.mergePermissions(group, groupType)
|
||||
u.mergeFilePatterns(group, groupType)
|
||||
u.Filters.BandwidthLimits = append(u.Filters.BandwidthLimits, group.UserSettings.Filters.BandwidthLimits...)
|
||||
u.Filters.DataTransferLimits = append(u.Filters.DataTransferLimits, group.UserSettings.Filters.DataTransferLimits...)
|
||||
u.Filters.AllowedIP = append(u.Filters.AllowedIP, group.UserSettings.Filters.AllowedIP...)
|
||||
u.Filters.DeniedIP = append(u.Filters.DeniedIP, group.UserSettings.Filters.DeniedIP...)
|
||||
u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, group.UserSettings.Filters.DeniedLoginMethods...)
|
||||
u.Filters.DeniedProtocols = append(u.Filters.DeniedProtocols, group.UserSettings.Filters.DeniedProtocols...)
|
||||
u.Filters.WebClient = append(u.Filters.WebClient, group.UserSettings.Filters.WebClient...)
|
||||
u.Filters.TwoFactorAuthProtocols = append(u.Filters.TwoFactorAuthProtocols, group.UserSettings.Filters.TwoFactorAuthProtocols...)
|
||||
}
|
||||
|
||||
func (u *User) mergeVirtualFolders(group Group, groupType int) {
|
||||
if len(group.VirtualFolders) > 0 {
|
||||
folderPaths := make(map[string]bool)
|
||||
for _, folder := range u.VirtualFolders {
|
||||
folderPaths[folder.VirtualPath] = true
|
||||
}
|
||||
for _, folder := range group.VirtualFolders {
|
||||
if folder.VirtualPath == "/" && groupType != sdk.GroupTypePrimary {
|
||||
continue
|
||||
}
|
||||
folder.VirtualPath = u.replacePlaceholder(folder.VirtualPath)
|
||||
if _, ok := folderPaths[folder.VirtualPath]; !ok {
|
||||
folder.MappedPath = u.replacePlaceholder(folder.MappedPath)
|
||||
u.VirtualFolders = append(u.VirtualFolders, folder)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) mergePermissions(group Group, groupType int) {
|
||||
for k, v := range group.UserSettings.Permissions {
|
||||
if k == "/" {
|
||||
if groupType == sdk.GroupTypePrimary {
|
||||
u.Permissions[k] = v
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
k = u.replacePlaceholder(k)
|
||||
if _, ok := u.Permissions[k]; !ok {
|
||||
u.Permissions[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) mergeFilePatterns(group Group, groupType int) {
|
||||
if len(group.UserSettings.Filters.FilePatterns) > 0 {
|
||||
patternPaths := make(map[string]bool)
|
||||
for _, pattern := range u.Filters.FilePatterns {
|
||||
patternPaths[pattern.Path] = true
|
||||
}
|
||||
for _, pattern := range group.UserSettings.Filters.FilePatterns {
|
||||
if pattern.Path == "/" && groupType != sdk.GroupTypePrimary {
|
||||
continue
|
||||
}
|
||||
pattern.Path = u.replacePlaceholder(pattern.Path)
|
||||
if _, ok := patternPaths[pattern.Path]; !ok {
|
||||
u.Filters.FilePatterns = append(u.Filters.FilePatterns, pattern)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) removeDuplicatesAfterGroupMerge() {
|
||||
u.Filters.AllowedIP = util.RemoveDuplicates(u.Filters.AllowedIP)
|
||||
u.Filters.DeniedIP = util.RemoveDuplicates(u.Filters.DeniedIP)
|
||||
u.Filters.DeniedLoginMethods = util.RemoveDuplicates(u.Filters.DeniedLoginMethods)
|
||||
u.Filters.DeniedProtocols = util.RemoveDuplicates(u.Filters.DeniedProtocols)
|
||||
u.Filters.WebClient = util.RemoveDuplicates(u.Filters.WebClient)
|
||||
u.Filters.TwoFactorAuthProtocols = util.RemoveDuplicates(u.Filters.TwoFactorAuthProtocols)
|
||||
u.groupSettingsApplied = true
|
||||
}
|
||||
|
||||
func (u *User) getACopy() User {
|
||||
u.SetEmptySecretsIfNil()
|
||||
pubKeys := make([]string, len(u.PublicKeys))
|
||||
|
@ -1457,42 +1690,27 @@ func (u *User) getACopy() User {
|
|||
vfolder := u.VirtualFolders[idx].GetACopy()
|
||||
virtualFolders = append(virtualFolders, vfolder)
|
||||
}
|
||||
groups := make([]sdk.GroupMapping, 0, len(u.Groups))
|
||||
for _, g := range u.Groups {
|
||||
groups = append(groups, sdk.GroupMapping{
|
||||
Name: g.Name,
|
||||
Type: g.Type,
|
||||
})
|
||||
}
|
||||
permissions := make(map[string][]string)
|
||||
for k, v := range u.Permissions {
|
||||
perms := make([]string, len(v))
|
||||
copy(perms, v)
|
||||
permissions[k] = perms
|
||||
}
|
||||
filters := UserFilters{}
|
||||
filters.MaxUploadFileSize = u.Filters.MaxUploadFileSize
|
||||
filters.TLSUsername = u.Filters.TLSUsername
|
||||
filters.UserType = u.Filters.UserType
|
||||
filters := UserFilters{
|
||||
BaseUserFilters: copyBaseUserFilters(u.Filters.BaseUserFilters),
|
||||
}
|
||||
filters.TOTPConfig.Enabled = u.Filters.TOTPConfig.Enabled
|
||||
filters.TOTPConfig.ConfigName = u.Filters.TOTPConfig.ConfigName
|
||||
filters.TOTPConfig.Secret = u.Filters.TOTPConfig.Secret.Clone()
|
||||
filters.TOTPConfig.Protocols = make([]string, len(u.Filters.TOTPConfig.Protocols))
|
||||
copy(filters.TOTPConfig.Protocols, u.Filters.TOTPConfig.Protocols)
|
||||
filters.AllowedIP = make([]string, len(u.Filters.AllowedIP))
|
||||
copy(filters.AllowedIP, u.Filters.AllowedIP)
|
||||
filters.DeniedIP = make([]string, len(u.Filters.DeniedIP))
|
||||
copy(filters.DeniedIP, u.Filters.DeniedIP)
|
||||
filters.DeniedLoginMethods = make([]string, len(u.Filters.DeniedLoginMethods))
|
||||
copy(filters.DeniedLoginMethods, u.Filters.DeniedLoginMethods)
|
||||
filters.FilePatterns = make([]sdk.PatternsFilter, len(u.Filters.FilePatterns))
|
||||
copy(filters.FilePatterns, u.Filters.FilePatterns)
|
||||
filters.DeniedProtocols = make([]string, len(u.Filters.DeniedProtocols))
|
||||
copy(filters.DeniedProtocols, u.Filters.DeniedProtocols)
|
||||
filters.TwoFactorAuthProtocols = make([]string, len(u.Filters.TwoFactorAuthProtocols))
|
||||
copy(filters.TwoFactorAuthProtocols, u.Filters.TwoFactorAuthProtocols)
|
||||
filters.Hooks.ExternalAuthDisabled = u.Filters.Hooks.ExternalAuthDisabled
|
||||
filters.Hooks.PreLoginDisabled = u.Filters.Hooks.PreLoginDisabled
|
||||
filters.Hooks.CheckPasswordDisabled = u.Filters.Hooks.CheckPasswordDisabled
|
||||
filters.DisableFsChecks = u.Filters.DisableFsChecks
|
||||
filters.StartDirectory = u.Filters.StartDirectory
|
||||
filters.AllowAPIKeyAuth = u.Filters.AllowAPIKeyAuth
|
||||
filters.ExternalAuthCacheTime = u.Filters.ExternalAuthCacheTime
|
||||
filters.WebClient = make([]string, len(u.Filters.WebClient))
|
||||
copy(filters.WebClient, u.Filters.WebClient)
|
||||
filters.RecoveryCodes = make([]RecoveryCode, 0, len(u.Filters.RecoveryCodes))
|
||||
for _, code := range u.Filters.RecoveryCodes {
|
||||
if code.Secret == nil {
|
||||
|
@ -1503,29 +1721,6 @@ func (u *User) getACopy() User {
|
|||
Used: code.Used,
|
||||
})
|
||||
}
|
||||
filters.BandwidthLimits = make([]sdk.BandwidthLimit, 0, len(u.Filters.BandwidthLimits))
|
||||
for _, limit := range u.Filters.BandwidthLimits {
|
||||
bwLimit := sdk.BandwidthLimit{
|
||||
UploadBandwidth: limit.UploadBandwidth,
|
||||
DownloadBandwidth: limit.DownloadBandwidth,
|
||||
Sources: make([]string, 0, len(limit.Sources)),
|
||||
}
|
||||
bwLimit.Sources = make([]string, len(limit.Sources))
|
||||
copy(bwLimit.Sources, limit.Sources)
|
||||
filters.BandwidthLimits = append(filters.BandwidthLimits, bwLimit)
|
||||
}
|
||||
filters.DataTransferLimits = make([]sdk.DataTransferLimit, 0, len(u.Filters.DataTransferLimits))
|
||||
for _, limit := range u.Filters.DataTransferLimits {
|
||||
dtLimit := sdk.DataTransferLimit{
|
||||
UploadDataTransfer: limit.UploadDataTransfer,
|
||||
DownloadDataTransfer: limit.DownloadDataTransfer,
|
||||
TotalDataTransfer: limit.TotalDataTransfer,
|
||||
Sources: make([]string, 0, len(limit.Sources)),
|
||||
}
|
||||
dtLimit.Sources = make([]string, len(limit.Sources))
|
||||
copy(dtLimit.Sources, limit.Sources)
|
||||
filters.DataTransferLimits = append(filters.DataTransferLimits, dtLimit)
|
||||
}
|
||||
|
||||
return User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
|
@ -1559,9 +1754,11 @@ func (u *User) getACopy() User {
|
|||
CreatedAt: u.CreatedAt,
|
||||
UpdatedAt: u.UpdatedAt,
|
||||
},
|
||||
Filters: filters,
|
||||
VirtualFolders: virtualFolders,
|
||||
FsConfig: u.FsConfig.GetACopy(),
|
||||
Filters: filters,
|
||||
VirtualFolders: virtualFolders,
|
||||
Groups: groups,
|
||||
FsConfig: u.FsConfig.GetACopy(),
|
||||
groupSettingsApplied: u.groupSettingsApplied,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
41
docs/groups.md
Normal file
41
docs/groups.md
Normal file
|
@ -0,0 +1,41 @@
|
|||
# Groups
|
||||
|
||||
Using groups simplifies the administration of multiple accounts by letting you assign settings once to a group, instead of multiple times to each individual user.
|
||||
|
||||
SFTPGo supports two types of groups:
|
||||
|
||||
- primary groups
|
||||
- secondary groups
|
||||
|
||||
A user can be a member of a primary group and many secondary groups. Depending on the group type, the settings are inherited differently.
|
||||
|
||||
The following settings are inherited from the primary group:
|
||||
|
||||
- home dir, if set for the group will replace the one defined for the user. The `%username%` placeholder is replaced with the username
|
||||
- filesystem config, if the provider set for the group is different from the "local provider" will replace the one defined for the user. The `%username%` placeholder is replaced with the username within the defined "prefix" and the "username" for the SFTP filesystem config
|
||||
- max sessions, quota size/files, upload/download bandwidth, upload/download/total data transfer, max upload size, external auth cache time: if they are set to `0` for the user they are replaced with the value set for the group, if different from `0`
|
||||
- TLS username, check password hook disabled, pre-login hook disabled, external auth hook disabled, filesystem checks disabled, allow API key authentication: if they are not set for the user they are replaced with the value set for the group
|
||||
- starting directory, if the user does not have a starting directory set, the value set for the group is used, if any. The `%username%` placeholder is replaced with the username
|
||||
|
||||
The following settings are inherited from the primary and secondary groups:
|
||||
|
||||
- virtual folders, file patterns, permissions: they are added to the user configuration if the user does not already have a setting for the configured path. The `/` path is ignored for secondary groups. The `%username%` placeholder is replaced with the username within the virtual path.
|
||||
- per-source bandwidth limits
|
||||
- per-source data transfer limits
|
||||
- allowed/denied IPs
|
||||
- denied login methods and protocols
|
||||
- two factor auth protocols
|
||||
- web client/REST API permissions
|
||||
|
||||
The settings from the primary group are always merged first.
|
||||
|
||||
The final settings are a combination of the user settings and the group ones.
|
||||
For example you can define the following groups:
|
||||
|
||||
- "group1", it has a virtual directory to mount on `/vdir1`
|
||||
- "group2", it has a virtual directory to mount on `/vdir2`
|
||||
- "group3", it has a virtual directory to mount on `/vdir3`
|
||||
|
||||
If you define users with a virtual directory to mount on `/vdir` and make them member of all the above groups, they will have virtual directories mounted on `/vdir`, `/vdir1`, `/vdir2`, `/vdir3`. If users already have a virtual directory to mount on `/vdir1`, the group's one will be ignored.
|
||||
|
||||
Please note that if the same virtual path is set in more than one secondary group the behavior is undefined. For example if a user is a member of two secondary groups and each secondary group defines a virtual folder to mount on the `/vdir2` path, the virtual folder mounted on `/vdir2` may change with every login.
|
|
@ -782,6 +782,11 @@ func TestLoginExternalAuth(t *testing.T) {
|
|||
providerConf.ExternalAuthScope = 0
|
||||
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||
assert.NoError(t, err)
|
||||
g := getTestGroup()
|
||||
g.UserSettings.Filters.DeniedProtocols = []string{common.ProtocolFTP}
|
||||
group, _, err := httpdtest.AddGroup(g, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
client, err := getFTPClient(u, true, nil)
|
||||
if assert.NoError(t, err) {
|
||||
err = checkBasicFTP(client)
|
||||
|
@ -789,11 +794,32 @@ func TestLoginExternalAuth(t *testing.T) {
|
|||
err := client.Quit()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
u.Groups = []sdk.GroupMapping{
|
||||
{
|
||||
Name: group.Name,
|
||||
Type: sdk.GroupTypePrimary,
|
||||
},
|
||||
}
|
||||
err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, ""), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
_, err = getFTPClient(u, true, nil)
|
||||
if !assert.Error(t, err) {
|
||||
err := client.Quit()
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.Contains(t, err.Error(), "protocol FTP is not allowed")
|
||||
}
|
||||
|
||||
u.Groups = nil
|
||||
err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, ""), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
u.Username = defaultUsername + "1"
|
||||
client, err = getFTPClient(u, true, nil)
|
||||
if !assert.Error(t, err) {
|
||||
err := client.Quit()
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.Contains(t, err.Error(), "invalid credentials")
|
||||
}
|
||||
|
||||
user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)
|
||||
|
@ -803,6 +829,8 @@ func TestLoginExternalAuth(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveGroup(group, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = dataprovider.Close()
|
||||
assert.NoError(t, err)
|
||||
|
@ -2920,7 +2948,7 @@ func TestClientCertificateAndPwdAuth(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestExternatAuthWithClientCert(t *testing.T) {
|
||||
func TestExternalAuthWithClientCert(t *testing.T) {
|
||||
if runtime.GOOS == osWindows {
|
||||
t.Skip("this test is not available on Windows")
|
||||
}
|
||||
|
@ -3313,6 +3341,15 @@ func waitNoConnections() {
|
|||
}
|
||||
}
|
||||
|
||||
func getTestGroup() dataprovider.Group {
|
||||
return dataprovider.Group{
|
||||
BaseGroup: sdk.BaseGroup{
|
||||
Name: "test_group",
|
||||
Description: "test group description",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getTestUser() dataprovider.User {
|
||||
user := dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
|
|
2
go.mod
2
go.mod
|
@ -50,7 +50,7 @@ require (
|
|||
github.com/rs/cors v1.8.2
|
||||
github.com/rs/xid v1.4.0
|
||||
github.com/rs/zerolog v1.26.2-0.20220312163309-e9344a8c507b
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220412171743-453880fab5f7
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220425123921-2f843a49e012
|
||||
github.com/shirou/gopsutil/v3 v3.22.3
|
||||
github.com/spf13/afero v1.8.2
|
||||
github.com/spf13/cobra v1.4.0
|
||||
|
|
4
go.sum
4
go.sum
|
@ -688,8 +688,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
|
|||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220412171743-453880fab5f7 h1:y1N2hPOqO1sTCwvtlKWrAiLBLOfThPuE17Tvju1wohs=
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220412171743-453880fab5f7/go.mod h1:m5J7DH8unhD5RUsREFRiidP8zgBjup0+iQaxQnYHJOM=
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220425123921-2f843a49e012 h1:PkryXZIb/Ncl64ZYej8WKZ0QXlqyuu+CG0IG0GEo3do=
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220425123921-2f843a49e012/go.mod h1:m5J7DH8unhD5RUsREFRiidP8zgBjup0+iQaxQnYHJOM=
|
||||
github.com/shirou/gopsutil/v3 v3.22.3 h1:UebRzEomgMpv61e3hgD1tGooqX5trFbdU/ehphbHd00=
|
||||
github.com/shirou/gopsutil/v3 v3.22.3/go.mod h1:D01hZJ4pVHPpCTZ3m3T2+wDF2YAGfd+H4ifUguaQzHM=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
|
|
|
@ -18,12 +18,12 @@ func getFolders(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
folders, err := dataprovider.GetFolders(limit, offset, order)
|
||||
if err == nil {
|
||||
render.JSON(w, r, folders)
|
||||
} else {
|
||||
folders, err := dataprovider.GetFolders(limit, offset, order, false)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
render.JSON(w, r, folders)
|
||||
}
|
||||
|
||||
func addFolder(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -57,6 +57,7 @@ func updateFolder(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
users := folder.Users
|
||||
groups := folder.Groups
|
||||
folderID := folder.ID
|
||||
name = folder.Name
|
||||
currentS3AccessSecret := folder.FsConfig.S3Config.AccessSecret
|
||||
|
@ -82,7 +83,7 @@ func updateFolder(w http.ResponseWriter, r *http.Request) {
|
|||
folder.FsConfig.SetEmptySecretsIfNil()
|
||||
updateEncryptedSecrets(&folder.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl, currentGCSCredentials,
|
||||
currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey)
|
||||
err = dataprovider.UpdateFolder(&folder, users, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
err = dataprovider.UpdateFolder(&folder, users, groups, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
|
|
134
httpd/api_group.go
Normal file
134
httpd/api_group.go
Normal file
|
@ -0,0 +1,134 @@
|
|||
package httpd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/util"
|
||||
"github.com/drakkan/sftpgo/v2/vfs"
|
||||
)
|
||||
|
||||
func getGroups(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
limit, offset, order, err := getSearchFilters(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
groups, err := dataprovider.GetGroups(limit, offset, order, false)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
render.JSON(w, r, groups)
|
||||
}
|
||||
|
||||
func addGroup(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var group dataprovider.Group
|
||||
err = render.DecodeJSON(r.Body, &group)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = dataprovider.AddGroup(&group, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
renderGroup(w, r, group.Name, http.StatusCreated)
|
||||
}
|
||||
|
||||
func updateGroup(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
name := getURLParam(r, "name")
|
||||
group, err := dataprovider.GroupExists(name)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
users := group.Users
|
||||
groupID := group.ID
|
||||
name = group.Name
|
||||
currentS3AccessSecret := group.UserSettings.FsConfig.S3Config.AccessSecret
|
||||
currentAzAccountKey := group.UserSettings.FsConfig.AzBlobConfig.AccountKey
|
||||
currentAzSASUrl := group.UserSettings.FsConfig.AzBlobConfig.SASURL
|
||||
currentGCSCredentials := group.UserSettings.FsConfig.GCSConfig.Credentials
|
||||
currentCryptoPassphrase := group.UserSettings.FsConfig.CryptConfig.Passphrase
|
||||
currentSFTPPassword := group.UserSettings.FsConfig.SFTPConfig.Password
|
||||
currentSFTPKey := group.UserSettings.FsConfig.SFTPConfig.PrivateKey
|
||||
|
||||
group.UserSettings.FsConfig.S3Config = vfs.S3FsConfig{}
|
||||
group.UserSettings.FsConfig.AzBlobConfig = vfs.AzBlobFsConfig{}
|
||||
group.UserSettings.FsConfig.GCSConfig = vfs.GCSFsConfig{}
|
||||
group.UserSettings.FsConfig.CryptConfig = vfs.CryptFsConfig{}
|
||||
group.UserSettings.FsConfig.SFTPConfig = vfs.SFTPFsConfig{}
|
||||
err = render.DecodeJSON(r.Body, &group)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
group.ID = groupID
|
||||
group.Name = name
|
||||
group.UserSettings.FsConfig.SetEmptySecretsIfNil()
|
||||
updateEncryptedSecrets(&group.UserSettings.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl,
|
||||
currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey)
|
||||
err = dataprovider.UpdateGroup(&group, users, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
sendAPIResponse(w, r, nil, "Group updated", http.StatusOK)
|
||||
}
|
||||
|
||||
func renderGroup(w http.ResponseWriter, r *http.Request, name string, status int) {
|
||||
group, err := dataprovider.GroupExists(name)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
group.PrepareForRendering()
|
||||
if status != http.StatusOK {
|
||||
ctx := context.WithValue(r.Context(), render.StatusCtxKey, status)
|
||||
render.JSON(w, r.WithContext(ctx), group)
|
||||
} else {
|
||||
render.JSON(w, r, group)
|
||||
}
|
||||
}
|
||||
|
||||
func getGroupByName(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
name := getURLParam(r, "name")
|
||||
renderGroup(w, r, name, http.StatusOK)
|
||||
}
|
||||
|
||||
func deleteGroup(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
name := getURLParam(r, "name")
|
||||
err = dataprovider.DeleteGroup(name, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
sendAPIResponse(w, r, err, "Group deleted", http.StatusOK)
|
||||
}
|
|
@ -26,7 +26,7 @@ func getUserConnection(w http.ResponseWriter, r *http.Request) (*Connection, err
|
|||
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||
return nil, fmt.Errorf("invalid token claims %w", err)
|
||||
}
|
||||
user, err := dataprovider.UserExists(claims.Username)
|
||||
user, err := dataprovider.GetUserWithGroupSettings(claims.Username)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, nil, "Unable to retrieve your user", getRespStatus(err))
|
||||
return nil, err
|
||||
|
@ -461,22 +461,22 @@ func updateUserProfile(w http.ResponseWriter, r *http.Request) {
|
|||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.UserExists(claims.Username)
|
||||
user, userMerged, err := dataprovider.GetUserVariants(claims.Username)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
if !user.CanManagePublicKeys() && !user.CanChangeAPIKeyAuth() && !user.CanChangeInfo() {
|
||||
if !userMerged.CanManagePublicKeys() && !userMerged.CanChangeAPIKeyAuth() && !userMerged.CanChangeInfo() {
|
||||
sendAPIResponse(w, r, nil, "You are not allowed to change anything", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if user.CanManagePublicKeys() {
|
||||
if userMerged.CanManagePublicKeys() {
|
||||
user.PublicKeys = req.PublicKeys
|
||||
}
|
||||
if user.CanChangeAPIKeyAuth() {
|
||||
if userMerged.CanChangeAPIKeyAuth() {
|
||||
user.Filters.AllowAPIKeyAuth = req.AllowAPIKeyAuth
|
||||
}
|
||||
if user.CanChangeInfo() {
|
||||
if userMerged.CanChangeInfo() {
|
||||
user.Email = req.Email
|
||||
user.Description = req.Description
|
||||
}
|
||||
|
@ -518,14 +518,14 @@ func doChangeUserPassword(r *http.Request, currentPassword, newPassword, confirm
|
|||
if err != nil || claims.Username == "" {
|
||||
return errors.New("invalid token claims")
|
||||
}
|
||||
user, err := dataprovider.CheckUserAndPass(claims.Username, currentPassword, util.GetIPFromRemoteAddress(r.RemoteAddr),
|
||||
_, err = dataprovider.CheckUserAndPass(claims.Username, currentPassword, util.GetIPFromRemoteAddress(r.RemoteAddr),
|
||||
getProtocolFromRequest(r))
|
||||
if err != nil {
|
||||
return util.NewValidationError("current password does not match")
|
||||
}
|
||||
user.Password = newPassword
|
||||
|
||||
return dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
return dataprovider.UpdateUserPassword(claims.Username, newPassword, dataprovider.ActionExecutorSelf,
|
||||
util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
}
|
||||
|
||||
func setModificationTimeFromHeader(r *http.Request, c *Connection, filePath string) {
|
||||
|
|
|
@ -163,13 +163,17 @@ func loadData(w http.ResponseWriter, r *http.Request) {
|
|||
func restoreBackup(content []byte, inputFile string, scanQuota, mode int, executor, ipAddress string) error {
|
||||
dump, err := dataprovider.ParseDumpData(content)
|
||||
if err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("Unable to parse backup content: %v", err))
|
||||
return util.NewValidationError(fmt.Sprintf("unable to parse backup content: %v", err))
|
||||
}
|
||||
|
||||
if err = RestoreFolders(dump.Folders, inputFile, mode, scanQuota, executor, ipAddress); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = RestoreGroups(dump.Groups, inputFile, mode, executor, ipAddress); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = RestoreUsers(dump.Users, inputFile, mode, scanQuota, executor, ipAddress); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -229,12 +233,12 @@ func RestoreFolders(folders []vfs.BaseVirtualFolder, inputFile string, mode, sca
|
|||
}
|
||||
folder.ID = f.ID
|
||||
folder.Name = f.Name
|
||||
err = dataprovider.UpdateFolder(&folder, f.Users, executor, ipAddress)
|
||||
logger.Debug(logSender, "", "restoring existing folder: %+v, dump file: %#v, error: %v", folder, inputFile, err)
|
||||
err = dataprovider.UpdateFolder(&folder, f.Users, f.Groups, executor, ipAddress)
|
||||
logger.Debug(logSender, "", "restoring existing folder %#v, dump file: %#v, error: %v", folder.Name, inputFile, err)
|
||||
} else {
|
||||
folder.Users = nil
|
||||
err = dataprovider.AddFolder(&folder)
|
||||
logger.Debug(logSender, "", "adding new folder: %+v, dump file: %#v, error: %v", folder, inputFile, err)
|
||||
logger.Debug(logSender, "", "adding new folder %#v, dump file: %#v, error: %v", folder.Name, inputFile, err)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to restore folder %#v: %w", folder.Name, err)
|
||||
|
@ -264,12 +268,10 @@ func RestoreShares(shares []dataprovider.Share, inputFile string, mode int, exec
|
|||
}
|
||||
share.ID = s.ID
|
||||
err = dataprovider.UpdateShare(&share, executor, ipAddress)
|
||||
share.Password = redactedSecret
|
||||
logger.Debug(logSender, "", "restoring existing share: %+v, dump file: %#v, error: %v", share, inputFile, err)
|
||||
logger.Debug(logSender, "", "restoring existing share %#v, dump file: %#v, error: %v", share.ShareID, inputFile, err)
|
||||
} else {
|
||||
err = dataprovider.AddShare(&share, executor, ipAddress)
|
||||
share.Password = redactedSecret
|
||||
logger.Debug(logSender, "", "adding new share: %+v, dump file: %#v, error: %v", share, inputFile, err)
|
||||
logger.Debug(logSender, "", "adding new share %#v, dump file: %#v, error: %v", share.ShareID, inputFile, err)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to restore share %#v: %w", share.ShareID, err)
|
||||
|
@ -294,12 +296,10 @@ func RestoreAPIKeys(apiKeys []dataprovider.APIKey, inputFile string, mode int, e
|
|||
}
|
||||
apiKey.ID = k.ID
|
||||
err = dataprovider.UpdateAPIKey(&apiKey, executor, ipAddress)
|
||||
apiKey.Key = redactedSecret
|
||||
logger.Debug(logSender, "", "restoring existing API key: %+v, dump file: %#v, error: %v", apiKey, inputFile, err)
|
||||
logger.Debug(logSender, "", "restoring existing API key %#v, dump file: %#v, error: %v", apiKey.KeyID, inputFile, err)
|
||||
} else {
|
||||
err = dataprovider.AddAPIKey(&apiKey, executor, ipAddress)
|
||||
apiKey.Key = redactedSecret
|
||||
logger.Debug(logSender, "", "adding new API key: %+v, dump file: %#v, error: %v", apiKey, inputFile, err)
|
||||
logger.Debug(logSender, "", "adding new API key %#v, dump file: %#v, error: %v", apiKey.KeyID, inputFile, err)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to restore API key %#v: %w", apiKey.KeyID, err)
|
||||
|
@ -321,12 +321,10 @@ func RestoreAdmins(admins []dataprovider.Admin, inputFile string, mode int, exec
|
|||
admin.ID = a.ID
|
||||
admin.Username = a.Username
|
||||
err = dataprovider.UpdateAdmin(&admin, executor, ipAddress)
|
||||
admin.Password = redactedSecret
|
||||
logger.Debug(logSender, "", "restoring existing admin: %+v, dump file: %#v, error: %v", admin, inputFile, err)
|
||||
logger.Debug(logSender, "", "restoring existing admin %#v, dump file: %#v, error: %v", admin.Username, inputFile, err)
|
||||
} else {
|
||||
err = dataprovider.AddAdmin(&admin, executor, ipAddress)
|
||||
admin.Password = redactedSecret
|
||||
logger.Debug(logSender, "", "adding new admin: %+v, dump file: %#v, error: %v", admin, inputFile, err)
|
||||
logger.Debug(logSender, "", "adding new admin %#v, dump file: %#v, error: %v", admin.Username, inputFile, err)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to restore admin %#v: %w", admin.Username, err)
|
||||
|
@ -336,6 +334,31 @@ func RestoreAdmins(admins []dataprovider.Admin, inputFile string, mode int, exec
|
|||
return nil
|
||||
}
|
||||
|
||||
// RestoreGroups restores the specified groups
|
||||
func RestoreGroups(groups []dataprovider.Group, inputFile string, mode int, executor, ipAddress string) error {
|
||||
for _, group := range groups {
|
||||
group := group // pin
|
||||
g, err := dataprovider.GroupExists(group.Name)
|
||||
if err == nil {
|
||||
if mode == 1 {
|
||||
logger.Debug(logSender, "", "loaddata mode 1, existing group %#v not updated", g.Name)
|
||||
continue
|
||||
}
|
||||
group.ID = g.ID
|
||||
group.Name = g.Name
|
||||
err = dataprovider.UpdateGroup(&group, g.Users, executor, ipAddress)
|
||||
logger.Debug(logSender, "", "restoring existing group: %#v, dump file: %#v, error: %v", group.Name, inputFile, err)
|
||||
} else {
|
||||
err = dataprovider.AddGroup(&group, executor, ipAddress)
|
||||
logger.Debug(logSender, "", "adding new group: %#v, dump file: %#v, error: %v", group.Name, inputFile, err)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to restore group %#v: %w", group.Name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestoreUsers restores the specified users
|
||||
func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota int, executor, ipAddress string) error {
|
||||
for _, user := range users {
|
||||
|
@ -349,15 +372,13 @@ func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota i
|
|||
user.ID = u.ID
|
||||
user.Username = u.Username
|
||||
err = dataprovider.UpdateUser(&user, executor, ipAddress)
|
||||
user.Password = redactedSecret
|
||||
logger.Debug(logSender, "", "restoring existing user: %+v, dump file: %#v, error: %v", user, inputFile, err)
|
||||
logger.Debug(logSender, "", "restoring existing user: %#v, dump file: %#v, error: %v", user.Username, inputFile, err)
|
||||
if mode == 2 && err == nil {
|
||||
disconnectUser(user.Username)
|
||||
}
|
||||
} else {
|
||||
err = dataprovider.AddUser(&user, executor, ipAddress)
|
||||
user.Password = redactedSecret
|
||||
logger.Debug(logSender, "", "adding new user: %+v, dump file: %#v, error: %v", user, inputFile, err)
|
||||
logger.Debug(logSender, "", "adding new user: %#v, dump file: %#v, error: %v", user.Username, inputFile, err)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to restore user %#v: %w", user.Username, err)
|
||||
|
|
|
@ -82,7 +82,7 @@ func getMetadataChecks(w http.ResponseWriter, r *http.Request) {
|
|||
func startMetadataCheck(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
user, err := dataprovider.UserExists(getURLParam(r, "username"))
|
||||
user, err := dataprovider.GetUserWithGroupSettings(getURLParam(r, "username"))
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
|
|
|
@ -141,7 +141,7 @@ func updateUserTransferQuotaUsage(w http.ResponseWriter, r *http.Request) {
|
|||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.UserExists(getURLParam(r, "username"))
|
||||
user, err := dataprovider.GetUserWithGroupSettings(getURLParam(r, "username"))
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
|
@ -171,7 +171,7 @@ func doUpdateUserQuotaUsage(w http.ResponseWriter, r *http.Request, username str
|
|||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.UserExists(username)
|
||||
user, err := dataprovider.GetUserWithGroupSettings(username)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
|
@ -228,7 +228,7 @@ func doStartUserQuotaScan(w http.ResponseWriter, r *http.Request, username strin
|
|||
sendAPIResponse(w, r, nil, "Quota tracking is disabled!", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.UserExists(username)
|
||||
user, err := dataprovider.GetUserWithGroupSettings(username)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
|
|
|
@ -18,7 +18,7 @@ func getRetentionChecks(w http.ResponseWriter, r *http.Request) {
|
|||
func startRetentionCheck(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
username := getURLParam(r, "username")
|
||||
user, err := dataprovider.UserExists(username)
|
||||
user, err := dataprovider.GetUserWithGroupSettings(username)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
|
|
|
@ -406,7 +406,7 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, s
|
|||
return share, nil, dataprovider.ErrInvalidCredentials
|
||||
}
|
||||
}
|
||||
user, err := dataprovider.UserExists(share.Username)
|
||||
user, err := dataprovider.GetUserWithGroupSettings(share.Username)
|
||||
if err != nil {
|
||||
renderError(err, "", getRespStatus(err))
|
||||
return share, nil, err
|
||||
|
@ -428,7 +428,7 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, s
|
|||
|
||||
func validateBrowsableShare(share dataprovider.Share, connection *Connection) error {
|
||||
if len(share.Paths) != 1 {
|
||||
return util.NewValidationError("A share with multiple paths is not browsable")
|
||||
return util.NewValidationError("a share with multiple paths is not browsable")
|
||||
}
|
||||
basePath := share.Paths[0]
|
||||
info, err := connection.Stat(basePath, 0)
|
||||
|
@ -436,7 +436,7 @@ func validateBrowsableShare(share dataprovider.Share, connection *Connection) er
|
|||
return fmt.Errorf("unable to check the share directory: %w", err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return util.NewValidationError("The shared object is not a directory and so it is not browsable")
|
||||
return util.NewValidationError("the shared object is not a directory and so it is not browsable")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -529,19 +529,19 @@ func handleForgotPassword(r *http.Request, username string, isAdmin bool) error
|
|||
var user dataprovider.User
|
||||
|
||||
if username == "" {
|
||||
return util.NewValidationError("Username is mandatory")
|
||||
return util.NewValidationError("username is mandatory")
|
||||
}
|
||||
if isAdmin {
|
||||
admin, err = dataprovider.AdminExists(username)
|
||||
email = admin.Email
|
||||
subject = fmt.Sprintf("Email Verification Code for admin %#v", username)
|
||||
} else {
|
||||
user, err = dataprovider.UserExists(username)
|
||||
user, err = dataprovider.GetUserWithGroupSettings(username)
|
||||
email = user.Email
|
||||
subject = fmt.Sprintf("Email Verification Code for user %#v", username)
|
||||
if err == nil {
|
||||
if !isUserAllowedToResetPassword(r, &user) {
|
||||
return util.NewValidationError("You are not allowed to reset your password")
|
||||
return util.NewValidationError("you are not allowed to reset your password")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -584,47 +584,47 @@ func handleResetPassword(r *http.Request, code, newPassword string, isAdmin bool
|
|||
var err error
|
||||
|
||||
if newPassword == "" {
|
||||
return &admin, &user, util.NewValidationError("Please set a password")
|
||||
return &admin, &user, util.NewValidationError("please set a password")
|
||||
}
|
||||
if code == "" {
|
||||
return &admin, &user, util.NewValidationError("Please set a confirmation code")
|
||||
return &admin, &user, util.NewValidationError("please set a confirmation code")
|
||||
}
|
||||
c, ok := resetCodes.Load(code)
|
||||
if !ok {
|
||||
return &admin, &user, util.NewValidationError("Confirmation code not found")
|
||||
return &admin, &user, util.NewValidationError("confirmation code not found")
|
||||
}
|
||||
resetCode := c.(*resetCode)
|
||||
if resetCode.IsAdmin != isAdmin {
|
||||
return &admin, &user, util.NewValidationError("Invalid confirmation code")
|
||||
return &admin, &user, util.NewValidationError("invalid confirmation code")
|
||||
}
|
||||
if isAdmin {
|
||||
admin, err = dataprovider.AdminExists(resetCode.Username)
|
||||
if err != nil {
|
||||
return &admin, &user, util.NewValidationError("Unable to associate the confirmation code with an existing admin")
|
||||
return &admin, &user, util.NewValidationError("unable to associate the confirmation code with an existing admin")
|
||||
}
|
||||
admin.Password = newPassword
|
||||
err = dataprovider.UpdateAdmin(&admin, admin.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
err = dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
if err != nil {
|
||||
return &admin, &user, util.NewGenericError(fmt.Sprintf("Unable to set the new password: %v", err))
|
||||
return &admin, &user, util.NewGenericError(fmt.Sprintf("unable to set the new password: %v", err))
|
||||
}
|
||||
} else {
|
||||
user, err = dataprovider.UserExists(resetCode.Username)
|
||||
if err != nil {
|
||||
return &admin, &user, util.NewValidationError("Unable to associate the confirmation code with an existing user")
|
||||
}
|
||||
if err == nil {
|
||||
if !isUserAllowedToResetPassword(r, &user) {
|
||||
return &admin, &user, util.NewValidationError("You are not allowed to reset your password")
|
||||
}
|
||||
}
|
||||
user.Password = newPassword
|
||||
err = dataprovider.UpdateUser(&user, user.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
if err != nil {
|
||||
return &admin, &user, util.NewGenericError(fmt.Sprintf("Unable to set the new password: %v", err))
|
||||
resetCodes.Delete(code)
|
||||
return &admin, &user, nil
|
||||
}
|
||||
user, err = dataprovider.GetUserWithGroupSettings(resetCode.Username)
|
||||
if err != nil {
|
||||
return &admin, &user, util.NewValidationError("Unable to associate the confirmation code with an existing user")
|
||||
}
|
||||
if err == nil {
|
||||
if !isUserAllowedToResetPassword(r, &user) {
|
||||
return &admin, &user, util.NewValidationError("you are not allowed to reset your password")
|
||||
}
|
||||
}
|
||||
resetCodes.Delete(code)
|
||||
return &admin, &user, nil
|
||||
err = dataprovider.UpdateUserPassword(user.Username, newPassword, dataprovider.ActionExecutorSelf,
|
||||
util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
if err == nil {
|
||||
resetCodes.Delete(code)
|
||||
}
|
||||
return &admin, &user, err
|
||||
}
|
||||
|
||||
func isUserAllowedToResetPassword(r *http.Request, user *dataprovider.User) bool {
|
||||
|
|
|
@ -42,6 +42,7 @@ const (
|
|||
userPath = "/api/v2/users"
|
||||
versionPath = "/api/v2/version"
|
||||
folderPath = "/api/v2/folders"
|
||||
groupPath = "/api/v2/groups"
|
||||
serverStatusPath = "/api/v2/status"
|
||||
dumpDataPath = "/api/v2/dumpdata"
|
||||
loadDataPath = "/api/v2/loaddata"
|
||||
|
@ -101,6 +102,8 @@ const (
|
|||
webConnectionsPathDefault = "/web/admin/connections"
|
||||
webFoldersPathDefault = "/web/admin/folders"
|
||||
webFolderPathDefault = "/web/admin/folder"
|
||||
webGroupsPathDefault = "/web/admin/groups"
|
||||
webGroupPathDefault = "/web/admin/group"
|
||||
webStatusPathDefault = "/web/admin/status"
|
||||
webAdminsPathDefault = "/web/admin/managers"
|
||||
webAdminPathDefault = "/web/admin/manager"
|
||||
|
@ -180,6 +183,8 @@ var (
|
|||
webConnectionsPath string
|
||||
webFoldersPath string
|
||||
webFolderPath string
|
||||
webGroupsPath string
|
||||
webGroupPath string
|
||||
webStatusPath string
|
||||
webAdminsPath string
|
||||
webAdminPath string
|
||||
|
@ -764,6 +769,8 @@ func updateWebAdminURLs(baseURL string) {
|
|||
webConnectionsPath = path.Join(baseURL, webConnectionsPathDefault)
|
||||
webFoldersPath = path.Join(baseURL, webFoldersPathDefault)
|
||||
webFolderPath = path.Join(baseURL, webFolderPathDefault)
|
||||
webGroupsPath = path.Join(baseURL, webGroupsPathDefault)
|
||||
webGroupPath = path.Join(baseURL, webGroupPathDefault)
|
||||
webStatusPath = path.Join(baseURL, webStatusPathDefault)
|
||||
webAdminsPath = path.Join(baseURL, webAdminsPathDefault)
|
||||
webAdminPath = path.Join(baseURL, webAdminPathDefault)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -593,11 +593,36 @@ func TestInvalidToken(t *testing.T) {
|
|||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
addGroup(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
updateGroup(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
deleteGroup(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
server.handleWebAddAdminPost(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
server.handleWebAddGroupPost(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
server.handleWebUpdateGroupPost(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
server.handleWebClientTwoFactorRecoveryPost(rr, req)
|
||||
assert.Equal(t, http.StatusNotFound, rr.Code)
|
||||
|
|
|
@ -418,7 +418,7 @@ func authenticateUserWithAPIKey(username, keyID string, tokenAuth *jwtauth.JWTAu
|
|||
if err := common.Config.ExecutePostConnectHook(ipAddr, protocol); err != nil {
|
||||
return err
|
||||
}
|
||||
user, err := dataprovider.UserExists(username)
|
||||
user, err := dataprovider.GetUserWithGroupSettings(username)
|
||||
if err != nil {
|
||||
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
|
||||
dataprovider.LoginMethodPassword, ipAddr, err)
|
||||
|
|
|
@ -260,10 +260,6 @@ func (s *httpdServer) handleWebClientPasswordResetPost(w http.ResponseWriter, r
|
|||
}
|
||||
_, user, err := handleResetPassword(r, r.Form.Get("code"), r.Form.Get("password"), false)
|
||||
if err != nil {
|
||||
if e, ok := err.(*util.ValidationError); ok {
|
||||
s.renderClientResetPwdPage(w, e.GetErrorString(), ipAddr)
|
||||
return
|
||||
}
|
||||
s.renderClientResetPwdPage(w, err.Error(), ipAddr)
|
||||
return
|
||||
}
|
||||
|
@ -305,12 +301,12 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter
|
|||
s.renderClientTwoFactorRecoveryPage(w, err.Error(), ipAddr)
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.UserExists(username)
|
||||
user, userMerged, err := dataprovider.GetUserVariants(username)
|
||||
if err != nil {
|
||||
s.renderClientTwoFactorRecoveryPage(w, "Invalid credentials", ipAddr)
|
||||
return
|
||||
}
|
||||
if !user.Filters.TOTPConfig.Enabled || !util.IsStringInSlice(common.ProtocolHTTP, user.Filters.TOTPConfig.Protocols) {
|
||||
if !userMerged.Filters.TOTPConfig.Enabled || !util.IsStringInSlice(common.ProtocolHTTP, userMerged.Filters.TOTPConfig.Protocols) {
|
||||
s.renderClientTwoFactorPage(w, "Two factory authentication is not enabled", ipAddr)
|
||||
return
|
||||
}
|
||||
|
@ -332,7 +328,7 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter
|
|||
return
|
||||
}
|
||||
connectionID := fmt.Sprintf("%v_%v", getProtocolFromRequest(r), xid.New().String())
|
||||
s.loginUser(w, r, &user, connectionID, ipAddr, true,
|
||||
s.loginUser(w, r, &userMerged, connectionID, ipAddr, true,
|
||||
s.renderClientTwoFactorRecoveryPage)
|
||||
return
|
||||
}
|
||||
|
@ -362,7 +358,7 @@ func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *htt
|
|||
s.renderClientTwoFactorPage(w, err.Error(), ipAddr)
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.UserExists(username)
|
||||
user, err := dataprovider.GetUserWithGroupSettings(username)
|
||||
if err != nil {
|
||||
s.renderClientTwoFactorPage(w, "Invalid credentials", ipAddr)
|
||||
return
|
||||
|
@ -912,7 +908,7 @@ func (s *httpdServer) checkCookieExpiration(w http.ResponseWriter, r *http.Reque
|
|||
}
|
||||
|
||||
func (s *httpdServer) refreshClientToken(w http.ResponseWriter, r *http.Request, tokenClaims jwtTokenClaims) {
|
||||
user, err := dataprovider.UserExists(tokenClaims.Username)
|
||||
user, err := dataprovider.GetUserWithGroupSettings(tokenClaims.Username)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -1211,6 +1207,11 @@ func (s *httpdServer) initializeRouter() {
|
|||
router.With(s.checkPerm(dataprovider.PermAdminAddUsers)).Post(folderPath, addFolder)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(folderPath+"/{name}", updateFolder)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(folderPath+"/{name}", deleteFolder)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Get(groupPath, getGroups)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Get(groupPath+"/{name}", getGroupByName)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Post(groupPath, addGroup)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Put(groupPath+"/{name}", updateGroup)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Delete(groupPath+"/{name}", deleteGroup)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(dumpDataPath, dumpData)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(loadDataPath, loadData)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(loadDataPath, loadDataFromRequest)
|
||||
|
@ -1519,6 +1520,17 @@ func (s *httpdServer) setupWebAdminRoutes() {
|
|||
router.With(s.checkPerm(dataprovider.PermAdminAddUsers)).Post(webUserPath, s.handleWebAddUserPost)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Post(webUserPath+"/{username}",
|
||||
s.handleWebUpdateUserPost)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie).
|
||||
Get(webGroupsPath, s.handleWebGetGroups)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie).
|
||||
Get(webGroupPath, s.handleWebAddGroupGet)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Post(webGroupPath, s.handleWebAddGroupPost)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie).
|
||||
Get(webGroupPath+"/{name}", s.handleWebUpdateGroupGet)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Post(webGroupPath+"/{name}",
|
||||
s.handleWebUpdateGroupPost)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageGroups), verifyCSRFHeader).
|
||||
Delete(webGroupPath+"/{name}", deleteGroup)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminViewConnections), s.refreshCookie).
|
||||
Get(webConnectionsPath, s.handleWebGetConnections)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie).
|
||||
|
|
|
@ -43,40 +43,51 @@ const (
|
|||
folderPageModeTemplate
|
||||
)
|
||||
|
||||
type groupPageMode int
|
||||
|
||||
const (
|
||||
templateAdminDir = "webadmin"
|
||||
templateBase = "base.html"
|
||||
templateBaseLogin = "baselogin.html"
|
||||
templateFsConfig = "fsconfig.html"
|
||||
templateUsers = "users.html"
|
||||
templateUser = "user.html"
|
||||
templateAdmins = "admins.html"
|
||||
templateAdmin = "admin.html"
|
||||
templateConnections = "connections.html"
|
||||
templateFolders = "folders.html"
|
||||
templateFolder = "folder.html"
|
||||
templateMessage = "message.html"
|
||||
templateStatus = "status.html"
|
||||
templateLogin = "login.html"
|
||||
templateDefender = "defender.html"
|
||||
templateProfile = "profile.html"
|
||||
templateChangePwd = "changepassword.html"
|
||||
templateMaintenance = "maintenance.html"
|
||||
templateMFA = "mfa.html"
|
||||
templateSetup = "adminsetup.html"
|
||||
pageUsersTitle = "Users"
|
||||
pageAdminsTitle = "Admins"
|
||||
pageConnectionsTitle = "Connections"
|
||||
pageStatusTitle = "Status"
|
||||
pageFoldersTitle = "Folders"
|
||||
pageProfileTitle = "My profile"
|
||||
pageChangePwdTitle = "Change password"
|
||||
pageMaintenanceTitle = "Maintenance"
|
||||
pageDefenderTitle = "Defender"
|
||||
pageForgotPwdTitle = "SFTPGo Admin - Forgot password"
|
||||
pageResetPwdTitle = "SFTPGo Admin - Reset password"
|
||||
pageSetupTitle = "Create first admin user"
|
||||
defaultQueryLimit = 500
|
||||
groupPageModeAdd groupPageMode = iota + 1
|
||||
groupPageModeUpdate
|
||||
)
|
||||
|
||||
const (
|
||||
templateAdminDir = "webadmin"
|
||||
templateBase = "base.html"
|
||||
templateBaseLogin = "baselogin.html"
|
||||
templateFsConfig = "fsconfig.html"
|
||||
templateSharedComponents = "sharedcomponents.html"
|
||||
templateUsers = "users.html"
|
||||
templateUser = "user.html"
|
||||
templateAdmins = "admins.html"
|
||||
templateAdmin = "admin.html"
|
||||
templateConnections = "connections.html"
|
||||
templateGroups = "groups.html"
|
||||
templateGroup = "group.html"
|
||||
templateFolders = "folders.html"
|
||||
templateFolder = "folder.html"
|
||||
templateMessage = "message.html"
|
||||
templateStatus = "status.html"
|
||||
templateLogin = "login.html"
|
||||
templateDefender = "defender.html"
|
||||
templateProfile = "profile.html"
|
||||
templateChangePwd = "changepassword.html"
|
||||
templateMaintenance = "maintenance.html"
|
||||
templateMFA = "mfa.html"
|
||||
templateSetup = "adminsetup.html"
|
||||
pageUsersTitle = "Users"
|
||||
pageAdminsTitle = "Admins"
|
||||
pageConnectionsTitle = "Connections"
|
||||
pageStatusTitle = "Status"
|
||||
pageFoldersTitle = "Folders"
|
||||
pageGroupsTitle = "Groups"
|
||||
pageProfileTitle = "My profile"
|
||||
pageChangePwdTitle = "Change password"
|
||||
pageMaintenanceTitle = "Maintenance"
|
||||
pageDefenderTitle = "Defender"
|
||||
pageForgotPwdTitle = "SFTPGo Admin - Forgot password"
|
||||
pageResetPwdTitle = "SFTPGo Admin - Reset password"
|
||||
pageSetupTitle = "Create first admin user"
|
||||
defaultQueryLimit = 500
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -93,6 +104,8 @@ type basePage struct {
|
|||
AdminURL string
|
||||
QuotaScanURL string
|
||||
ConnectionsURL string
|
||||
GroupsURL string
|
||||
GroupURL string
|
||||
FoldersURL string
|
||||
FolderURL string
|
||||
FolderTemplateURL string
|
||||
|
@ -109,6 +122,7 @@ type basePage struct {
|
|||
AdminsTitle string
|
||||
ConnectionsTitle string
|
||||
FoldersTitle string
|
||||
GroupsTitle string
|
||||
StatusTitle string
|
||||
MaintenanceTitle string
|
||||
DefenderTitle string
|
||||
|
@ -135,6 +149,11 @@ type foldersPage struct {
|
|||
Folders []vfs.BaseVirtualFolder
|
||||
}
|
||||
|
||||
type groupsPage struct {
|
||||
basePage
|
||||
Groups []dataprovider.Group
|
||||
}
|
||||
|
||||
type connectionsPage struct {
|
||||
basePage
|
||||
Connections []common.ConnectionStatus
|
||||
|
@ -148,6 +167,7 @@ type statusPage struct {
|
|||
type fsWrapper struct {
|
||||
vfs.Filesystem
|
||||
IsUserPage bool
|
||||
IsGroupPage bool
|
||||
HasUsersBaseDir bool
|
||||
DirPath string
|
||||
}
|
||||
|
@ -166,6 +186,7 @@ type userPage struct {
|
|||
RedactedSecret string
|
||||
Mode userPageMode
|
||||
VirtualFolders []vfs.BaseVirtualFolder
|
||||
Groups []dataprovider.Group
|
||||
CanImpersonate bool
|
||||
FsWrapper fsWrapper
|
||||
}
|
||||
|
@ -228,6 +249,20 @@ type folderPage struct {
|
|||
FsWrapper fsWrapper
|
||||
}
|
||||
|
||||
type groupPage struct {
|
||||
basePage
|
||||
Group *dataprovider.Group
|
||||
Error string
|
||||
Mode groupPageMode
|
||||
ValidPerms []string
|
||||
ValidLoginMethods []string
|
||||
ValidProtocols []string
|
||||
TwoFactorProtocols []string
|
||||
WebClientOptions []string
|
||||
VirtualFolders []vfs.BaseVirtualFolder
|
||||
FsWrapper fsWrapper
|
||||
}
|
||||
|
||||
type messagePage struct {
|
||||
basePage
|
||||
Error string
|
||||
|
@ -247,6 +282,7 @@ func loadAdminTemplates(templatesPath string) {
|
|||
}
|
||||
userPaths := []string{
|
||||
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateSharedComponents),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateFsConfig),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateUser),
|
||||
}
|
||||
|
@ -283,6 +319,16 @@ func loadAdminTemplates(templatesPath string) {
|
|||
filepath.Join(templatesPath, templateAdminDir, templateFsConfig),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateFolder),
|
||||
}
|
||||
groupsPaths := []string{
|
||||
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateGroups),
|
||||
}
|
||||
groupPaths := []string{
|
||||
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateFsConfig),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateSharedComponents),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateGroup),
|
||||
}
|
||||
statusPaths := []string{
|
||||
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateStatus),
|
||||
|
@ -336,6 +382,8 @@ func loadAdminTemplates(templatesPath string) {
|
|||
adminTmpl := util.LoadTemplate(nil, adminPaths...)
|
||||
connectionsTmpl := util.LoadTemplate(nil, connectionsPaths...)
|
||||
messageTmpl := util.LoadTemplate(nil, messagePaths...)
|
||||
groupsTmpl := util.LoadTemplate(nil, groupsPaths...)
|
||||
groupTmpl := util.LoadTemplate(fsBaseTpl, groupPaths...)
|
||||
foldersTmpl := util.LoadTemplate(nil, foldersPaths...)
|
||||
folderTmpl := util.LoadTemplate(fsBaseTpl, folderPaths...)
|
||||
statusTmpl := util.LoadTemplate(nil, statusPaths...)
|
||||
|
@ -357,6 +405,8 @@ func loadAdminTemplates(templatesPath string) {
|
|||
adminTemplates[templateAdmin] = adminTmpl
|
||||
adminTemplates[templateConnections] = connectionsTmpl
|
||||
adminTemplates[templateMessage] = messageTmpl
|
||||
adminTemplates[templateGroups] = groupsTmpl
|
||||
adminTemplates[templateGroup] = groupTmpl
|
||||
adminTemplates[templateFolders] = foldersTmpl
|
||||
adminTemplates[templateFolder] = folderTmpl
|
||||
adminTemplates[templateStatus] = statusTmpl
|
||||
|
@ -386,6 +436,8 @@ func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request)
|
|||
UserTemplateURL: webTemplateUser,
|
||||
AdminsURL: webAdminsPath,
|
||||
AdminURL: webAdminPath,
|
||||
GroupsURL: webGroupsPath,
|
||||
GroupURL: webGroupPath,
|
||||
FoldersURL: webFoldersPath,
|
||||
FolderURL: webFolderPath,
|
||||
FolderTemplateURL: webTemplateFolder,
|
||||
|
@ -404,6 +456,7 @@ func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request)
|
|||
AdminsTitle: pageAdminsTitle,
|
||||
ConnectionsTitle: pageConnectionsTitle,
|
||||
FoldersTitle: pageFoldersTitle,
|
||||
GroupsTitle: pageGroupsTitle,
|
||||
StatusTitle: pageStatusTitle,
|
||||
MaintenanceTitle: pageMaintenanceTitle,
|
||||
DefenderTitle: pageDefenderTitle,
|
||||
|
@ -595,7 +648,11 @@ func (s *httpdServer) renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Re
|
|||
func (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, user *dataprovider.User,
|
||||
mode userPageMode, error string,
|
||||
) {
|
||||
folders, err := s.getWebVirtualFolders(w, r, defaultQueryLimit)
|
||||
folders, err := s.getWebVirtualFolders(w, r, defaultQueryLimit, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
groups, err := s.getWebGroups(w, r, defaultQueryLimit, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -628,10 +685,12 @@ func (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, use
|
|||
WebClientOptions: sdk.WebClientOptions,
|
||||
RootDirPerms: user.GetPermissionsForPath("/"),
|
||||
VirtualFolders: folders,
|
||||
Groups: groups,
|
||||
CanImpersonate: os.Getuid() == 0,
|
||||
FsWrapper: fsWrapper{
|
||||
Filesystem: user.FsConfig,
|
||||
IsUserPage: true,
|
||||
IsGroupPage: false,
|
||||
HasUsersBaseDir: dataprovider.HasUsersBaseDir(),
|
||||
DirPath: user.HomeDir,
|
||||
},
|
||||
|
@ -639,7 +698,52 @@ func (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, use
|
|||
renderAdminTemplate(w, templateUser, data)
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderFolderPage(w http.ResponseWriter, r *http.Request, folder vfs.BaseVirtualFolder, mode folderPageMode, error string) {
|
||||
func (s *httpdServer) renderGroupPage(w http.ResponseWriter, r *http.Request, group dataprovider.Group,
|
||||
mode groupPageMode, error string,
|
||||
) {
|
||||
folders, err := s.getWebVirtualFolders(w, r, defaultQueryLimit, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
group.SetEmptySecretsIfNil()
|
||||
group.UserSettings.FsConfig.RedactedSecret = redactedSecret
|
||||
var title, currentURL string
|
||||
switch mode {
|
||||
case groupPageModeAdd:
|
||||
title = "Add a new group"
|
||||
currentURL = webGroupPath
|
||||
case groupPageModeUpdate:
|
||||
title = "Update group"
|
||||
currentURL = fmt.Sprintf("%v/%v", webGroupPath, url.PathEscape(group.Name))
|
||||
}
|
||||
group.UserSettings.FsConfig.RedactedSecret = redactedSecret
|
||||
group.UserSettings.FsConfig.SetEmptySecretsIfNil()
|
||||
|
||||
data := groupPage{
|
||||
basePage: s.getBasePageData(title, currentURL, r),
|
||||
Error: error,
|
||||
Group: &group,
|
||||
Mode: mode,
|
||||
ValidPerms: dataprovider.ValidPerms,
|
||||
ValidLoginMethods: dataprovider.ValidLoginMethods,
|
||||
ValidProtocols: dataprovider.ValidProtocols,
|
||||
TwoFactorProtocols: dataprovider.MFAProtocols,
|
||||
WebClientOptions: sdk.WebClientOptions,
|
||||
VirtualFolders: folders,
|
||||
FsWrapper: fsWrapper{
|
||||
Filesystem: group.UserSettings.FsConfig,
|
||||
IsUserPage: false,
|
||||
IsGroupPage: true,
|
||||
HasUsersBaseDir: false,
|
||||
DirPath: group.UserSettings.HomeDir,
|
||||
},
|
||||
}
|
||||
renderAdminTemplate(w, templateGroup, data)
|
||||
}
|
||||
|
||||
func (s *httpdServer) renderFolderPage(w http.ResponseWriter, r *http.Request, folder vfs.BaseVirtualFolder,
|
||||
mode folderPageMode, error string,
|
||||
) {
|
||||
var title, currentURL string
|
||||
switch mode {
|
||||
case folderPageModeAdd:
|
||||
|
@ -663,6 +767,7 @@ func (s *httpdServer) renderFolderPage(w http.ResponseWriter, r *http.Request, f
|
|||
FsWrapper: fsWrapper{
|
||||
Filesystem: folder.FsConfig,
|
||||
IsUserPage: false,
|
||||
IsGroupPage: false,
|
||||
HasUsersBaseDir: false,
|
||||
DirPath: folder.MappedPath,
|
||||
},
|
||||
|
@ -763,9 +868,8 @@ func getVirtualFoldersFromPostFields(r *http.Request) []vfs.VirtualFolder {
|
|||
return virtualFolders
|
||||
}
|
||||
|
||||
func getUserPermissionsFromPostFields(r *http.Request) map[string][]string {
|
||||
func getSubDirPermissionsFromPostFields(r *http.Request) map[string][]string {
|
||||
permissions := make(map[string][]string)
|
||||
permissions["/"] = r.Form["permissions"]
|
||||
|
||||
for k := range r.Form {
|
||||
if strings.HasPrefix(k, "sub_perm_path") {
|
||||
|
@ -780,6 +884,13 @@ func getUserPermissionsFromPostFields(r *http.Request) map[string][]string {
|
|||
return permissions
|
||||
}
|
||||
|
||||
func getUserPermissionsFromPostFields(r *http.Request) map[string][]string {
|
||||
permissions := getSubDirPermissionsFromPostFields(r)
|
||||
permissions["/"] = r.Form["permissions"]
|
||||
|
||||
return permissions
|
||||
}
|
||||
|
||||
func getDataTransferLimitsFromPostFields(r *http.Request) ([]sdk.DataTransferLimit, error) {
|
||||
var result []sdk.DataTransferLimit
|
||||
|
||||
|
@ -928,6 +1039,27 @@ func getFilePatternsFromPostField(r *http.Request) []sdk.PatternsFilter {
|
|||
return result
|
||||
}
|
||||
|
||||
func getGroupsFromUserPostFields(r *http.Request) []sdk.GroupMapping {
|
||||
var groups []sdk.GroupMapping
|
||||
|
||||
primaryGroup := r.Form.Get("primary_group")
|
||||
if primaryGroup != "" {
|
||||
groups = append(groups, sdk.GroupMapping{
|
||||
Name: primaryGroup,
|
||||
Type: sdk.GroupTypePrimary,
|
||||
})
|
||||
}
|
||||
secondaryGroups := r.Form["secondary_groups"]
|
||||
for _, name := range secondaryGroups {
|
||||
groups = append(groups, sdk.GroupMapping{
|
||||
Name: name,
|
||||
Type: sdk.GroupTypeSecondary,
|
||||
})
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error) {
|
||||
var filters sdk.BaseUserFilters
|
||||
bwLimits, err := getBandwidthLimitsFromPostFields(r)
|
||||
|
@ -938,6 +1070,10 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
|
|||
if err != nil {
|
||||
return filters, err
|
||||
}
|
||||
maxFileSize, err := strconv.ParseInt(r.Form.Get("max_upload_file_size"), 10, 64)
|
||||
if err != nil {
|
||||
return filters, fmt.Errorf("invalid max upload file size: %w", err)
|
||||
}
|
||||
filters.BandwidthLimits = bwLimits
|
||||
filters.DataTransferLimits = dtLimits
|
||||
filters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
|
||||
|
@ -960,9 +1096,13 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
|
|||
}
|
||||
filters.DisableFsChecks = len(r.Form.Get("disable_fs_checks")) > 0
|
||||
filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
|
||||
filters.ExternalAuthCacheTime, err = strconv.ParseInt(r.Form.Get("external_auth_cache_time"), 10, 64)
|
||||
filters.StartDirectory = r.Form.Get("start_directory")
|
||||
return filters, err
|
||||
filters.MaxUploadFileSize = maxFileSize
|
||||
filters.ExternalAuthCacheTime, err = strconv.ParseInt(r.Form.Get("external_auth_cache_time"), 10, 64)
|
||||
if err != nil {
|
||||
return filters, fmt.Errorf("invalid external auth cache time: %w", err)
|
||||
}
|
||||
return filters, nil
|
||||
}
|
||||
|
||||
func getSecretFromFormField(r *http.Request, field string) *kms.Secret {
|
||||
|
@ -990,27 +1130,30 @@ func getS3Config(r *http.Request) (vfs.S3FsConfig, error) {
|
|||
config.KeyPrefix = r.Form.Get("s3_key_prefix")
|
||||
config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("s3_upload_part_size"), 10, 64)
|
||||
if err != nil {
|
||||
return config, err
|
||||
return config, fmt.Errorf("invalid s3 upload part size: %w", err)
|
||||
}
|
||||
config.UploadConcurrency, err = strconv.Atoi(r.Form.Get("s3_upload_concurrency"))
|
||||
if err != nil {
|
||||
return config, err
|
||||
return config, fmt.Errorf("invalid s3 upload concurrency: %w", err)
|
||||
}
|
||||
config.DownloadPartSize, err = strconv.ParseInt(r.Form.Get("s3_download_part_size"), 10, 64)
|
||||
if err != nil {
|
||||
return config, err
|
||||
return config, fmt.Errorf("invalid s3 download part size: %w", err)
|
||||
}
|
||||
config.DownloadConcurrency, err = strconv.Atoi(r.Form.Get("s3_download_concurrency"))
|
||||
if err != nil {
|
||||
return config, err
|
||||
return config, fmt.Errorf("invalid s3 download concurrency: %w", err)
|
||||
}
|
||||
config.ForcePathStyle = r.Form.Get("s3_force_path_style") != ""
|
||||
config.DownloadPartMaxTime, err = strconv.Atoi(r.Form.Get("s3_download_part_max_time"))
|
||||
if err != nil {
|
||||
return config, err
|
||||
return config, fmt.Errorf("invalid s3 download part max time: %w", err)
|
||||
}
|
||||
config.UploadPartMaxTime, err = strconv.Atoi(r.Form.Get("s3_upload_part_max_time"))
|
||||
return config, err
|
||||
if err != nil {
|
||||
return config, fmt.Errorf("invalid s3 upload part max time: %w", err)
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func getGCSConfig(r *http.Request) (vfs.GCSFsConfig, error) {
|
||||
|
@ -1059,7 +1202,10 @@ func getSFTPConfig(r *http.Request) (vfs.SFTPFsConfig, error) {
|
|||
config.Prefix = r.Form.Get("sftp_prefix")
|
||||
config.DisableCouncurrentReads = len(r.Form.Get("sftp_disable_concurrent_reads")) > 0
|
||||
config.BufferSize, err = strconv.ParseInt(r.Form.Get("sftp_buffer_size"), 10, 64)
|
||||
return config, err
|
||||
if err != nil {
|
||||
return config, fmt.Errorf("invalid SFTP buffer size: %w", err)
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func getAzureConfig(r *http.Request) (vfs.AzBlobFsConfig, error) {
|
||||
|
@ -1075,18 +1221,21 @@ func getAzureConfig(r *http.Request) (vfs.AzBlobFsConfig, error) {
|
|||
config.UseEmulator = len(r.Form.Get("az_use_emulator")) > 0
|
||||
config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("az_upload_part_size"), 10, 64)
|
||||
if err != nil {
|
||||
return config, err
|
||||
return config, fmt.Errorf("invalid azure upload part size: %w", err)
|
||||
}
|
||||
config.UploadConcurrency, err = strconv.Atoi(r.Form.Get("az_upload_concurrency"))
|
||||
if err != nil {
|
||||
return config, err
|
||||
return config, fmt.Errorf("invalid azure upload concurrency: %w", err)
|
||||
}
|
||||
config.DownloadPartSize, err = strconv.ParseInt(r.Form.Get("az_download_part_size"), 10, 64)
|
||||
if err != nil {
|
||||
return config, err
|
||||
return config, fmt.Errorf("invalid azure download part size: %w", err)
|
||||
}
|
||||
config.DownloadConcurrency, err = strconv.Atoi(r.Form.Get("az_download_concurrency"))
|
||||
return config, err
|
||||
if err != nil {
|
||||
return config, fmt.Errorf("invalid azure download concurrency: %w", err)
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func getFsConfigFromPostFields(r *http.Request) (vfs.Filesystem, error) {
|
||||
|
@ -1287,7 +1436,7 @@ func getQuotaLimits(r *http.Request) (int64, int, error) {
|
|||
}
|
||||
|
||||
func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
|
||||
var user dataprovider.User
|
||||
user := dataprovider.User{}
|
||||
err := r.ParseMultipartForm(maxRequestSize)
|
||||
if err != nil {
|
||||
return user, err
|
||||
|
@ -1370,13 +1519,71 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
|
|||
},
|
||||
VirtualFolders: getVirtualFoldersFromPostFields(r),
|
||||
FsConfig: fsConfig,
|
||||
Groups: getGroupsFromUserPostFields(r),
|
||||
}
|
||||
maxFileSize, err := strconv.ParseInt(r.Form.Get("max_upload_file_size"), 10, 64)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func getGroupFromPostFields(r *http.Request) (dataprovider.Group, error) {
|
||||
group := dataprovider.Group{}
|
||||
err := r.ParseMultipartForm(maxRequestSize)
|
||||
if err != nil {
|
||||
return user, fmt.Errorf("invalid max upload file size: %w", err)
|
||||
return group, err
|
||||
}
|
||||
user.Filters.MaxUploadFileSize = maxFileSize
|
||||
return user, err
|
||||
defer r.MultipartForm.RemoveAll() //nolint:errcheck
|
||||
|
||||
maxSessions, err := strconv.Atoi(r.Form.Get("max_sessions"))
|
||||
if err != nil {
|
||||
return group, fmt.Errorf("invalid max sessions: %w", err)
|
||||
}
|
||||
quotaSize, quotaFiles, err := getQuotaLimits(r)
|
||||
if err != nil {
|
||||
return group, err
|
||||
}
|
||||
bandwidthUL, err := strconv.ParseInt(r.Form.Get("upload_bandwidth"), 10, 64)
|
||||
if err != nil {
|
||||
return group, fmt.Errorf("invalid upload bandwidth: %w", err)
|
||||
}
|
||||
bandwidthDL, err := strconv.ParseInt(r.Form.Get("download_bandwidth"), 10, 64)
|
||||
if err != nil {
|
||||
return group, fmt.Errorf("invalid download bandwidth: %w", err)
|
||||
}
|
||||
dataTransferUL, dataTransferDL, dataTransferTotal, err := getTransferLimits(r)
|
||||
if err != nil {
|
||||
return group, err
|
||||
}
|
||||
fsConfig, err := getFsConfigFromPostFields(r)
|
||||
if err != nil {
|
||||
return group, err
|
||||
}
|
||||
filters, err := getFiltersFromUserPostFields(r)
|
||||
if err != nil {
|
||||
return group, err
|
||||
}
|
||||
group = dataprovider.Group{
|
||||
BaseGroup: sdk.BaseGroup{
|
||||
Name: r.Form.Get("name"),
|
||||
Description: r.Form.Get("description"),
|
||||
},
|
||||
UserSettings: dataprovider.GroupUserSettings{
|
||||
BaseGroupUserSettings: sdk.BaseGroupUserSettings{
|
||||
HomeDir: r.Form.Get("home_dir"),
|
||||
MaxSessions: maxSessions,
|
||||
QuotaSize: quotaSize,
|
||||
QuotaFiles: quotaFiles,
|
||||
Permissions: getSubDirPermissionsFromPostFields(r),
|
||||
UploadBandwidth: bandwidthUL,
|
||||
DownloadBandwidth: bandwidthDL,
|
||||
UploadDataTransfer: dataTransferUL,
|
||||
DownloadDataTransfer: dataTransferDL,
|
||||
TotalDataTransfer: dataTransferTotal,
|
||||
Filters: filters,
|
||||
},
|
||||
FsConfig: fsConfig,
|
||||
},
|
||||
VirtualFolders: getVirtualFoldersFromPostFields(r),
|
||||
}
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebAdminForgotPwd(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -1927,11 +2134,11 @@ func (s *httpdServer) handleWebAddUserPost(w http.ResponseWriter, r *http.Reques
|
|||
PublicKeys: user.PublicKeys,
|
||||
})
|
||||
err = dataprovider.AddUser(&user, claims.Username, ipAddr)
|
||||
if err == nil {
|
||||
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
|
||||
} else {
|
||||
if err != nil {
|
||||
s.renderUserPage(w, r, &user, userPageModeAdd, err.Error())
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -1979,14 +2186,14 @@ func (s *httpdServer) handleWebUpdateUserPost(w http.ResponseWriter, r *http.Req
|
|||
})
|
||||
|
||||
err = dataprovider.UpdateUser(&updatedUser, claims.Username, ipAddr)
|
||||
if err == nil {
|
||||
if r.Form.Get("disconnect") != "" {
|
||||
disconnectUser(user.Username)
|
||||
}
|
||||
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
|
||||
} else {
|
||||
s.renderUserPage(w, r, &user, userPageModeUpdate, err.Error())
|
||||
if err != nil {
|
||||
s.renderUserPage(w, r, &updatedUser, userPageModeUpdate, err.Error())
|
||||
return
|
||||
}
|
||||
if r.Form.Get("disconnect") != "" {
|
||||
disconnectUser(user.Username)
|
||||
}
|
||||
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebGetStatus(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -2108,18 +2315,18 @@ func (s *httpdServer) handleWebUpdateFolderPost(w http.ResponseWriter, r *http.R
|
|||
|
||||
updatedFolder = getFolderFromTemplate(updatedFolder, updatedFolder.Name)
|
||||
|
||||
err = dataprovider.UpdateFolder(&updatedFolder, folder.Users, claims.Username, ipAddr)
|
||||
err = dataprovider.UpdateFolder(&updatedFolder, folder.Users, folder.Groups, claims.Username, ipAddr)
|
||||
if err != nil {
|
||||
s.renderFolderPage(w, r, folder, folderPageModeUpdate, err.Error())
|
||||
s.renderFolderPage(w, r, updatedFolder, folderPageModeUpdate, err.Error())
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, webFoldersPath, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *httpdServer) getWebVirtualFolders(w http.ResponseWriter, r *http.Request, limit int) ([]vfs.BaseVirtualFolder, error) {
|
||||
func (s *httpdServer) getWebVirtualFolders(w http.ResponseWriter, r *http.Request, limit int, minimal bool) ([]vfs.BaseVirtualFolder, error) {
|
||||
folders := make([]vfs.BaseVirtualFolder, 0, limit)
|
||||
for {
|
||||
f, err := dataprovider.GetFolders(limit, len(folders), dataprovider.OrderASC)
|
||||
f, err := dataprovider.GetFolders(limit, len(folders), dataprovider.OrderASC, minimal)
|
||||
if err != nil {
|
||||
s.renderInternalServerErrorPage(w, r, err)
|
||||
return folders, err
|
||||
|
@ -2142,7 +2349,7 @@ func (s *httpdServer) handleWebGetFolders(w http.ResponseWriter, r *http.Request
|
|||
limit = defaultQueryLimit
|
||||
}
|
||||
}
|
||||
folders, err := s.getWebVirtualFolders(w, r, limit)
|
||||
folders, err := s.getWebVirtualFolders(w, r, limit, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -2153,3 +2360,127 @@ func (s *httpdServer) handleWebGetFolders(w http.ResponseWriter, r *http.Request
|
|||
}
|
||||
renderAdminTemplate(w, templateFolders, data)
|
||||
}
|
||||
|
||||
func (s *httpdServer) getWebGroups(w http.ResponseWriter, r *http.Request, limit int, minimal bool) ([]dataprovider.Group, error) {
|
||||
groups := make([]dataprovider.Group, 0, limit)
|
||||
for {
|
||||
f, err := dataprovider.GetGroups(limit, len(groups), dataprovider.OrderASC, minimal)
|
||||
if err != nil {
|
||||
s.renderInternalServerErrorPage(w, r, err)
|
||||
return groups, err
|
||||
}
|
||||
groups = append(groups, f...)
|
||||
if len(f) < limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebGetGroups(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
limit := defaultQueryLimit
|
||||
if _, ok := r.URL.Query()["qlimit"]; ok {
|
||||
var err error
|
||||
limit, err = strconv.Atoi(r.URL.Query().Get("qlimit"))
|
||||
if err != nil {
|
||||
limit = defaultQueryLimit
|
||||
}
|
||||
}
|
||||
groups, err := s.getWebGroups(w, r, limit, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data := groupsPage{
|
||||
basePage: s.getBasePageData(pageGroupsTitle, webGroupsPath, r),
|
||||
Groups: groups,
|
||||
}
|
||||
renderAdminTemplate(w, templateGroups, data)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebAddGroupGet(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
s.renderGroupPage(w, r, dataprovider.Group{}, groupPageModeAdd, "")
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebAddGroupPost(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
|
||||
return
|
||||
}
|
||||
group, err := getGroupFromPostFields(r)
|
||||
if err != nil {
|
||||
s.renderGroupPage(w, r, group, groupPageModeAdd, err.Error())
|
||||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
s.renderForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
err = dataprovider.AddGroup(&group, claims.Username, ipAddr)
|
||||
if err != nil {
|
||||
s.renderGroupPage(w, r, group, groupPageModeAdd, err.Error())
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, webGroupsPath, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebUpdateGroupGet(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
name := getURLParam(r, "name")
|
||||
group, err := dataprovider.GroupExists(name)
|
||||
if err == nil {
|
||||
s.renderGroupPage(w, r, group, groupPageModeUpdate, "")
|
||||
} else if _, ok := err.(*util.RecordNotFoundError); ok {
|
||||
s.renderNotFoundPage(w, r, err)
|
||||
} else {
|
||||
s.renderInternalServerErrorPage(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebUpdateGroupPost(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
|
||||
return
|
||||
}
|
||||
name := getURLParam(r, "name")
|
||||
group, err := dataprovider.GroupExists(name)
|
||||
if _, ok := err.(*util.RecordNotFoundError); ok {
|
||||
s.renderNotFoundPage(w, r, err)
|
||||
return
|
||||
} else if err != nil {
|
||||
s.renderInternalServerErrorPage(w, r, err)
|
||||
return
|
||||
}
|
||||
updatedGroup, err := getGroupFromPostFields(r)
|
||||
if err != nil {
|
||||
s.renderGroupPage(w, r, group, groupPageModeUpdate, err.Error())
|
||||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||
s.renderForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
updatedGroup.ID = group.ID
|
||||
updatedGroup.Name = group.Name
|
||||
updatedGroup.SetEmptySecretsIfNil()
|
||||
|
||||
updateEncryptedSecrets(&updatedGroup.UserSettings.FsConfig, group.UserSettings.FsConfig.S3Config.AccessSecret,
|
||||
group.UserSettings.FsConfig.AzBlobConfig.AccountKey, group.UserSettings.FsConfig.AzBlobConfig.SASURL,
|
||||
group.UserSettings.FsConfig.GCSConfig.Credentials, group.UserSettings.FsConfig.CryptConfig.Passphrase,
|
||||
group.UserSettings.FsConfig.SFTPConfig.Password, group.UserSettings.FsConfig.SFTPConfig.PrivateKey)
|
||||
|
||||
err = dataprovider.UpdateGroup(&updatedGroup, group.Users, claims.Username, ipAddr)
|
||||
if err != nil {
|
||||
s.renderGroupPage(w, r, updatedGroup, groupPageModeUpdate, err.Error())
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, webGroupsPath, http.StatusSeeOther)
|
||||
}
|
||||
|
|
|
@ -556,7 +556,7 @@ func (s *httpdServer) renderClientProfilePage(w http.ResponseWriter, r *http.Req
|
|||
baseClientPage: s.getBaseClientPageData(pageClientProfileTitle, webClientProfilePath, r),
|
||||
Error: error,
|
||||
}
|
||||
user, err := dataprovider.UserExists(data.LoggedUser.Username)
|
||||
user, userMerged, err := dataprovider.GetUserVariants(data.LoggedUser.Username)
|
||||
if err != nil {
|
||||
s.renderClientInternalServerErrorPage(w, r, err)
|
||||
return
|
||||
|
@ -565,7 +565,7 @@ func (s *httpdServer) renderClientProfilePage(w http.ResponseWriter, r *http.Req
|
|||
data.AllowAPIKeyAuth = user.Filters.AllowAPIKeyAuth
|
||||
data.Email = user.Email
|
||||
data.Description = user.Description
|
||||
data.CanSubmit = user.CanChangeAPIKeyAuth() || user.CanManagePublicKeys() || user.CanChangeInfo()
|
||||
data.CanSubmit = userMerged.CanChangeAPIKeyAuth() || userMerged.CanManagePublicKeys() || userMerged.CanChangeInfo()
|
||||
renderClientTemplate(w, templateClientProfile, data)
|
||||
}
|
||||
|
||||
|
@ -586,7 +586,7 @@ func (s *httpdServer) handleWebClientDownloadZip(w http.ResponseWriter, r *http.
|
|||
return
|
||||
}
|
||||
|
||||
user, err := dataprovider.UserExists(claims.Username)
|
||||
user, err := dataprovider.GetUserWithGroupSettings(claims.Username)
|
||||
if err != nil {
|
||||
s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
|
||||
return
|
||||
|
@ -735,7 +735,7 @@ func (s *httpdServer) handleClientGetDirContents(w http.ResponseWriter, r *http.
|
|||
return
|
||||
}
|
||||
|
||||
user, err := dataprovider.UserExists(claims.Username)
|
||||
user, err := dataprovider.GetUserWithGroupSettings(claims.Username)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, nil, "Unable to retrieve your user", getRespStatus(err))
|
||||
return
|
||||
|
@ -812,7 +812,7 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques
|
|||
return
|
||||
}
|
||||
|
||||
user, err := dataprovider.UserExists(claims.Username)
|
||||
user, err := dataprovider.GetUserWithGroupSettings(claims.Username)
|
||||
if err != nil {
|
||||
s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
|
||||
return
|
||||
|
@ -872,7 +872,7 @@ func (s *httpdServer) handleClientEditFile(w http.ResponseWriter, r *http.Reques
|
|||
return
|
||||
}
|
||||
|
||||
user, err := dataprovider.UserExists(claims.Username)
|
||||
user, err := dataprovider.GetUserWithGroupSettings(claims.Username)
|
||||
if err != nil {
|
||||
s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
|
||||
return
|
||||
|
@ -1120,22 +1120,22 @@ func (s *httpdServer) handleWebClientProfilePost(w http.ResponseWriter, r *http.
|
|||
s.renderClientForbiddenPage(w, r, "Invalid token claims")
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.UserExists(claims.Username)
|
||||
user, userMerged, err := dataprovider.GetUserVariants(claims.Username)
|
||||
if err != nil {
|
||||
s.renderClientProfilePage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
if !user.CanManagePublicKeys() && !user.CanChangeAPIKeyAuth() && !user.CanChangeInfo() {
|
||||
if !userMerged.CanManagePublicKeys() && !userMerged.CanChangeAPIKeyAuth() && !userMerged.CanChangeInfo() {
|
||||
s.renderClientForbiddenPage(w, r, "You are not allowed to change anything")
|
||||
return
|
||||
}
|
||||
if user.CanManagePublicKeys() {
|
||||
if userMerged.CanManagePublicKeys() {
|
||||
user.PublicKeys = r.Form["public_keys"]
|
||||
}
|
||||
if user.CanChangeAPIKeyAuth() {
|
||||
if userMerged.CanChangeAPIKeyAuth() {
|
||||
user.Filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
|
||||
}
|
||||
if user.CanChangeInfo() {
|
||||
if userMerged.CanChangeInfo() {
|
||||
user.Email = r.Form.Get("email")
|
||||
user.Description = r.Form.Get("description")
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
"github.com/sftpgo/sdk"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/common"
|
||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||
|
@ -33,6 +34,7 @@ const (
|
|||
quotaScanPath = "/api/v2/quotas/users/scans"
|
||||
quotaScanVFolderPath = "/api/v2/quotas/folders/scans"
|
||||
userPath = "/api/v2/users"
|
||||
groupPath = "/api/v2/groups"
|
||||
versionPath = "/api/v2/version"
|
||||
folderPath = "/api/v2/folders"
|
||||
serverStatusPath = "/api/v2/status"
|
||||
|
@ -244,6 +246,115 @@ func GetUsers(limit, offset int64, expectedStatusCode int) ([]dataprovider.User,
|
|||
return users, body, err
|
||||
}
|
||||
|
||||
// AddGroup adds a new group and checks the received HTTP Status code against expectedStatusCode.
|
||||
func AddGroup(group dataprovider.Group, expectedStatusCode int) (dataprovider.Group, []byte, error) {
|
||||
var newGroup dataprovider.Group
|
||||
var body []byte
|
||||
asJSON, _ := json.Marshal(group)
|
||||
resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(groupPath), bytes.NewBuffer(asJSON),
|
||||
"application/json", getDefaultToken())
|
||||
if err != nil {
|
||||
return newGroup, body, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||
if expectedStatusCode != http.StatusCreated {
|
||||
body, _ = getResponseBody(resp)
|
||||
return newGroup, body, err
|
||||
}
|
||||
if err == nil {
|
||||
err = render.DecodeJSON(resp.Body, &newGroup)
|
||||
} else {
|
||||
body, _ = getResponseBody(resp)
|
||||
}
|
||||
if err == nil {
|
||||
err = checkGroup(group, newGroup)
|
||||
}
|
||||
return newGroup, body, err
|
||||
}
|
||||
|
||||
// UpdateGroup updates an existing group and checks the received HTTP Status code against expectedStatusCode
|
||||
func UpdateGroup(group dataprovider.Group, expectedStatusCode int) (dataprovider.Group, []byte, error) {
|
||||
var newGroup dataprovider.Group
|
||||
var body []byte
|
||||
|
||||
asJSON, _ := json.Marshal(group)
|
||||
resp, err := sendHTTPRequest(http.MethodPut, buildURLRelativeToBase(groupPath, url.PathEscape(group.Name)),
|
||||
bytes.NewBuffer(asJSON), "application/json", getDefaultToken())
|
||||
if err != nil {
|
||||
return newGroup, body, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ = getResponseBody(resp)
|
||||
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||
if expectedStatusCode != http.StatusOK {
|
||||
return newGroup, body, err
|
||||
}
|
||||
if err == nil {
|
||||
newGroup, body, err = GetGroupByName(group.Name, expectedStatusCode)
|
||||
}
|
||||
if err == nil {
|
||||
err = checkGroup(group, newGroup)
|
||||
}
|
||||
return newGroup, body, err
|
||||
}
|
||||
|
||||
// RemoveGroup removes an existing group and checks the received HTTP Status code against expectedStatusCode.
|
||||
func RemoveGroup(group dataprovider.Group, expectedStatusCode int) ([]byte, error) {
|
||||
var body []byte
|
||||
resp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(groupPath, url.PathEscape(group.Name)),
|
||||
nil, "", getDefaultToken())
|
||||
if err != nil {
|
||||
return body, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ = getResponseBody(resp)
|
||||
return body, checkResponse(resp.StatusCode, expectedStatusCode)
|
||||
}
|
||||
|
||||
// GetGroupByName gets a group by name and checks the received HTTP Status code against expectedStatusCode.
|
||||
func GetGroupByName(name string, expectedStatusCode int) (dataprovider.Group, []byte, error) {
|
||||
var group dataprovider.Group
|
||||
var body []byte
|
||||
resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(groupPath, url.PathEscape(name)),
|
||||
nil, "", getDefaultToken())
|
||||
if err != nil {
|
||||
return group, body, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||
if err == nil && expectedStatusCode == http.StatusOK {
|
||||
err = render.DecodeJSON(resp.Body, &group)
|
||||
} else {
|
||||
body, _ = getResponseBody(resp)
|
||||
}
|
||||
return group, body, err
|
||||
}
|
||||
|
||||
// GetGroups returns a list of groups and checks the received HTTP Status code against expectedStatusCode.
|
||||
// The number of results can be limited specifying a limit.
|
||||
// Some results can be skipped specifying an offset.
|
||||
func GetGroups(limit, offset int64, expectedStatusCode int) ([]dataprovider.Group, []byte, error) {
|
||||
var groups []dataprovider.Group
|
||||
var body []byte
|
||||
url, err := addLimitAndOffsetQueryParams(buildURLRelativeToBase(groupPath), limit, offset)
|
||||
if err != nil {
|
||||
return groups, body, err
|
||||
}
|
||||
resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "", getDefaultToken())
|
||||
if err != nil {
|
||||
return groups, body, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||
if err == nil && expectedStatusCode == http.StatusOK {
|
||||
err = render.DecodeJSON(resp.Body, &groups)
|
||||
} else {
|
||||
body, _ = getResponseBody(resp)
|
||||
}
|
||||
return groups, body, err
|
||||
}
|
||||
|
||||
// AddAdmin adds a new admin and checks the received HTTP Status code against expectedStatusCode.
|
||||
func AddAdmin(admin dataprovider.Admin, expectedStatusCode int) (dataprovider.Admin, []byte, error) {
|
||||
var newAdmin dataprovider.Admin
|
||||
|
@ -1043,6 +1154,35 @@ func getResponseBody(resp *http.Response) ([]byte, error) {
|
|||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func checkGroup(expected dataprovider.Group, actual dataprovider.Group) error {
|
||||
if expected.ID <= 0 {
|
||||
if actual.ID <= 0 {
|
||||
return errors.New("actual group ID must be > 0")
|
||||
}
|
||||
} else {
|
||||
if actual.ID != expected.ID {
|
||||
return errors.New("group ID mismatch")
|
||||
}
|
||||
}
|
||||
if dataprovider.ConvertName(expected.Name) != actual.Name {
|
||||
return errors.New("name mismatch")
|
||||
}
|
||||
if expected.Description != actual.Description {
|
||||
return errors.New("description mismatch")
|
||||
}
|
||||
if err := compareEqualGroupSettingsFields(expected.UserSettings.BaseGroupUserSettings,
|
||||
actual.UserSettings.BaseGroupUserSettings); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := compareVirtualFolders(expected.VirtualFolders, actual.VirtualFolders); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := compareUserFilters(expected.UserSettings.Filters, actual.UserSettings.Filters); err != nil {
|
||||
return err
|
||||
}
|
||||
return compareFsConfig(&expected.UserSettings.FsConfig, &actual.UserSettings.FsConfig)
|
||||
}
|
||||
|
||||
func checkFolder(expected *vfs.BaseVirtualFolder, actual *vfs.BaseVirtualFolder) error {
|
||||
if expected.ID <= 0 {
|
||||
if actual.ID <= 0 {
|
||||
|
@ -1185,27 +1325,30 @@ func checkUser(expected *dataprovider.User, actual *dataprovider.User) error {
|
|||
if expected.Email != actual.Email {
|
||||
return errors.New("email mismatch")
|
||||
}
|
||||
if err := compareUserPermissions(expected, actual); err != nil {
|
||||
if err := compareUserPermissions(expected.Permissions, actual.Permissions); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := compareUserFilters(expected, actual); err != nil {
|
||||
if err := compareUserFilters(expected.Filters.BaseUserFilters, actual.Filters.BaseUserFilters); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := compareFsConfig(&expected.FsConfig, &actual.FsConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := compareUserVirtualFolders(expected, actual); err != nil {
|
||||
if err := compareUserGroups(expected, actual); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := compareVirtualFolders(expected.VirtualFolders, actual.VirtualFolders); err != nil {
|
||||
return err
|
||||
}
|
||||
return compareEqualsUserFields(expected, actual)
|
||||
}
|
||||
|
||||
func compareUserPermissions(expected *dataprovider.User, actual *dataprovider.User) error {
|
||||
if len(expected.Permissions) != len(actual.Permissions) {
|
||||
func compareUserPermissions(expected map[string][]string, actual map[string][]string) error {
|
||||
if len(expected) != len(actual) {
|
||||
return errors.New("permissions mismatch")
|
||||
}
|
||||
for dir, perms := range expected.Permissions {
|
||||
if actualPerms, ok := actual.Permissions[dir]; ok {
|
||||
for dir, perms := range expected {
|
||||
if actualPerms, ok := actual[dir]; ok {
|
||||
for _, v := range actualPerms {
|
||||
if !util.IsStringInSlice(v, perms) {
|
||||
return errors.New("permissions contents mismatch")
|
||||
|
@ -1218,13 +1361,34 @@ func compareUserPermissions(expected *dataprovider.User, actual *dataprovider.Us
|
|||
return nil
|
||||
}
|
||||
|
||||
func compareUserVirtualFolders(expected *dataprovider.User, actual *dataprovider.User) error {
|
||||
if len(actual.VirtualFolders) != len(expected.VirtualFolders) {
|
||||
func compareUserGroups(expected *dataprovider.User, actual *dataprovider.User) error {
|
||||
if len(actual.Groups) != len(expected.Groups) {
|
||||
return errors.New("groups len mismatch")
|
||||
}
|
||||
for _, g := range actual.Groups {
|
||||
found := false
|
||||
for _, g1 := range expected.Groups {
|
||||
if g1.Name == g.Name {
|
||||
found = true
|
||||
if g1.Type != g.Type {
|
||||
return fmt.Errorf("type mismatch for group %s", g.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return errors.New("groups mismatch")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func compareVirtualFolders(expected []vfs.VirtualFolder, actual []vfs.VirtualFolder) error {
|
||||
if len(actual) != len(expected) {
|
||||
return errors.New("virtual folders len mismatch")
|
||||
}
|
||||
for _, v := range actual.VirtualFolders {
|
||||
for _, v := range actual {
|
||||
found := false
|
||||
for _, v1 := range expected.VirtualFolders {
|
||||
for _, v1 := range expected {
|
||||
if path.Clean(v.VirtualPath) == path.Clean(v1.VirtualPath) {
|
||||
if err := checkFolder(&v1.BaseVirtualFolder, &v.BaseVirtualFolder); err != nil {
|
||||
return err
|
||||
|
@ -1455,80 +1619,80 @@ func checkEncryptedSecret(expected, actual *kms.Secret) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func compareUserFilterSubStructs(expected *dataprovider.User, actual *dataprovider.User) error {
|
||||
for _, IPMask := range expected.Filters.AllowedIP {
|
||||
if !util.IsStringInSlice(IPMask, actual.Filters.AllowedIP) {
|
||||
func compareUserFilterSubStructs(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
|
||||
for _, IPMask := range expected.AllowedIP {
|
||||
if !util.IsStringInSlice(IPMask, actual.AllowedIP) {
|
||||
return errors.New("allowed IP contents mismatch")
|
||||
}
|
||||
}
|
||||
for _, IPMask := range expected.Filters.DeniedIP {
|
||||
if !util.IsStringInSlice(IPMask, actual.Filters.DeniedIP) {
|
||||
for _, IPMask := range expected.DeniedIP {
|
||||
if !util.IsStringInSlice(IPMask, actual.DeniedIP) {
|
||||
return errors.New("denied IP contents mismatch")
|
||||
}
|
||||
}
|
||||
for _, method := range expected.Filters.DeniedLoginMethods {
|
||||
if !util.IsStringInSlice(method, actual.Filters.DeniedLoginMethods) {
|
||||
for _, method := range expected.DeniedLoginMethods {
|
||||
if !util.IsStringInSlice(method, actual.DeniedLoginMethods) {
|
||||
return errors.New("denied login methods contents mismatch")
|
||||
}
|
||||
}
|
||||
for _, protocol := range expected.Filters.DeniedProtocols {
|
||||
if !util.IsStringInSlice(protocol, actual.Filters.DeniedProtocols) {
|
||||
for _, protocol := range expected.DeniedProtocols {
|
||||
if !util.IsStringInSlice(protocol, actual.DeniedProtocols) {
|
||||
return errors.New("denied protocols contents mismatch")
|
||||
}
|
||||
}
|
||||
for _, options := range expected.Filters.WebClient {
|
||||
if !util.IsStringInSlice(options, actual.Filters.WebClient) {
|
||||
for _, options := range expected.WebClient {
|
||||
if !util.IsStringInSlice(options, actual.WebClient) {
|
||||
return errors.New("web client options contents mismatch")
|
||||
}
|
||||
}
|
||||
return compareUserFiltersEqualFields(expected, actual)
|
||||
}
|
||||
|
||||
func compareUserFiltersEqualFields(expected *dataprovider.User, actual *dataprovider.User) error {
|
||||
if expected.Filters.Hooks.ExternalAuthDisabled != actual.Filters.Hooks.ExternalAuthDisabled {
|
||||
func compareUserFiltersEqualFields(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
|
||||
if expected.Hooks.ExternalAuthDisabled != actual.Hooks.ExternalAuthDisabled {
|
||||
return errors.New("external_auth_disabled hook mismatch")
|
||||
}
|
||||
if expected.Filters.Hooks.PreLoginDisabled != actual.Filters.Hooks.PreLoginDisabled {
|
||||
if expected.Hooks.PreLoginDisabled != actual.Hooks.PreLoginDisabled {
|
||||
return errors.New("pre_login_disabled hook mismatch")
|
||||
}
|
||||
if expected.Filters.Hooks.CheckPasswordDisabled != actual.Filters.Hooks.CheckPasswordDisabled {
|
||||
if expected.Hooks.CheckPasswordDisabled != actual.Hooks.CheckPasswordDisabled {
|
||||
return errors.New("check_password_disabled hook mismatch")
|
||||
}
|
||||
if expected.Filters.DisableFsChecks != actual.Filters.DisableFsChecks {
|
||||
if expected.DisableFsChecks != actual.DisableFsChecks {
|
||||
return errors.New("disable_fs_checks mismatch")
|
||||
}
|
||||
if expected.Filters.StartDirectory != actual.Filters.StartDirectory {
|
||||
if expected.StartDirectory != actual.StartDirectory {
|
||||
return errors.New("start_directory mismatch")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func compareUserFilters(expected *dataprovider.User, actual *dataprovider.User) error {
|
||||
if len(expected.Filters.AllowedIP) != len(actual.Filters.AllowedIP) {
|
||||
func compareUserFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
|
||||
if len(expected.AllowedIP) != len(actual.AllowedIP) {
|
||||
return errors.New("allowed IP mismatch")
|
||||
}
|
||||
if len(expected.Filters.DeniedIP) != len(actual.Filters.DeniedIP) {
|
||||
if len(expected.DeniedIP) != len(actual.DeniedIP) {
|
||||
return errors.New("denied IP mismatch")
|
||||
}
|
||||
if len(expected.Filters.DeniedLoginMethods) != len(actual.Filters.DeniedLoginMethods) {
|
||||
if len(expected.DeniedLoginMethods) != len(actual.DeniedLoginMethods) {
|
||||
return errors.New("denied login methods mismatch")
|
||||
}
|
||||
if len(expected.Filters.DeniedProtocols) != len(actual.Filters.DeniedProtocols) {
|
||||
if len(expected.DeniedProtocols) != len(actual.DeniedProtocols) {
|
||||
return errors.New("denied protocols mismatch")
|
||||
}
|
||||
if expected.Filters.MaxUploadFileSize != actual.Filters.MaxUploadFileSize {
|
||||
if expected.MaxUploadFileSize != actual.MaxUploadFileSize {
|
||||
return errors.New("max upload file size mismatch")
|
||||
}
|
||||
if expected.Filters.TLSUsername != actual.Filters.TLSUsername {
|
||||
if expected.TLSUsername != actual.TLSUsername {
|
||||
return errors.New("TLSUsername mismatch")
|
||||
}
|
||||
if len(expected.Filters.WebClient) != len(actual.Filters.WebClient) {
|
||||
if len(expected.WebClient) != len(actual.WebClient) {
|
||||
return errors.New("WebClient filter mismatch")
|
||||
}
|
||||
if expected.Filters.AllowAPIKeyAuth != actual.Filters.AllowAPIKeyAuth {
|
||||
if expected.AllowAPIKeyAuth != actual.AllowAPIKeyAuth {
|
||||
return errors.New("allow_api_key_auth mismatch")
|
||||
}
|
||||
if expected.Filters.ExternalAuthCacheTime != actual.Filters.ExternalAuthCacheTime {
|
||||
if expected.ExternalAuthCacheTime != actual.ExternalAuthCacheTime {
|
||||
return errors.New("external_auth_cache_time mismatch")
|
||||
}
|
||||
if err := compareUserFilterSubStructs(expected, actual); err != nil {
|
||||
|
@ -1555,21 +1719,21 @@ func checkFilterMatch(expected []string, actual []string) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func compareUserDataTransferLimitFilters(expected *dataprovider.User, actual *dataprovider.User) error {
|
||||
if len(expected.Filters.DataTransferLimits) != len(actual.Filters.DataTransferLimits) {
|
||||
func compareUserDataTransferLimitFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
|
||||
if len(expected.DataTransferLimits) != len(actual.DataTransferLimits) {
|
||||
return errors.New("data transfer limits filters mismatch")
|
||||
}
|
||||
for idx, l := range expected.Filters.DataTransferLimits {
|
||||
if actual.Filters.DataTransferLimits[idx].UploadDataTransfer != l.UploadDataTransfer {
|
||||
for idx, l := range expected.DataTransferLimits {
|
||||
if actual.DataTransferLimits[idx].UploadDataTransfer != l.UploadDataTransfer {
|
||||
return errors.New("data transfer limit upload_data_transfer mismatch")
|
||||
}
|
||||
if actual.Filters.DataTransferLimits[idx].DownloadDataTransfer != l.DownloadDataTransfer {
|
||||
if actual.DataTransferLimits[idx].DownloadDataTransfer != l.DownloadDataTransfer {
|
||||
return errors.New("data transfer limit download_data_transfer mismatch")
|
||||
}
|
||||
if actual.Filters.DataTransferLimits[idx].TotalDataTransfer != l.TotalDataTransfer {
|
||||
if actual.DataTransferLimits[idx].TotalDataTransfer != l.TotalDataTransfer {
|
||||
return errors.New("data transfer limit total_data_transfer mismatch")
|
||||
}
|
||||
for _, source := range actual.Filters.DataTransferLimits[idx].Sources {
|
||||
for _, source := range actual.DataTransferLimits[idx].Sources {
|
||||
if !util.IsStringInSlice(source, l.Sources) {
|
||||
return errors.New("data transfer limit source mismatch")
|
||||
}
|
||||
|
@ -1579,22 +1743,22 @@ func compareUserDataTransferLimitFilters(expected *dataprovider.User, actual *da
|
|||
return nil
|
||||
}
|
||||
|
||||
func compareUserBandwidthLimitFilters(expected *dataprovider.User, actual *dataprovider.User) error {
|
||||
if len(expected.Filters.BandwidthLimits) != len(actual.Filters.BandwidthLimits) {
|
||||
func compareUserBandwidthLimitFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
|
||||
if len(expected.BandwidthLimits) != len(actual.BandwidthLimits) {
|
||||
return errors.New("bandwidth limits filters mismatch")
|
||||
}
|
||||
|
||||
for idx, l := range expected.Filters.BandwidthLimits {
|
||||
if actual.Filters.BandwidthLimits[idx].UploadBandwidth != l.UploadBandwidth {
|
||||
for idx, l := range expected.BandwidthLimits {
|
||||
if actual.BandwidthLimits[idx].UploadBandwidth != l.UploadBandwidth {
|
||||
return errors.New("bandwidth filters upload_bandwidth mismatch")
|
||||
}
|
||||
if actual.Filters.BandwidthLimits[idx].DownloadBandwidth != l.DownloadBandwidth {
|
||||
if actual.BandwidthLimits[idx].DownloadBandwidth != l.DownloadBandwidth {
|
||||
return errors.New("bandwidth filters download_bandwidth mismatch")
|
||||
}
|
||||
if len(actual.Filters.BandwidthLimits[idx].Sources) != len(l.Sources) {
|
||||
if len(actual.BandwidthLimits[idx].Sources) != len(l.Sources) {
|
||||
return errors.New("bandwidth filters sources mismatch")
|
||||
}
|
||||
for _, source := range actual.Filters.BandwidthLimits[idx].Sources {
|
||||
for _, source := range actual.BandwidthLimits[idx].Sources {
|
||||
if !util.IsStringInSlice(source, l.Sources) {
|
||||
return errors.New("bandwidth filters source mismatch")
|
||||
}
|
||||
|
@ -1604,13 +1768,13 @@ func compareUserBandwidthLimitFilters(expected *dataprovider.User, actual *datap
|
|||
return nil
|
||||
}
|
||||
|
||||
func compareUserFilePatternsFilters(expected *dataprovider.User, actual *dataprovider.User) error {
|
||||
if len(expected.Filters.FilePatterns) != len(actual.Filters.FilePatterns) {
|
||||
func compareUserFilePatternsFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
|
||||
if len(expected.FilePatterns) != len(actual.FilePatterns) {
|
||||
return errors.New("file patterns mismatch")
|
||||
}
|
||||
for _, f := range expected.Filters.FilePatterns {
|
||||
for _, f := range expected.FilePatterns {
|
||||
found := false
|
||||
for _, f1 := range actual.Filters.FilePatterns {
|
||||
for _, f1 := range actual.FilePatterns {
|
||||
if path.Clean(f.Path) == path.Clean(f1.Path) && f.DenyPolicy == f1.DenyPolicy {
|
||||
if !checkFilterMatch(f.AllowedPatterns, f1.AllowedPatterns) ||
|
||||
!checkFilterMatch(f.DeniedPatterns, f1.DeniedPatterns) {
|
||||
|
@ -1626,6 +1790,37 @@ func compareUserFilePatternsFilters(expected *dataprovider.User, actual *datapro
|
|||
return nil
|
||||
}
|
||||
|
||||
func compareEqualGroupSettingsFields(expected sdk.BaseGroupUserSettings, actual sdk.BaseGroupUserSettings) error {
|
||||
if expected.HomeDir != actual.HomeDir {
|
||||
return errors.New("home dir mismatch")
|
||||
}
|
||||
if expected.MaxSessions != actual.MaxSessions {
|
||||
return errors.New("MaxSessions mismatch")
|
||||
}
|
||||
if expected.QuotaSize != actual.QuotaSize {
|
||||
return errors.New("QuotaSize mismatch")
|
||||
}
|
||||
if expected.QuotaFiles != actual.QuotaFiles {
|
||||
return errors.New("QuotaFiles mismatch")
|
||||
}
|
||||
if expected.UploadBandwidth != actual.UploadBandwidth {
|
||||
return errors.New("UploadBandwidth mismatch")
|
||||
}
|
||||
if expected.DownloadBandwidth != actual.DownloadBandwidth {
|
||||
return errors.New("DownloadBandwidth mismatch")
|
||||
}
|
||||
if expected.UploadDataTransfer != actual.UploadDataTransfer {
|
||||
return errors.New("upload_data_transfer mismatch")
|
||||
}
|
||||
if expected.DownloadDataTransfer != actual.DownloadDataTransfer {
|
||||
return errors.New("download_data_transfer mismatch")
|
||||
}
|
||||
if expected.TotalDataTransfer != actual.TotalDataTransfer {
|
||||
return errors.New("total_data_transfer mismatch")
|
||||
}
|
||||
return compareUserPermissions(expected.Permissions, actual.Permissions)
|
||||
}
|
||||
|
||||
func compareEqualsUserFields(expected *dataprovider.User, actual *dataprovider.User) error {
|
||||
if dataprovider.ConvertName(expected.Username) != actual.Username {
|
||||
return errors.New("username mismatch")
|
||||
|
|
|
@ -9,6 +9,7 @@ tags:
|
|||
- name: defender
|
||||
- name: quota
|
||||
- name: folders
|
||||
- name: groups
|
||||
- name: users
|
||||
- name: data retention
|
||||
- name: events
|
||||
|
@ -22,6 +23,7 @@ info:
|
|||
Several storage backends are supported and they are configurable per-user, so you can serve a local directory for a user and an S3 bucket (or part of it) for another one.
|
||||
SFTPGo also supports virtual folders, a virtual folder can use any of the supported storage backends. So you can have, for example, an S3 user that exposes a Google Cloud Storage bucket (or part of it) on a specified path and an encrypted local filesystem on another one.
|
||||
Virtual folders can be private or shared among multiple users, for shared virtual folders you can define different quota limits for each user.
|
||||
SFTPGo supports groups to simplify the administration of multiple accounts by letting you assign settings once to a group, instead of multiple times to each individual user.
|
||||
The SFTPGo WebClient allows end users to change their credentials, browse and manage their files in the browser and setup two-factor authentication which works with Authy, Google Authenticator and other compatible apps.
|
||||
From the WebClient each authorized user can also create HTTP/S links to externally share files and folders securely, by setting limits to the number of downloads/uploads, protecting the share with a password, limiting access by source IP address, setting an automatic expiration date.
|
||||
version: 2.2.2-dev
|
||||
|
@ -1696,7 +1698,7 @@ paths:
|
|||
- in: query
|
||||
name: order
|
||||
required: false
|
||||
description: Ordering folders by path. Default ASC
|
||||
description: Ordering folders by name. Default ASC
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
|
@ -1844,6 +1846,181 @@ paths:
|
|||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/groups:
|
||||
get:
|
||||
tags:
|
||||
- groups
|
||||
summary: Get groups
|
||||
description: Returns an array with one or more groups
|
||||
operationId: get_groups
|
||||
parameters:
|
||||
- in: query
|
||||
name: offset
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
default: 0
|
||||
required: false
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 500
|
||||
default: 100
|
||||
required: false
|
||||
description: 'The maximum number of items to return. Max value is 500, default is 100'
|
||||
- in: query
|
||||
name: order
|
||||
required: false
|
||||
description: Ordering groups by name. Default ASC
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- ASC
|
||||
- DESC
|
||||
example: ASC
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Group'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
post:
|
||||
tags:
|
||||
- groups
|
||||
summary: Add group
|
||||
operationId: add_group
|
||||
description: Adds a new group
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Group'
|
||||
responses:
|
||||
'201':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Group'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
'/groups/{name}':
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
description: group name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
get:
|
||||
tags:
|
||||
- groups
|
||||
summary: Find groups by name
|
||||
description: Returns the group with the given name if it exists.
|
||||
operationId: get_group_by_name
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Group'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
put:
|
||||
tags:
|
||||
- groups
|
||||
summary: Update group
|
||||
description: Updates an existing group
|
||||
operationId: update_group
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Group'
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
message: User updated
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
delete:
|
||||
tags:
|
||||
- groups
|
||||
summary: Delete
|
||||
description: Deletes an existing group
|
||||
operationId: delete_group
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
message: User deleted
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/events/fs:
|
||||
get:
|
||||
tags:
|
||||
|
@ -4606,7 +4783,7 @@ components:
|
|||
total_data_transfer:
|
||||
type: integer
|
||||
description: 'Maximum total data transfer as MB. 0 means unlimited. You can set a total data transfer instead of the individual values for uploads and downloads'
|
||||
UserFilters:
|
||||
BaseUserFilters:
|
||||
type: object
|
||||
properties:
|
||||
allowed_ip:
|
||||
|
@ -4665,12 +4842,6 @@ components:
|
|||
description: 'API key authentication allows to impersonate this user with an API key'
|
||||
user_type:
|
||||
$ref: '#/components/schemas/UserType'
|
||||
totp_config:
|
||||
$ref: '#/components/schemas/UserTOTPConfig'
|
||||
recovery_codes:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RecoveryCode'
|
||||
bandwidth_limits:
|
||||
type: array
|
||||
items:
|
||||
|
@ -4691,6 +4862,17 @@ components:
|
|||
$ref: '#/components/schemas/MFAProtocols'
|
||||
description: 'Defines protocols that require two factor authentication'
|
||||
description: Additional user options
|
||||
UserFilters:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/BaseUserFilters'
|
||||
- type: object
|
||||
properties:
|
||||
totp_config:
|
||||
$ref: '#/components/schemas/UserTOTPConfig'
|
||||
recovery_codes:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RecoveryCode'
|
||||
Secret:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -4984,7 +5166,7 @@ components:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/VirtualFolder'
|
||||
description: mapping between virtual SFTPGo paths and filesystem paths outside the user home directory. Supported for local filesystem only. If one or more of the specified folders are not inside the dataprovider they will be automatically created. You have to create the folder on the filesystem yourself
|
||||
description: mapping between virtual SFTPGo paths and virtual folders. If one or more of the specified folders are not inside the dataprovider they will be automatically created. You have to create the folder on the filesystem yourself
|
||||
uid:
|
||||
type: integer
|
||||
format: int32
|
||||
|
@ -5070,6 +5252,10 @@ components:
|
|||
additional_info:
|
||||
type: string
|
||||
description: Free form text field for external systems
|
||||
groups:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/GroupMapping'
|
||||
oidc_custom_fields:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
|
@ -5652,6 +5838,95 @@ components:
|
|||
example:
|
||||
- 192.0.2.0/24
|
||||
- '2001:db8::/32'
|
||||
GroupUserSettings:
|
||||
type: object
|
||||
properties:
|
||||
home_dir:
|
||||
type: string
|
||||
max_sessions:
|
||||
type: integer
|
||||
format: int32
|
||||
quota_size:
|
||||
type: integer
|
||||
format: int64
|
||||
quota_files:
|
||||
type: integer
|
||||
format: int32
|
||||
permissions:
|
||||
type: object
|
||||
items:
|
||||
$ref: '#/components/schemas/DirPermissions'
|
||||
minItems: 1
|
||||
example:
|
||||
/:
|
||||
- '*'
|
||||
/somedir:
|
||||
- list
|
||||
- download
|
||||
upload_bandwidth:
|
||||
type: integer
|
||||
description: 'Maximum upload bandwidth as KB/s'
|
||||
download_bandwidth:
|
||||
type: integer
|
||||
description: 'Maximum download bandwidth as KB/s'
|
||||
upload_data_transfer:
|
||||
type: integer
|
||||
description: 'Maximum data transfer allowed for uploads as MB'
|
||||
download_data_transfer:
|
||||
type: integer
|
||||
description: 'Maximum data transfer allowed for downloads as MB'
|
||||
total_data_transfer:
|
||||
type: integer
|
||||
description: 'Maximum total data transfer as MB'
|
||||
filters:
|
||||
$ref: '#/components/schemas/BaseUserFilters'
|
||||
Group:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int32
|
||||
minimum: 1
|
||||
name:
|
||||
type: string
|
||||
description: name is unique
|
||||
description:
|
||||
type: string
|
||||
description: 'optional description'
|
||||
created_at:
|
||||
type: integer
|
||||
format: int64
|
||||
description: creation time as unix timestamp in milliseconds
|
||||
updated_at:
|
||||
type: integer
|
||||
format: int64
|
||||
description: last update time as unix timestamp in milliseconds
|
||||
user_settings:
|
||||
$ref: '#/components/schemas/GroupUserSettings'
|
||||
virtual_folders:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/VirtualFolder'
|
||||
description: mapping between virtual SFTPGo paths and folders
|
||||
users:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: list of usernames associated with this group
|
||||
GroupMapping:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: group name
|
||||
type:
|
||||
enum:
|
||||
- 1
|
||||
- 2
|
||||
description: |
|
||||
Group type:
|
||||
* `1` - Primary group
|
||||
* `2` - Secondaru group
|
||||
BackupData:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -5663,6 +5938,10 @@ components:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/BaseVirtualFolder'
|
||||
groups:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Group'
|
||||
admins:
|
||||
type: array
|
||||
items:
|
||||
|
|
|
@ -308,6 +308,10 @@ func (s *Service) restoreDump(dump *dataprovider.BackupData) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("unable to restore folders from file %#v: %v", s.LoadDataFrom, err)
|
||||
}
|
||||
err = httpd.RestoreGroups(dump.Groups, s.LoadDataFrom, s.LoadDataMode, dataprovider.ActionExecutorSystem, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to restore groups from file %#v: %v", s.LoadDataFrom, err)
|
||||
}
|
||||
err = httpd.RestoreUsers(dump.Users, s.LoadDataFrom, s.LoadDataMode, s.LoadDataQuotaScan, dataprovider.ActionExecutorSystem, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to restore users from file %#v: %v", s.LoadDataFrom, err)
|
||||
|
|
|
@ -222,7 +222,7 @@ func (c *Configuration) getServerConfig() *ssh.ServerConfig {
|
|||
},
|
||||
NextAuthMethodsCallback: func(conn ssh.ConnMetadata) []string {
|
||||
var nextMethods []string
|
||||
user, err := dataprovider.UserExists(conn.User())
|
||||
user, err := dataprovider.GetUserWithGroupSettings(conn.User())
|
||||
if err == nil {
|
||||
nextMethods = user.GetNextAuthMethods(conn.PartialSuccessMethods(), c.PasswordAuthentication)
|
||||
}
|
||||
|
|
|
@ -450,6 +450,7 @@ func TestInitialization(t *testing.T) {
|
|||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
|
||||
err = createTestFile(revokeUserCerts, 10*1024*1024)
|
||||
assert.NoError(t, err)
|
||||
sftpdConf.RevokedUserCertsFile = revokeUserCerts
|
||||
err = sftpdConf.Initialize(configDir)
|
||||
assert.Error(t, err)
|
||||
|
@ -606,6 +607,39 @@ func TestBasicSFTPFsHandling(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGroupSettingsOverride(t *testing.T) {
|
||||
usePubKey := true
|
||||
g := getTestGroup()
|
||||
g.UserSettings.Filters.StartDirectory = "/%username%"
|
||||
group, _, err := httpdtest.AddGroup(g, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
u := getTestUser(usePubKey)
|
||||
u.Groups = []sdk.GroupMapping{
|
||||
{
|
||||
Name: group.Name,
|
||||
Type: sdk.GroupTypePrimary,
|
||||
},
|
||||
}
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
conn, client, err := getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
|
||||
currentDir, err := client.Getwd()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/"+user.Username, currentDir)
|
||||
}
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveGroup(group, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestStartDirectory(t *testing.T) {
|
||||
usePubKey := false
|
||||
startDir := "/st@ rt/dir"
|
||||
|
@ -10297,6 +10331,15 @@ func waitTCPListening(address string) {
|
|||
}
|
||||
}
|
||||
|
||||
func getTestGroup() dataprovider.Group {
|
||||
return dataprovider.Group{
|
||||
BaseGroup: sdk.BaseGroup{
|
||||
Name: "test_group",
|
||||
Description: "test group description",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getTestUser(usePubKey bool) dataprovider.User {
|
||||
user := dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
|
|
6
static/vendor/bootstrap-select/css/bootstrap-select.min.css
vendored
Normal file
6
static/vendor/bootstrap-select/css/bootstrap-select.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
9
static/vendor/bootstrap-select/js/bootstrap-select.min.js
vendored
Normal file
9
static/vendor/bootstrap-select/js/bootstrap-select.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -93,6 +93,14 @@
|
|||
</li>
|
||||
{{end}}
|
||||
|
||||
{{ if .LoggedAdmin.HasPermission "manage_groups"}}
|
||||
<li class="nav-item {{if eq .CurrentURL .GroupsURL}}active{{end}}">
|
||||
<a class="nav-link" href="{{.GroupsURL}}">
|
||||
<i class="fas fa-user-friends"></i>
|
||||
<span>{{.GroupsTitle}}</span></a>
|
||||
</li>
|
||||
{{end}}
|
||||
|
||||
{{ if .LoggedAdmin.HasPermission "view_conns"}}
|
||||
<li class="nav-item {{if eq .CurrentURL .ConnectionsURL}}active{{end}}">
|
||||
<a class="nav-link" href="{{.ConnectionsURL}}">
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
{{define "title"}}{{.Title}}{{end}}
|
||||
|
||||
{{define "extra_css"}}
|
||||
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
|
||||
{{end}}
|
||||
|
||||
{{define "page_body"}}
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
|
@ -92,6 +96,7 @@
|
|||
{{end}}
|
||||
|
||||
{{define "extra_js"}}
|
||||
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function () {
|
||||
onFilesystemChanged('{{.Folder.FsConfig.Provider.Name}}');
|
||||
|
@ -120,7 +125,7 @@
|
|||
});
|
||||
|
||||
});
|
||||
|
||||
{{template "fsjs"}}
|
||||
</script>
|
||||
|
||||
{{template "fsjs"}}
|
||||
{{end}}
|
|
@ -7,7 +7,7 @@
|
|||
<div class="form-group row">
|
||||
<label for="idFilesystem" class="col-sm-2 col-form-label">Storage</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idFilesystem" name="fs_provider"
|
||||
<select class="form-control selectpicker" id="idFilesystem" name="fs_provider"
|
||||
onchange="onFilesystemChanged(this.value)">
|
||||
{{ range ListFSProviders }}
|
||||
<option value="{{.Name}}" {{if eq . $.Provider }}selected{{end}}>{{.ShortInfo}}</option>
|
||||
|
@ -15,14 +15,14 @@
|
|||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{{if .IsUserPage}}
|
||||
{{if or .IsUserPage .IsGroupPage}}
|
||||
<div class="form-group row">
|
||||
<label for="idHomeDir" class="col-sm-2 col-form-label">Home Dir</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idHomeDir" name="home_dir" placeholder="Absolute path to a local directory"
|
||||
value="{{.DirPath}}" aria-describedby="homeDirHelpBlock">
|
||||
<small id="homeDirHelpBlock" class="form-text text-muted">
|
||||
{{if not .DirPath}}{{if .HasUsersBaseDir}}Leave blank for an appropriate default{{else}}Required for local storage providers. For non-local filesystems it will store temporary files, you can leave blank for an appropriate default{{end}}{{end}}
|
||||
{{if not .DirPath}}{{if .HasUsersBaseDir}}Leave blank for an appropriate default{{else}}{{if .IsGroupPage}}Leave blank and the storage to "local" to not override the root filesystem{{else}}Required for local storage providers. For non-local filesystems it will store temporary files, you can leave blank for an appropriate default{{end}}{{end}}{{end}}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -299,7 +299,7 @@
|
|||
<div class="form-group row fsconfig fsconfig-azblobfs">
|
||||
<label for="idAzAccessTier" class="col-sm-2 col-form-label">Access Tier</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idAzAccessTier" name="az_access_tier">
|
||||
<select class="form-control selectpicker" id="idAzAccessTier" name="az_access_tier">
|
||||
<option value="" {{if eq .AzBlobConfig.AccessTier "" }}selected{{end}}>Default</option>
|
||||
<option value="Hot" {{if eq .AzBlobConfig.AccessTier "Hot" }}selected{{end}}>Hot</option>
|
||||
<option value="Cool" {{if eq .AzBlobConfig.AccessTier "Cool" }}selected{{end}}>Cool</option>
|
||||
|
@ -459,6 +459,7 @@
|
|||
{{end}}
|
||||
|
||||
{{define "fsjs"}}
|
||||
<script type="text/javascript">
|
||||
function onFilesystemChanged(val){
|
||||
// each fsconfig form-group has the 'fsconfig' css class
|
||||
// as well as a 'fsconfig-{name}' class where name is the FilesystemProvider.Name
|
||||
|
@ -466,4 +467,5 @@
|
|||
$('.form-group.fsconfig').hide();
|
||||
$('.form-group.fsconfig-'+val).show();
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
754
templates/webadmin/group.html
Normal file
754
templates/webadmin/group.html
Normal file
|
@ -0,0 +1,754 @@
|
|||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}{{.Title}}{{end}}
|
||||
|
||||
{{define "extra_css"}}
|
||||
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
|
||||
{{end}}
|
||||
|
||||
{{define "page_body"}}
|
||||
<!-- Page Heading -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">{{.Title}}</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .Error}}
|
||||
<div class="card mb-4 border-left-warning">
|
||||
<div class="card-body text-form-error">{{.Error}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="card mb-4 border-left-info">
|
||||
<div class="card-body">
|
||||
The <span class="text-success">%username%</span> placeholder will be replaced with the username of the associated users.
|
||||
</div>
|
||||
</div>
|
||||
<form id="group_form" enctype="multipart/form-data" action="{{.CurrentURL}}" method="POST" autocomplete="off">
|
||||
<div class="form-group row">
|
||||
<label for="idGroupName" class="col-sm-2 col-form-label">Name</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idGroupName" name="name" placeholder=""
|
||||
value="{{.Group.Name}}" maxlength="255" autocomplete="nope" required {{if eq .Mode 2}}readonly{{end}}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idDescription" name="description" placeholder=""
|
||||
value="{{.Group.Description}}" maxlength="255" aria-describedby="descriptionHelpBlock">
|
||||
<small id="descriptionHelpBlock" class="form-text text-muted">
|
||||
Optional description
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{template "fshtml" .FsWrapper}}
|
||||
{{if .VirtualFolders}}
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header">
|
||||
<b>Virtual folders</b>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-4">Quota -1 means included within user quota, 0 unlimited. Don't set -1 for shared folders</h6>
|
||||
<div class="form-group row">
|
||||
<div class="col-md-12 form_field_vfolders_outer">
|
||||
{{range $idx, $val := .Group.VirtualFolders}}
|
||||
<div class="row form_field_vfolder_outer_row">
|
||||
<div class="form-group col-md-3">
|
||||
<input type="text" class="form-control" id="idVolderPath{{$idx}}" name="vfolder_path" placeholder="mount path, i.e. /vfolder" value="{{$val.VirtualPath}}" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control selectpicker" data-live-search="true" id="idVfolderName{{$idx}}" name="vfolder_name">
|
||||
<option value=""></option>
|
||||
{{range $.VirtualFolders}}
|
||||
<option value="{{.Name}}" {{if eq $val.Name .Name}}selected{{end}}>{{.Name}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<input type="number" class="form-control" id="idVfolderQuotaSize{{$idx}}" name="vfolder_quota_size"
|
||||
value="{{$val.QuotaSize}}" min="-1" aria-describedby="vqsHelpBlock{{$idx}}">
|
||||
<small id="vqsHelpBlock{{$idx}}" class="form-text text-muted">
|
||||
Quota size (bytes)
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<input type="number" class="form-control" id="idVfolderQuotaFiles{{$idx}}" name="vfolder_quota_files"
|
||||
value="{{$val.QuotaFiles}}" min="-1" aria-describedby="vqfHelpBlock{{$idx}}">
|
||||
<small id="vqfHelpBlock{{$idx}}" class="form-text text-muted">
|
||||
Quota files
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_vfolder_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="row form_field_vfolder_outer_row">
|
||||
<div class="form-group col-md-3">
|
||||
<input type="text" class="form-control" id="idVolderPath0" name="vfolder_path" placeholder="mount path, i.e. /vfolder" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control selectpicker" data-live-search="true" id="idVfolderName0" name="vfolder_name">
|
||||
<option value=""></option>
|
||||
{{range .VirtualFolders}}
|
||||
<option value="{{.Name}}">{{.Name}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<input type="number" class="form-control" id="idVfolderQuotaSize0" name="vfolder_quota_size"
|
||||
value="" min="-1" aria-describedby="vqsHelpBlock0">
|
||||
<small id="vqsHelpBlock0" class="form-text text-muted">
|
||||
Quota size (bytes)
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<input type="number" class="form-control" id="idVfolderQuotaFiles0" name="vfolder_quota_files"
|
||||
value="" min="-1" aria-describedby="vqfHelpBlock0">
|
||||
<small id="vqfHelpBlock0" class="form-text text-muted">
|
||||
Quota files
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_vfolder_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mx-1">
|
||||
<button type="button" class="btn btn-secondary add_new_vfolder_field_btn">
|
||||
<i class="fas fa-plus"></i> Add new virtual folder
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="accordion" id="accordionUser">
|
||||
<div class="card">
|
||||
<div class="card-header" id="headingPermissions">
|
||||
<h2 class="mb-0">
|
||||
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse"
|
||||
data-target="#collapsePermissions" aria-expanded="true" aria-controls="collapsePermissions">
|
||||
<h6 class="m-0 font-weight-bold text-primary">ACLs</h6>
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div id="collapsePermissions" class="collapse" aria-labelledby="headingPermissions" data-parent="#accordionUser">
|
||||
<div class="card-body">
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header">
|
||||
<b>Per-directory permissions</b>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group row">
|
||||
<div class="col-md-12 form_field_dirperms_outer">
|
||||
{{range $idx, $dirPerms := .Group.GetPermissions -}}
|
||||
<div class="row form_field_dirperms_outer_row">
|
||||
<div class="form-group col-md-8">
|
||||
<input type="text" class="form-control" id="idSubDirPermsPath{{$idx}}" name="sub_perm_path{{$idx}}" placeholder="directory path, i.e. /dir" value="{{$dirPerms.Path}}" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control selectpicker" id="idSubDirPermissions{{$idx}}" name="sub_perm_permissions{{$idx}}" multiple>
|
||||
{{range $validPerm := $.ValidPerms}}
|
||||
<option value="{{$validPerm}}" {{range $perm := $dirPerms.Permissions }}{{if eq $perm $validPerm}}selected{{end}}{{end}}>{{$validPerm}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_dirperms_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="row form_field_dirperms_outer_row">
|
||||
<div class="form-group col-md-8">
|
||||
<input type="text" class="form-control" id="idSubDirPermsPath0" name="sub_perm_path0" placeholder="directory path, i.e. /dir" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control selectpicker" id="idSubDirPermissions0" name="sub_perm_permissions0" multiple>
|
||||
{{range $validPerm := .ValidPerms}}
|
||||
<option value="{{$validPerm}}">{{$validPerm}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_dirperms_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mx-1">
|
||||
<button type="button" class="btn btn-secondary add_new_dirperms_field_btn">
|
||||
<i class="fas fa-plus"></i> Add new directory permissions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header">
|
||||
<b>Per-directory pattern restrictions</b>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-4">Comma separated denied or allowed files/directories, based on shell patterns.</h6>
|
||||
<p class="card-text">Denied entries are visible in directory listing by default, you can hide them by setting the "Hidden" policy, but please be aware that this may cause performance issues for large directories</p>
|
||||
<div class="form-group row">
|
||||
<div class="col-md-12 form_field_patterns_outer">
|
||||
{{range $idx, $pattern := .Group.UserSettings.Filters.GetFlatFilePatterns -}}
|
||||
<div class="row form_field_patterns_outer_row">
|
||||
<div class="form-group col-md-3">
|
||||
<input type="text" class="form-control" id="idPatternPath{{$idx}}" name="pattern_path{{$idx}}" placeholder="directory path, i.e. /dir" value="{{$pattern.Path}}" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<input type="text" class="form-control" id="idPatterns{{$idx}}" name="patterns{{$idx}}" placeholder="*.zip,?.txt" value="{{$pattern.GetCommaSeparatedPatterns}}" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<select class="form-control selectpicker" id="idPatternType{{$idx}}" name="pattern_type{{$idx}}">
|
||||
<option value="denied" {{if $pattern.IsDenied}}selected{{end}}>Denied</option>
|
||||
<option value="allowed" {{if $pattern.IsAllowed}}selected{{end}}>Allowed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<select class="form-control selectpicker" id="idPatternPolicy{{$idx}}" name="pattern_policy{{$idx}}">
|
||||
<option value="0" {{if eq $pattern.DenyPolicy 0}}selected{{end}}>Visible</option>
|
||||
<option value="1" {{if eq $pattern.DenyPolicy 1}}selected{{end}}>Hidden</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_pattern_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="row form_field_patterns_outer_row">
|
||||
<div class="form-group col-md-3">
|
||||
<input type="text" class="form-control" id="idPatternPath0" name="pattern_path0" placeholder="directory path, i.e. /dir" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<input type="text" class="form-control" id="idPatterns0" name="patterns0" placeholder="*.zip,?.txt" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<select class="form-control selectpicker" id="idPatternType0" name="pattern_type0">
|
||||
<option value="denied">Denied</option>
|
||||
<option value="allowed">Allowed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<select class="form-control selectpicker" id="idPatternPolicy0" name="pattern_policy0">
|
||||
<option value="0">Visible</option>
|
||||
<option value="1">Hidden</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_pattern_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mx-1">
|
||||
<button type="button" class="btn btn-secondary add_new_pattern_field_btn">
|
||||
<i class="fas fa-plus"></i> Add new file pattern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idMaxSessions" class="col-sm-2 col-form-label">Max sessions</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" class="form-control" id="idMaxSessions" name="max_sessions" placeholder=""
|
||||
value="{{.Group.UserSettings.MaxSessions}}" min="0" aria-describedby="sessionsHelpBlock">
|
||||
<small id="sessionsHelpBlock" class="form-text text-muted">
|
||||
Maximun number of concurrent sessions. 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idProtocols" class="col-sm-2 col-form-label">Denied protocols</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control selectpicker" id="idProtocols" name="denied_protocols" multiple>
|
||||
{{range $protocol := .ValidProtocols}}
|
||||
<option value="{{$protocol}}" {{range $p :=$.Group.UserSettings.Filters.DeniedProtocols }}{{if eq $p $protocol}}selected{{end}}{{end}}>{{$protocol}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idLoginMethods" class="col-sm-2 col-form-label">Denied login methods</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control selectpicker" id="idLoginMethods" name="denied_login_methods" multiple aria-describedby="deniedLoginMethodsHelpBlock">
|
||||
{{range $method := .ValidLoginMethods}}
|
||||
<option value="{{$method}}" {{range $m :=$.Group.UserSettings.Filters.DeniedLoginMethods }}{{if eq $m $method}}selected{{end}}{{end}}>{{$method}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
<small id="deniedLoginMethodsHelpBlock" class="form-text text-muted">
|
||||
"password" is valid for all supported protocols, "password-over-SSH" only for SSH/SFTP/SCP
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idTwoFactorProtocols" class="col-sm-2 col-form-label">Require two-factor auth for</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control selectpicker" id="idTwoFactorProtocols" name="required_two_factor_protocols" multiple>
|
||||
{{range $protocol := .TwoFactorProtocols}}
|
||||
<option value="{{$protocol}}" {{range $p :=$.Group.UserSettings.Filters.TwoFactorAuthProtocols }}{{if eq $p $protocol}}selected{{end}}{{end}}>{{$protocol}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idWebClient" class="col-sm-2 col-form-label">Web client/REST API</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control selectpicker" id="idWebClient" name="web_client_options" multiple>
|
||||
{{range $option := .WebClientOptions}}
|
||||
<option value="{{$option}}" {{range $p :=$.Group.UserSettings.Filters.WebClient }}{{if eq $p $option}}selected{{end}}{{end}}>{{$option}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idDeniedIP" class="col-sm-2 col-form-label">Denied IP/Mask</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="idDeniedIP" name="denied_ip" rows="3" placeholder=""
|
||||
aria-describedby="deniedIPHelpBlock">{{.Group.GetDeniedIPAsString}}</textarea>
|
||||
<small id="deniedIPHelpBlock" class="form-text text-muted">
|
||||
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idAllowedIP" class="col-sm-2 col-form-label">Allowed IP/Mask</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="idAllowedIP" name="allowed_ip" rows="3" placeholder=""
|
||||
aria-describedby="allowedIPHelpBlock">{{.Group.GetAllowedIPAsString}}</textarea>
|
||||
<small id="allowedIPHelpBlock" class="form-text text-muted">
|
||||
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header" id="headingQuota">
|
||||
<h2 class="mb-0">
|
||||
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse"
|
||||
data-target="#collapseQuota" aria-expanded="false" aria-controls="collapseQuota">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Disk quota and bandwidth limits</h6>
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
<div id="collapseQuota" class="collapse" aria-labelledby="headingTwo" data-parent="#accordionUser">
|
||||
<div class="card-body">
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idQuotaSize" class="col-sm-2 col-form-label">Quota size (bytes)</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="number" class="form-control" id="idQuotaSize" name="quota_size" placeholder=""
|
||||
value="{{.Group.UserSettings.QuotaSize}}" min="0" aria-describedby="qsHelpBlock">
|
||||
<small id="qsHelpBlock" class="form-text text-muted">
|
||||
0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-sm-2"></div>
|
||||
<label for="idQuotaFiles" class="col-sm-2 col-form-label">Quota files</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="number" class="form-control" id="idQuotaFiles" name="quota_files" placeholder=""
|
||||
value="{{.Group.UserSettings.QuotaFiles}}" min="0" aria-describedby="qfHelpBlock">
|
||||
<small id="qfHelpBlock" class="form-text text-muted">
|
||||
0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idMaxUploadSize" class="col-sm-2 col-form-label">Max file upload size (bytes)</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" class="form-control" id="idMaxUploadSize" name="max_upload_file_size"
|
||||
placeholder="" value="{{.Group.UserSettings.Filters.MaxUploadFileSize}}" min="0"
|
||||
aria-describedby="fqsHelpBlock">
|
||||
<small id="fqsHelpBlock" class="form-text text-muted">
|
||||
Maximum upload size for a single file. 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idUploadBandwidth" class="col-sm-2 col-form-label">Bandwidth UL (KB/s)</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="number" class="form-control" id="idUploadBandwidth" name="upload_bandwidth"
|
||||
placeholder="" value="{{.Group.UserSettings.UploadBandwidth}}" min="0" aria-describedby="ulHelpBlock">
|
||||
<small id="ulHelpBlock" class="form-text text-muted">
|
||||
0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-sm-2"></div>
|
||||
<label for="idDownloadBandwidth" class="col-sm-2 col-form-label">Bandwidth DL (KB/s)</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="number" class="form-control" id="idDownloadBandwidth" name="download_bandwidth"
|
||||
placeholder="" value="{{.Group.UserSettings.DownloadBandwidth}}" min="0" aria-describedby="dlHelpBlock">
|
||||
<small id="dlHelpBlock" class="form-text text-muted">
|
||||
0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header">
|
||||
<b>Per-source bandwidth speed limits</b>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group row">
|
||||
<div class="col-md-12 form_field_bwlimits_outer">
|
||||
{{range $idx, $bwLimit := .Group.UserSettings.Filters.BandwidthLimits -}}
|
||||
<div class="row form_field_bwlimits_outer_row">
|
||||
<div class="form-group col-md-8">
|
||||
<textarea class="form-control" id="idBandwidthLimitSources{{$idx}}" name="bandwidth_limit_sources{{$idx}}" rows="4" placeholder=""
|
||||
aria-describedby="bwLimitSourcesHelpBlock{{$idx}}">{{$bwLimit.GetSourcesAsString}}</textarea>
|
||||
<small id="bwLimitSourcesHelpBlock{{$idx}}" class="form-text text-muted">
|
||||
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idUploadBandwidthSource{{$idx}}" name="upload_bandwidth_source{{$idx}}"
|
||||
placeholder="" value="{{$bwLimit.UploadBandwidth}}" min="0" aria-describedby="ulHelpBlock{{$idx}}">
|
||||
<small id="ulHelpBlock{{$idx}}" class="form-text text-muted">
|
||||
UL (KB/s). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idDownloadBandwidthSource{{$idx}}" name="download_bandwidth_source{{$idx}}"
|
||||
placeholder="" value="{{$bwLimit.DownloadBandwidth}}" min="0" aria-describedby="dlHelpBlock{{$idx}}">
|
||||
<small id="dlHelpBlock{{$idx}}" class="form-text text-muted">
|
||||
DL (KB/s). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_bwlimit_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="row form_field_bwlimits_outer_row">
|
||||
<div class="form-group col-md-8">
|
||||
<textarea class="form-control" id="idBandwidthLimitSources0" name="bandwidth_limit_sources0" rows="4" placeholder=""
|
||||
aria-describedby="bwLimitSourcesHelpBlock0"></textarea>
|
||||
<small id="bwLimitSourcesHelpBlock0" class="form-text text-muted">
|
||||
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idUploadBandwidthSource0" name="upload_bandwidth_source0"
|
||||
placeholder="" value="" min="0" aria-describedby="ulHelpBlock0">
|
||||
<small id="ulHelpBlock0" class="form-text text-muted">
|
||||
UL (KB/s). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idDownloadBandwidthSource0" name="download_bandwidth_source0"
|
||||
placeholder="" value="" min="0" aria-describedby="dlHelpBlock0">
|
||||
<small id="dlHelpBlock0" class="form-text text-muted">
|
||||
DL (KB/s). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_bwlimit_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mx-1">
|
||||
<button type="button" class="btn btn-secondary add_new_bwlimit_field_btn">
|
||||
<i class="fas fa-plus"></i> Add new speed limit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idTransferUL" class="col-sm-2 col-form-label">Upload data transfer (MB)</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="number" class="form-control" id="idTransferUL" name="upload_data_transfer" placeholder=""
|
||||
value="{{.Group.UserSettings.UploadDataTransfer}}" min="0" aria-describedby="ulTransferHelpBlock">
|
||||
<small id="ulTransferHelpBlock" class="form-text text-muted">
|
||||
Maximum data transfer allowed for uploads. 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-sm-2"></div>
|
||||
<label for="idTransferDL" class="col-sm-2 col-form-label">Download data transfer (MB)</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="number" class="form-control" id="idTransferDL" name="download_data_transfer" placeholder=""
|
||||
value="{{.Group.UserSettings.DownloadDataTransfer}}" min="0" aria-describedby="dlTransferHelpBlock">
|
||||
<small id="dlTransferHelpBlock" class="form-text text-muted">
|
||||
Maximum data transfer allowed for downloads. 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idTransferTotal" class="col-sm-2 col-form-label">Total data transfer (MB)</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" class="form-control" id="idTransferTotal" name="total_data_transfer"
|
||||
placeholder="" value="{{.Group.UserSettings.TotalDataTransfer}}" min="0"
|
||||
aria-describedby="totalTransferHelpBlock">
|
||||
<small id="totalTransferHelpBlock" class="form-text text-muted">
|
||||
Maximum data transfer allowed for uploads + downloads. Replace the individual limits. 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header">
|
||||
<b>Per-source data transfer limits</b>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group row">
|
||||
<div class="col-md-12 form_field_dtlimits_outer">
|
||||
{{range $idx, $dtLimit := .Group.UserSettings.Filters.DataTransferLimits -}}
|
||||
<div class="row form_field_dtlimits_outer_row">
|
||||
<div class="form-group col-md-5">
|
||||
<textarea class="form-control" id="idDataTransferLimitSources{{$idx}}" name="data_transfer_limit_sources{{$idx}}" rows="4" placeholder=""
|
||||
aria-describedby="dtLimitSourcesHelpBlock{{$idx}}">{{$dtLimit.GetSourcesAsString}}</textarea>
|
||||
<small id="dtLimitSourcesHelpBlock{{$idx}}" class="form-text text-muted">
|
||||
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idUploadTransferSource{{$idx}}" name="upload_data_transfer_source{{$idx}}"
|
||||
placeholder="" value="{{$dtLimit.UploadDataTransfer}}" min="0" aria-describedby="ulDtHelpBlock{{$idx}}">
|
||||
<small id="ulDtHelpBlock{{$idx}}" class="form-text text-muted">
|
||||
UL (MB). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idDownloadTransferSource{{$idx}}" name="download_data_transfer_source{{$idx}}"
|
||||
placeholder="" value="{{$dtLimit.DownloadDataTransfer}}" min="0" aria-describedby="dlDtHelpBlock{{$idx}}">
|
||||
<small id="dlDtHelpBlock{{$idx}}" class="form-text text-muted">
|
||||
DL (MB). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idTotalTransferSource{{$idx}}" name="total_data_transfer_source{{$idx}}"
|
||||
placeholder="" value="{{$dtLimit.TotalDataTransfer}}" min="0" aria-describedby="totalDtHelpBlock{{$idx}}">
|
||||
<small id="totalDtHelpBlock{{$idx}}" class="form-text text-muted">
|
||||
Total (MB). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_dtlimit_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="row form_field_dtlimits_outer_row">
|
||||
<div class="form-group col-md-5">
|
||||
<textarea class="form-control" id="idDataTransferLimitSources0" name="data_transfer_limit_sources0" rows="4" placeholder=""
|
||||
aria-describedby="dtLimitSourcesHelpBlock0"></textarea>
|
||||
<small id="dtLimitSourcesHelpBlock0" class="form-text text-muted">
|
||||
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idUploadTransferSource0" name="upload_data_transfer_source0"
|
||||
placeholder="" value="" min="0" aria-describedby="ulDtHelpBlock0">
|
||||
<small id="ulDtHelpBlock0" class="form-text text-muted">
|
||||
UL (MB). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idDownloadTransferSource0" name="download_data_transfer_source0"
|
||||
placeholder="" value="" min="0" aria-describedby="dlDtHelpBlock0">
|
||||
<small id="dlDtHelpBlock0" class="form-text text-muted">
|
||||
DL (MB). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idTotalTransferSource0" name="total_data_transfer_source0"
|
||||
placeholder="" value="" min="0" aria-describedby="totalDtHelpBlock0">
|
||||
<small id="totalDtHelpBlock0" class="form-text text-muted">
|
||||
Total (MB). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_dtlimit_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mx-1">
|
||||
<button type="button" class="btn btn-secondary add_new_dtlimit_field_btn">
|
||||
<i class="fas fa-plus"></i> Add new data transfer limit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header" id="headingAdvanced">
|
||||
<h2 class="mb-0">
|
||||
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse"
|
||||
data-target="#collapseAdvanced" aria-expanded="false" aria-controls="collapseAdvanced">
|
||||
<h6 class="m-0 font-weight-bold text-primary">More</h6>
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
<div id="collapseAdvanced" class="collapse" aria-labelledby="headingTwo" data-parent="#accordionUser">
|
||||
<div class="card-body">
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idStartDirectory" class="col-sm-2 col-form-label">Start directory</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idStartDirectory" name="start_directory" placeholder=""
|
||||
value="{{.Group.UserSettings.Filters.StartDirectory}}" aria-describedby="startDirHelpBlock">
|
||||
<small id="startDirHelpBlock" class="form-text text-muted">
|
||||
Alternate start directory to use instead of "/". Supported for SFTP/FTP/HTTP
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idTLSUsername" class="col-sm-2 col-form-label">TLS username</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control selectpicker" id="idTLSUsername" name="tls_username" aria-describedby="tlsUsernameHelpBlock">
|
||||
<option value="None" {{if eq .Group.UserSettings.Filters.TLSUsername "None" }}selected{{end}}>None</option>
|
||||
<option value="CommonName" {{if eq .Group.UserSettings.Filters.TLSUsername "CommonName" }}selected{{end}}>Common Name</option>
|
||||
</select>
|
||||
<small id="tlsUsernameHelpBlock" class="form-text text-muted">
|
||||
Defines the TLS certificate field to use as username. Ignored if mutual TLS is disabled
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idHooks" class="col-sm-2 col-form-label">Hooks</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control selectpicker" id="idHooks" name="hooks" multiple>
|
||||
<option value="external_auth_disabled" {{if .Group.UserSettings.Filters.Hooks.ExternalAuthDisabled}}selected{{end}}>
|
||||
External auth disabled
|
||||
</option>
|
||||
<option value="pre_login_disabled" {{if .Group.UserSettings.Filters.Hooks.PreLoginDisabled}}selected{{end}}>
|
||||
Pre-login disabled
|
||||
</option>
|
||||
<option value="check_password_disabled" {{if .Group.UserSettings.Filters.Hooks.CheckPasswordDisabled}}selected{{end}}>
|
||||
Check password disabled
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="idDisableFsChecks" name="disable_fs_checks"
|
||||
{{if .Group.UserSettings.Filters.DisableFsChecks}}checked{{end}} aria-describedby="disableFsChecksHelpBlock">
|
||||
<label for="idDisableFsChecks" class="form-check-label">Disable filesystem checks</label>
|
||||
<small id="disableFsChecksHelpBlock" class="form-text text-muted">
|
||||
Disable checks for existence and automatic creation of home directory and virtual folders
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="idAllowAPIKeyAuth" name="allow_api_key_auth"
|
||||
{{if .Group.UserSettings.Filters.AllowAPIKeyAuth}}checked{{end}} aria-describedby="allowAPIKeyAuthHelpBlock">
|
||||
<label for="idAllowAPIKeyAuth" class="form-check-label">Allow API key authentication</label>
|
||||
<small id="allowAPIKeyAuthHelpBlock" class="form-text text-muted">
|
||||
Allow to impersonate the associated users, in REST API, with an API key
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row {{if not .Group.HasExternalAuth}}d-none{{end}}">
|
||||
<label for="idExtAuthCacheTime" class="col-sm-2 col-form-label">External auth cache time</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" min="0" class="form-control" id="idExtAuthCacheTime" name="external_auth_cache_time" placeholder=""
|
||||
value="{{.Group.UserSettings.Filters.ExternalAuthCacheTime}}" aria-describedby="extAuthCacheHelpBlock">
|
||||
<small id="extAuthCacheHelpBlock" class="form-text text-muted">
|
||||
Cache time, in seconds, for users authenticated using an external auth hook. 0 means no cache
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
|
||||
<div class="col-sm-12 text-right px-0">
|
||||
<button type="submit" class="btn btn-primary mt-3 ml-3 px-5" name="form_action" value="submit">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "extra_js"}}
|
||||
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function () {
|
||||
{{if .Error}}
|
||||
$('#accordionUser .collapse').removeAttr("data-parent").collapse('show');
|
||||
{{end}}
|
||||
onFilesystemChanged('{{.Group.UserSettings.FsConfig.Provider.Name}}');
|
||||
});
|
||||
</script>
|
||||
|
||||
{{template "fsjs"}}
|
||||
{{template "shared_user_group" .}}
|
||||
{{end}}
|
193
templates/webadmin/groups.html
Normal file
193
templates/webadmin/groups.html
Normal file
|
@ -0,0 +1,193 @@
|
|||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}{{.Title}}{{end}}
|
||||
|
||||
{{define "extra_css"}}
|
||||
<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
|
||||
{{end}}
|
||||
|
||||
{{define "page_body"}}
|
||||
<div id="errorMsg" class="card mb-4 border-left-warning" style="display: none;">
|
||||
<div id="errorTxt" class="card-body text-form-error"></div>
|
||||
</div>
|
||||
|
||||
<div id="successMsg" class="card mb-4 border-left-success" style="display: none;">
|
||||
<div id="successTxt" class="card-body"></div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">View and manage groups</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Used by</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Groups}}
|
||||
<tr>
|
||||
<td>{{.Name}}</td>
|
||||
<td>{{.Description}}</td>
|
||||
<td>{{.GetUsersAsString}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "dialog"}}
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">
|
||||
Confirmation required
|
||||
</h5>
|
||||
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">Do you want to delete the selected group? A referenced group cannot be removed</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" type="button" data-dismiss="modal">
|
||||
Cancel
|
||||
</button>
|
||||
<a class="btn btn-warning" href="#" onclick="deleteAction()">
|
||||
Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "extra_js"}}
|
||||
<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/dataTables.select.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
function deleteAction() {
|
||||
var table = $('#dataTable').DataTable();
|
||||
table.button('delete:name').enable(false);
|
||||
var groupName = table.row({ selected: true }).data()[0];
|
||||
var path = '{{.GroupURL}}' + "/" + fixedEncodeURIComponent(groupName);
|
||||
$('#deleteModal').modal('hide');
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: 'DELETE',
|
||||
dataType: 'json',
|
||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||
timeout: 15000,
|
||||
success: function (result) {
|
||||
window.location.href = '{{.GroupsURL}}';
|
||||
},
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
var txt = "Unable to delete the selected group";
|
||||
if ($xhr) {
|
||||
var json = $xhr.responseJSON;
|
||||
if (json) {
|
||||
if (json.message){
|
||||
txt += ": " + json.message;
|
||||
} else {
|
||||
txt += ": " + json.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
$('#errorTxt').text(txt);
|
||||
$('#errorMsg').show();
|
||||
setTimeout(function () {
|
||||
$('#errorMsg').hide();
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$.fn.dataTable.ext.buttons.add = {
|
||||
text: '<i class="fas fa-plus"></i>',
|
||||
name: 'add',
|
||||
titleAttr: "Add",
|
||||
action: function (e, dt, node, config) {
|
||||
window.location.href = '{{.GroupURL}}';
|
||||
}
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.edit = {
|
||||
text: '<i class="fas fa-pen"></i>',
|
||||
name: 'edit',
|
||||
titleAttr: "Edit",
|
||||
action: function (e, dt, node, config) {
|
||||
var groupName = table.row({ selected: true }).data()[0];
|
||||
var path = '{{.GroupURL}}' + "/" + fixedEncodeURIComponent(groupName);
|
||||
window.location.href = path;
|
||||
},
|
||||
enabled: false
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.delete = {
|
||||
text: '<i class="fas fa-trash"></i>',
|
||||
name: 'delete',
|
||||
titleAttr: "Delete",
|
||||
action: function (e, dt, node, config) {
|
||||
$('#deleteModal').modal('show');
|
||||
},
|
||||
enabled: false
|
||||
};
|
||||
|
||||
var table = $('#dataTable').DataTable({
|
||||
"select": {
|
||||
"style": "single",
|
||||
"blurable": true
|
||||
},
|
||||
"stateSave": true,
|
||||
"stateDuration": 0,
|
||||
"buttons": [],
|
||||
"scrollX": false,
|
||||
"scrollY": false,
|
||||
"responsive": true,
|
||||
"language": {
|
||||
"emptyTable": "No group defined"
|
||||
},
|
||||
"order": [[0, 'asc']]
|
||||
});
|
||||
|
||||
new $.fn.dataTable.FixedHeader( table );
|
||||
|
||||
{{if .LoggedAdmin.HasPermission "manage_groups"}}
|
||||
table.button().add(0,'delete');
|
||||
table.button().add(0,'edit');
|
||||
table.button().add(0,'add');
|
||||
|
||||
table.buttons().container().appendTo('.col-md-6:eq(0)', table.table().container());
|
||||
|
||||
table.on('select deselect', function () {
|
||||
var selectedRows = table.rows({ selected: true }).count();
|
||||
table.button('delete:name').enable(selectedRows == 1);
|
||||
table.button('edit:name').enable(selectedRows == 1);
|
||||
});
|
||||
{{end}}
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
217
templates/webadmin/sharedcomponents.html
Normal file
217
templates/webadmin/sharedcomponents.html
Normal file
|
@ -0,0 +1,217 @@
|
|||
{{define "shared_user_group"}}
|
||||
<script type="text/javascript">
|
||||
$("body").on("click", ".add_new_dirperms_field_btn", function () {
|
||||
var index = $(".form_field_dirperms_outer").find(".form_field_dirperms_outer_row").length;
|
||||
while (document.getElementById("idSubDirPermsPath"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_dirperms_outer").append(`
|
||||
<div class="row form_field_dirperms_outer_row">
|
||||
<div class="form-group col-md-8">
|
||||
<input type="text" class="form-control" id="idSubDirPermsPath${index}" name="sub_perm_path${index}" placeholder="directory path, i.e. /dir" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control" id="idSubDirPermissions${index}" name="sub_perm_permissions${index}" multiple>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_dirperms_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
{{- range .ValidPerms}}
|
||||
$("#idSubDirPermissions"+index).append($('<option>').val('{{.}}').text('{{.}}'));
|
||||
{{- end}}
|
||||
$("#idSubDirPermissions"+index).selectpicker();
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_dirperms_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_dirperms_outer_row").remove();
|
||||
});
|
||||
|
||||
$("body").on("click", ".add_new_vfolder_field_btn", function () {
|
||||
var index = $(".form_field_vfolders_outer").find(".form_field_vfolder_outer_row").length;
|
||||
while (document.getElementById("idVolderPath" + index) != null) {
|
||||
index++;
|
||||
}
|
||||
$(".form_field_vfolders_outer").append(`
|
||||
<div class="row form_field_vfolder_outer_row">
|
||||
<div class="form-group col-md-3">
|
||||
<input type="text" class="form-control" id="idVolderPath${index}" name="vfolder_path" placeholder="mount path, i.e. /vfolder" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control" id="idVfolderName${index}" name="vfolder_name">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<input type="number" class="form-control" id="idVfolderQuotaSize${index}" name="vfolder_quota_size"
|
||||
value="" min="-1" aria-describedby="vqsHelpBlock${index}">
|
||||
<small id="vqsHelpBlock${index}" class="form-text text-muted">
|
||||
Quota size (bytes)
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<input type="number" class="form-control" id="idVfolderQuotaFiles${index}" name="vfolder_quota_files"
|
||||
value="" min="-1" aria-describedby="vqfHelpBlock${index}">
|
||||
<small id="vqfHelpBlock${index}" class="form-text text-muted">
|
||||
Quota files
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_vfolder_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
{{- range .VirtualFolders}}
|
||||
$("#idVfolderName"+index).append($('<option>').val('{{.Name}}').text('{{.Name}}'));
|
||||
{{- end}}
|
||||
$("#idVfolderName"+index).selectpicker({'liveSearch': true});
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_vfolder_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_vfolder_outer_row").remove();
|
||||
});
|
||||
|
||||
$("body").on("click", ".add_new_bwlimit_field_btn", function () {
|
||||
var index = $(".form_field_bwlimits_outer").find(".form_field_bwlimits_outer_row").length;
|
||||
while (document.getElementById("idBandwidthLimitSources"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_bwlimits_outer").append(`
|
||||
<div class="row form_field_bwlimits_outer_row">
|
||||
<div class="form-group col-md-8">
|
||||
<textarea class="form-control" id="idBandwidthLimitSources0" name="bandwidth_limit_sources${index}" rows="4" placeholder=""
|
||||
aria-describedby="bwLimitSourcesHelpBlock${index}"></textarea>
|
||||
<small id="bwLimitSourcesHelpBlock${index}" class="form-text text-muted">
|
||||
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idUploadBandwidthSource${index}" name="upload_bandwidth_source${index}"
|
||||
placeholder="" value="" min="0" aria-describedby="ulHelpBlock${index}">
|
||||
<small id="ulHelpBlock${index}" class="form-text text-muted">
|
||||
UL (KB/s). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idDownloadBandwidthSource${index}" name="download_bandwidth_source${index}"
|
||||
placeholder="" value="" min="0" aria-describedby="dlHelpBlock${index}">
|
||||
<small id="dlHelpBlock${index}" class="form-text text-muted">
|
||||
DL (KB/s). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_bwlimit_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_bwlimit_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_bwlimits_outer_row").remove();
|
||||
});
|
||||
|
||||
$("body").on("click", ".add_new_dtlimit_field_btn", function () {
|
||||
var index = $(".form_field_dtlimits_outer").find(".form_field_dtlimits_outer_row").length;
|
||||
while (document.getElementById("idDataTransferLimitSources"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_dtlimits_outer").append(`
|
||||
<div class="row form_field_dtlimits_outer_row">
|
||||
<div class="form-group col-md-5">
|
||||
<textarea class="form-control" id="idDataTransferLimitSources${index}" name="data_transfer_limit_sources${index}" rows="4" placeholder=""
|
||||
aria-describedby="dtLimitSourcesHelpBlock${index}"></textarea>
|
||||
<small id="dtLimitSourcesHelpBlock${index}" class="form-text text-muted">
|
||||
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idUploadTransferSource${index}" name="upload_data_transfer_source${index}"
|
||||
placeholder="" value="" min="0" aria-describedby="ulDtHelpBlock${index}">
|
||||
<small id="ulDtHelpBlock${index}" class="form-text text-muted">
|
||||
UL (MB). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idDownloadTransferSource${index}" name="download_data_transfer_source${index}"
|
||||
placeholder="" value="" min="0" aria-describedby="dlDtHelpBlock${index}">
|
||||
<small id="dlDtHelpBlock${index}" class="form-text text-muted">
|
||||
DL (MB). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idTotalTransferSource${index}" name="total_data_transfer_source${index}"
|
||||
placeholder="" value="" min="0" aria-describedby="totalDtHelpBlock${index}">
|
||||
<small id="totalDtHelpBlock${index}" class="form-text text-muted">
|
||||
Total (MB). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_dtlimit_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_dtlimit_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_dtlimits_outer_row").remove();
|
||||
});
|
||||
|
||||
$("body").on("click", ".add_new_pattern_field_btn", function () {
|
||||
var index = $(".form_field_patterns_outer").find(".form_field_patterns_outer_row").length;
|
||||
while (document.getElementById("idPatternPath"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_patterns_outer").append(`
|
||||
<div class="row form_field_patterns_outer_row">
|
||||
<div class="form-group col-md-3">
|
||||
<input type="text" class="form-control" id="idPatternPath${index}" name="pattern_path${index}" placeholder="directory path, i.e. /dir" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<input type="text" class="form-control" id="idPatterns${index}" name="patterns${index}" placeholder="*.zip,?.txt" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<select class="form-control" id="idPatternType${index}" name="pattern_type${index}">
|
||||
<option value="denied">Denied</option>
|
||||
<option value="allowed">Allowed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<select class="form-control" id="idPatternPolicy${index}" name="pattern_policy${index}">
|
||||
<option value="0">Visible</option>
|
||||
<option value="1">Hidden</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_pattern_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
$("#idPatternType"+index).selectpicker();
|
||||
$("#idPatternPolicy"+index).selectpicker();
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_pattern_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_patterns_outer_row").remove();
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
{{define "extra_css"}}
|
||||
<link href="{{.StaticURL}}/vendor/tempusdominus/css/tempusdominus-bootstrap-4.min.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
|
||||
{{end}}
|
||||
|
||||
{{define "page_body"}}
|
||||
|
@ -140,6 +141,36 @@
|
|||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header">
|
||||
<b>Groups</b>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-4">Group membership impart the group settings if no override exist</h6>
|
||||
<div class="form-group row">
|
||||
<label for="idPrimaryGroup" class="col-sm-2 col-form-label">Primary group</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control selectpicker" data-live-search="true" id="idPrimaryGroup" name="primary_group">
|
||||
<option value=""></option>
|
||||
{{- range .Groups}}
|
||||
<option value="{{.Name}}" {{if $.User.HasPrimaryGroup .Name}}selected{{end}}>{{.Name}}</option>
|
||||
{{- end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="idSecondaryGroup" class="col-sm-2 col-form-label">Secondary groups</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control selectpicker" data-live-search="true" id="idSecondaryGroup" name="secondary_groups" multiple>
|
||||
{{- range .Groups}}
|
||||
<option value="{{.Name}}" {{if $.User.HasSecondaryGroup .Name}}selected{{end}}>{{.Name}}</option>
|
||||
{{- end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{template "fshtml" .FsWrapper}}
|
||||
{{if .VirtualFolders}}
|
||||
<div class="card bg-light mb-3">
|
||||
|
@ -156,7 +187,7 @@
|
|||
<input type="text" class="form-control" id="idVolderPath{{$idx}}" name="vfolder_path" placeholder="mount path, i.e. /vfolder" value="{{$val.VirtualPath}}" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control" id="idVfolderName{{$idx}}" name="vfolder_name">
|
||||
<select class="form-control selectpicker" data-live-search="true" id="idVfolderName{{$idx}}" name="vfolder_name">
|
||||
<option value=""></option>
|
||||
{{range $.VirtualFolders}}
|
||||
<option value="{{.Name}}" {{if eq $val.Name .Name}}selected{{end}}>{{.Name}}</option>
|
||||
|
@ -189,7 +220,7 @@
|
|||
<input type="text" class="form-control" id="idVolderPath0" name="vfolder_path" placeholder="mount path, i.e. /vfolder" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control" id="idVfolderName0" name="vfolder_name">
|
||||
<select class="form-control selectpicker" data-live-search="true" id="idVfolderName0" name="vfolder_name">
|
||||
<option value=""></option>
|
||||
{{range .VirtualFolders}}
|
||||
<option value="{{.Name}}">{{.Name}}</option>
|
||||
|
@ -245,7 +276,7 @@
|
|||
<div class="form-group row">
|
||||
<label for="idStatus" class="col-sm-2 col-form-label">Status</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idStatus" name="status">
|
||||
<select class="form-control selectpicker" id="idStatus" name="status">
|
||||
<option value="1" {{if eq .User.Status 1 }}selected{{end}}>Active</option>
|
||||
<option value="0" {{if eq .User.Status 0 }}selected{{end}}>Inactive</option>
|
||||
</select>
|
||||
|
@ -322,7 +353,7 @@
|
|||
<div class="form-group row">
|
||||
<label for="idPermissions" class="col-sm-2 col-form-label">Permissions</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idPermissions" name="permissions" required multiple>
|
||||
<select class="form-control selectpicker" id="idPermissions" name="permissions" required multiple>
|
||||
{{range $validPerm := .ValidPerms}}
|
||||
<option value="{{$validPerm}}" {{range $perm :=$.RootDirPerms }}{{if eq $perm $validPerm}}selected{{end}}{{end}}>{{$validPerm}}</option>
|
||||
{{end}}
|
||||
|
@ -343,7 +374,7 @@
|
|||
<input type="text" class="form-control" id="idSubDirPermsPath{{$idx}}" name="sub_perm_path{{$idx}}" placeholder="directory path, i.e. /dir" value="{{$dirPerms.Path}}" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control" id="idSubDirPermissions{{$idx}}" name="sub_perm_permissions{{$idx}}" multiple>
|
||||
<select class="form-control selectpicker" id="idSubDirPermissions{{$idx}}" name="sub_perm_permissions{{$idx}}" multiple>
|
||||
{{range $validPerm := $.ValidPerms}}
|
||||
<option value="{{$validPerm}}" {{range $perm := $dirPerms.Permissions }}{{if eq $perm $validPerm}}selected{{end}}{{end}}>{{$validPerm}}</option>
|
||||
{{end}}
|
||||
|
@ -361,7 +392,7 @@
|
|||
<input type="text" class="form-control" id="idSubDirPermsPath0" name="sub_perm_path0" placeholder="directory path, i.e. /dir" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control" id="idSubDirPermissions0" name="sub_perm_permissions0" multiple>
|
||||
<select class="form-control selectpicker" id="idSubDirPermissions0" name="sub_perm_permissions0" multiple>
|
||||
{{range $validPerm := .ValidPerms}}
|
||||
<option value="{{$validPerm}}">{{$validPerm}}</option>
|
||||
{{end}}
|
||||
|
@ -394,7 +425,7 @@
|
|||
<p class="card-text">Denied entries are visible in directory listing by default, you can hide them by setting the "Hidden" policy, but please be aware that this may cause performance issues for large directories</p>
|
||||
<div class="form-group row">
|
||||
<div class="col-md-12 form_field_patterns_outer">
|
||||
{{range $idx, $pattern := .User.GetFlatFilePatterns -}}
|
||||
{{range $idx, $pattern := .User.Filters.GetFlatFilePatterns -}}
|
||||
<div class="row form_field_patterns_outer_row">
|
||||
<div class="form-group col-md-3">
|
||||
<input type="text" class="form-control" id="idPatternPath{{$idx}}" name="pattern_path{{$idx}}" placeholder="directory path, i.e. /dir" value="{{$pattern.Path}}" maxlength="255">
|
||||
|
@ -403,13 +434,13 @@
|
|||
<input type="text" class="form-control" id="idPatterns{{$idx}}" name="patterns{{$idx}}" placeholder="*.zip,?.txt" value="{{$pattern.GetCommaSeparatedPatterns}}" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<select class="form-control" id="idPatternType{{$idx}}" name="pattern_type{{$idx}}">
|
||||
<select class="form-control selectpicker" id="idPatternType{{$idx}}" name="pattern_type{{$idx}}">
|
||||
<option value="denied" {{if $pattern.IsDenied}}selected{{end}}>Denied</option>
|
||||
<option value="allowed" {{if $pattern.IsAllowed}}selected{{end}}>Allowed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<select class="form-control" id="idPatternPolicy{{$idx}}" name="pattern_policy{{$idx}}">
|
||||
<select class="form-control selectpicker" id="idPatternPolicy{{$idx}}" name="pattern_policy{{$idx}}">
|
||||
<option value="0" {{if eq $pattern.DenyPolicy 0}}selected{{end}}>Visible</option>
|
||||
<option value="1" {{if eq $pattern.DenyPolicy 1}}selected{{end}}>Hidden</option>
|
||||
</select>
|
||||
|
@ -429,13 +460,13 @@
|
|||
<input type="text" class="form-control" id="idPatterns0" name="patterns0" placeholder="*.zip,?.txt" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<select class="form-control" id="idPatternType0" name="pattern_type0">
|
||||
<select class="form-control selectpicker" id="idPatternType0" name="pattern_type0">
|
||||
<option value="denied">Denied</option>
|
||||
<option value="allowed">Allowed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<select class="form-control" id="idPatternPolicy0" name="pattern_policy0">
|
||||
<select class="form-control selectpicker" id="idPatternPolicy0" name="pattern_policy0">
|
||||
<option value="0">Visible</option>
|
||||
<option value="1">Hidden</option>
|
||||
</select>
|
||||
|
@ -472,7 +503,7 @@
|
|||
<div class="form-group row">
|
||||
<label for="idProtocols" class="col-sm-2 col-form-label">Denied protocols</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idProtocols" name="denied_protocols" multiple>
|
||||
<select class="form-control selectpicker" id="idProtocols" name="denied_protocols" multiple>
|
||||
{{range $protocol := .ValidProtocols}}
|
||||
<option value="{{$protocol}}" {{range $p :=$.User.Filters.DeniedProtocols }}{{if eq $p $protocol}}selected{{end}}{{end}}>{{$protocol}}
|
||||
</option>
|
||||
|
@ -484,7 +515,7 @@
|
|||
<div class="form-group row">
|
||||
<label for="idLoginMethods" class="col-sm-2 col-form-label">Denied login methods</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idLoginMethods" name="denied_login_methods" multiple aria-describedby="deniedLoginMethodsHelpBlock">
|
||||
<select class="form-control selectpicker" id="idLoginMethods" name="denied_login_methods" multiple aria-describedby="deniedLoginMethodsHelpBlock">
|
||||
{{range $method := .ValidLoginMethods}}
|
||||
<option value="{{$method}}" {{range $m :=$.User.Filters.DeniedLoginMethods }}{{if eq $m $method}}selected{{end}}{{end}}>{{$method}}
|
||||
</option>
|
||||
|
@ -499,7 +530,7 @@
|
|||
<div class="form-group row">
|
||||
<label for="idTwoFactorProtocols" class="col-sm-2 col-form-label">Require two-factor auth for</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idTwoFactorProtocols" name="required_two_factor_protocols" multiple>
|
||||
<select class="form-control selectpicker" id="idTwoFactorProtocols" name="required_two_factor_protocols" multiple>
|
||||
{{range $protocol := .TwoFactorProtocols}}
|
||||
<option value="{{$protocol}}" {{range $p :=$.User.Filters.TwoFactorAuthProtocols }}{{if eq $p $protocol}}selected{{end}}{{end}}>{{$protocol}}
|
||||
</option>
|
||||
|
@ -511,7 +542,7 @@
|
|||
<div class="form-group row">
|
||||
<label for="idWebClient" class="col-sm-2 col-form-label">Web client/REST API</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idWebClient" name="web_client_options" multiple>
|
||||
<select class="form-control selectpicker" id="idWebClient" name="web_client_options" multiple>
|
||||
{{range $option := .WebClientOptions}}
|
||||
<option value="{{$option}}" {{range $p :=$.User.Filters.WebClient }}{{if eq $p $option}}selected{{end}}{{end}}>{{$option}}
|
||||
</option>
|
||||
|
@ -852,7 +883,7 @@
|
|||
<div class="form-group row">
|
||||
<label for="idTLSUsername" class="col-sm-2 col-form-label">TLS username</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idTLSUsername" name="tls_username" aria-describedby="tlsUsernameHelpBlock">
|
||||
<select class="form-control selectpicker" id="idTLSUsername" name="tls_username" aria-describedby="tlsUsernameHelpBlock">
|
||||
<option value="None" {{if eq .User.Filters.TLSUsername "None" }}selected{{end}}>None</option>
|
||||
<option value="CommonName" {{if eq .User.Filters.TLSUsername "CommonName" }}selected{{end}}>Common Name</option>
|
||||
</select>
|
||||
|
@ -862,6 +893,23 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idHooks" class="col-sm-2 col-form-label">Hooks</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control selectpicker" id="idHooks" name="hooks" multiple>
|
||||
<option value="external_auth_disabled" {{if .User.Filters.Hooks.ExternalAuthDisabled}}selected{{end}}>
|
||||
External auth disabled
|
||||
</option>
|
||||
<option value="pre_login_disabled" {{if .User.Filters.Hooks.PreLoginDisabled}}selected{{end}}>
|
||||
Pre-login disabled
|
||||
</option>
|
||||
<option value="check_password_disabled" {{if .User.Filters.Hooks.CheckPasswordDisabled}}selected{{end}}>
|
||||
Check password disabled
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row {{if not .CanImpersonate}}d-none{{end}}">
|
||||
<label for="idUID" class="col-sm-2 col-form-label">UID</label>
|
||||
<div class="col-sm-3">
|
||||
|
@ -887,23 +935,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idHooks" class="col-sm-2 col-form-label">Hooks</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idHooks" name="hooks" multiple>
|
||||
<option value="external_auth_disabled" {{if .User.Filters.Hooks.ExternalAuthDisabled}}selected{{end}}>
|
||||
External auth disabled
|
||||
</option>
|
||||
<option value="pre_login_disabled" {{if .User.Filters.Hooks.PreLoginDisabled}}selected{{end}}>
|
||||
Pre-login disabled
|
||||
</option>
|
||||
<option value="check_password_disabled" {{if .User.Filters.Hooks.CheckPasswordDisabled}}selected{{end}}>
|
||||
Check password disabled
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row {{if not .User.HasExternalAuth}}d-none{{end}}">
|
||||
<label for="idExtAuthCacheTime" class="col-sm-2 col-form-label">External auth cache time</label>
|
||||
<div class="col-sm-10">
|
||||
|
@ -950,8 +981,10 @@
|
|||
{{define "extra_js"}}
|
||||
<script src="{{.StaticURL}}/vendor/moment/js/moment.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/tempusdominus/js/tempusdominus-bootstrap-4.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function () {
|
||||
$.fn.selectpicker.Constructor.BootstrapVersion = '4';
|
||||
{{if .Error}}
|
||||
$('#accordionUser .collapse').removeAttr("data-parent").collapse('show');
|
||||
{{end}}
|
||||
|
@ -987,273 +1020,62 @@
|
|||
});
|
||||
|
||||
onFilesystemChanged('{{.User.FsConfig.Provider.Name}}');
|
||||
|
||||
$("body").on("click", ".add_new_pk_field_btn", function () {
|
||||
var index = $(".form_field_pk_outer").find(".form_field_pk_outer_row").length;
|
||||
while (document.getElementById("idPublicKey"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_pk_outer").append(`
|
||||
<div class="row form_field_pk_outer_row">
|
||||
<div class="form-group col-md-11">
|
||||
<textarea class="form-control" id="idPublicKey${index}" name="public_keys" rows="3"
|
||||
placeholder="Paste your public key here"></textarea>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_pk_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_pk_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_pk_outer_row").remove();
|
||||
});
|
||||
|
||||
$("body").on("click", ".add_new_dirperms_field_btn", function () {
|
||||
var index = $(".form_field_dirperms_outer").find(".form_field_dirperms_outer_row").length;
|
||||
while (document.getElementById("idSubDirPermsPath"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_dirperms_outer").append(`
|
||||
<div class="row form_field_dirperms_outer_row">
|
||||
<div class="form-group col-md-8">
|
||||
<input type="text" class="form-control" id="idSubDirPermsPath${index}" name="sub_perm_path${index}" placeholder="directory path, i.e. /dir" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control" id="idSubDirPermissions${index}" name="sub_perm_permissions${index}" multiple>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_dirperms_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
{{range .ValidPerms}}
|
||||
$("#idSubDirPermissions"+index).append($('<option>').val('{{.}}').text('{{.}}'));
|
||||
{{end}}
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_dirperms_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_dirperms_outer_row").remove();
|
||||
});
|
||||
|
||||
$("body").on("click", ".add_new_vfolder_field_btn", function () {
|
||||
var index = $(".form_field_vfolders_outer").find(".form_field_vfolder_outer_row").length;
|
||||
while (document.getElementById("idVolderPath"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_vfolders_outer").append(`
|
||||
<div class="row form_field_vfolder_outer_row">
|
||||
<div class="form-group col-md-3">
|
||||
<input type="text" class="form-control" id="idVolderPath${index}" name="vfolder_path" placeholder="mount path, i.e. /vfolder" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control" id="idVfolderName${index}" name="vfolder_name">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<input type="number" class="form-control" id="idVfolderQuotaSize${index}" name="vfolder_quota_size"
|
||||
value="" min="-1" aria-describedby="vqsHelpBlock${index}">
|
||||
<small id="vqsHelpBlock${index}" class="form-text text-muted">
|
||||
Quota size (bytes)
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<input type="number" class="form-control" id="idVfolderQuotaFiles${index}" name="vfolder_quota_files"
|
||||
value="" min="-1" aria-describedby="vqfHelpBlock${index}">
|
||||
<small id="vqfHelpBlock${index}" class="form-text text-muted">
|
||||
Quota files
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_vfolder_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
{{range .VirtualFolders}}
|
||||
$("#idVfolderName"+index).append($('<option>').val('{{.Name}}').text('{{.Name}}'));
|
||||
{{end}}
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_vfolder_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_vfolder_outer_row").remove();
|
||||
});
|
||||
|
||||
$("body").on("click", ".add_new_bwlimit_field_btn", function () {
|
||||
var index = $(".form_field_bwlimits_outer").find(".form_field_bwlimits_outer_row").length;
|
||||
while (document.getElementById("idBandwidthLimitSources"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_bwlimits_outer").append(`
|
||||
<div class="row form_field_bwlimits_outer_row">
|
||||
<div class="form-group col-md-8">
|
||||
<textarea class="form-control" id="idBandwidthLimitSources0" name="bandwidth_limit_sources${index}" rows="4" placeholder=""
|
||||
aria-describedby="bwLimitSourcesHelpBlock${index}"></textarea>
|
||||
<small id="bwLimitSourcesHelpBlock${index}" class="form-text text-muted">
|
||||
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idUploadBandwidthSource${index}" name="upload_bandwidth_source${index}"
|
||||
placeholder="" value="" min="0" aria-describedby="ulHelpBlock${index}">
|
||||
<small id="ulHelpBlock${index}" class="form-text text-muted">
|
||||
UL (KB/s). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idDownloadBandwidthSource${index}" name="download_bandwidth_source${index}"
|
||||
placeholder="" value="" min="0" aria-describedby="dlHelpBlock${index}">
|
||||
<small id="dlHelpBlock${index}" class="form-text text-muted">
|
||||
DL (KB/s). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_bwlimit_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_bwlimit_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_bwlimits_outer_row").remove();
|
||||
});
|
||||
|
||||
$("body").on("click", ".add_new_dtlimit_field_btn", function () {
|
||||
var index = $(".form_field_dtlimits_outer").find(".form_field_dtlimits_outer_row").length;
|
||||
while (document.getElementById("idDataTransferLimitSources"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_dtlimits_outer").append(`
|
||||
<div class="row form_field_dtlimits_outer_row">
|
||||
<div class="form-group col-md-5">
|
||||
<textarea class="form-control" id="idDataTransferLimitSources${index}" name="data_transfer_limit_sources${index}" rows="4" placeholder=""
|
||||
aria-describedby="dtLimitSourcesHelpBlock${index}"></textarea>
|
||||
<small id="dtLimitSourcesHelpBlock${index}" class="form-text text-muted">
|
||||
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idUploadTransferSource${index}" name="upload_data_transfer_source${index}"
|
||||
placeholder="" value="" min="0" aria-describedby="ulDtHelpBlock${index}">
|
||||
<small id="ulDtHelpBlock${index}" class="form-text text-muted">
|
||||
UL (MB). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idDownloadTransferSource${index}" name="download_data_transfer_source${index}"
|
||||
placeholder="" value="" min="0" aria-describedby="dlDtHelpBlock${index}">
|
||||
<small id="dlDtHelpBlock${index}" class="form-text text-muted">
|
||||
DL (MB). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idTotalTransferSource${index}" name="total_data_transfer_source${index}"
|
||||
placeholder="" value="" min="0" aria-describedby="totalDtHelpBlock${index}">
|
||||
<small id="totalDtHelpBlock${index}" class="form-text text-muted">
|
||||
Total (MB). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_dtlimit_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_dtlimit_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_dtlimits_outer_row").remove();
|
||||
});
|
||||
|
||||
$("body").on("click", ".add_new_pattern_field_btn", function () {
|
||||
var index = $(".form_field_patterns_outer").find(".form_field_patterns_outer_row").length;
|
||||
while (document.getElementById("idPatternPath"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_patterns_outer").append(`
|
||||
<div class="row form_field_patterns_outer_row">
|
||||
<div class="form-group col-md-3">
|
||||
<input type="text" class="form-control" id="idPatternPath${index}" name="pattern_path${index}" placeholder="directory path, i.e. /dir" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<input type="text" class="form-control" id="idPatterns${index}" name="patterns${index}" placeholder="*.zip,?.txt" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<select class="form-control" id="idPatternType${index}" name="pattern_type${index}">
|
||||
<option value="denied">Denied</option>
|
||||
<option value="allowed">Allowed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<select class="form-control" id="idPatternPolicy${index}" name="pattern_policy${index}">
|
||||
<option value="0">Visible</option>
|
||||
<option value="1">Hidden</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_pattern_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_pattern_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_patterns_outer_row").remove();
|
||||
});
|
||||
|
||||
$("body").on("click", ".add_new_tpl_user_field_btn", function () {
|
||||
var index = $(".form_field_tpl_users_outer").find(".form_field_tpl_user_outer_row").length;
|
||||
while (document.getElementById("idTplUsername"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_tpl_users_outer").append(`
|
||||
<div class="row form_field_tpl_user_outer_row">
|
||||
<div class="form-group col-md-3">
|
||||
<input type="text" class="form-control" id="idTplUsername${index}" name="tpl_username" placeholder="Username" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<input type="password" class="form-control" id="idTplPassword${index}" name="tpl_password" placeholder="Password" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-5">
|
||||
<textarea class="form-control" id="idTplPublicKey${index}" name="tpl_public_keys" rows="5"
|
||||
placeholder="Paste your public key here"></textarea>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_tpl_user_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_tpl_user_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_tpl_user_outer_row").remove();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
{{template "fsjs"}}
|
||||
$("body").on("click", ".add_new_pk_field_btn", function () {
|
||||
var index = $(".form_field_pk_outer").find(".form_field_pk_outer_row").length;
|
||||
while (document.getElementById("idPublicKey"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_pk_outer").append(`
|
||||
<div class="row form_field_pk_outer_row">
|
||||
<div class="form-group col-md-11">
|
||||
<textarea class="form-control" id="idPublicKey${index}" name="public_keys" rows="3"
|
||||
placeholder="Paste your public key here"></textarea>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_pk_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_pk_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_pk_outer_row").remove();
|
||||
});
|
||||
|
||||
$("body").on("click", ".add_new_tpl_user_field_btn", function () {
|
||||
var index = $(".form_field_tpl_users_outer").find(".form_field_tpl_user_outer_row").length;
|
||||
while (document.getElementById("idTplUsername"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_tpl_users_outer").append(`
|
||||
<div class="row form_field_tpl_user_outer_row">
|
||||
<div class="form-group col-md-3">
|
||||
<input type="text" class="form-control" id="idTplUsername${index}" name="tpl_username" placeholder="Username" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<input type="password" class="form-control" id="idTplPassword${index}" name="tpl_password" placeholder="Password" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-5">
|
||||
<textarea class="form-control" id="idTplPublicKey${index}" name="tpl_public_keys" rows="5"
|
||||
placeholder="Paste your public key here"></textarea>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_tpl_user_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_tpl_user_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_tpl_user_outer_row").remove();
|
||||
});
|
||||
</script>
|
||||
{{template "fsjs"}}
|
||||
{{template "shared_user_group" .}}
|
||||
{{end}}
|
||||
|
|
11
util/util.go
11
util/util.go
|
@ -71,15 +71,16 @@ func RemoveDuplicates(obj []string) []string {
|
|||
if len(obj) == 0 {
|
||||
return obj
|
||||
}
|
||||
result := make([]string, 0, len(obj))
|
||||
seen := make(map[string]bool)
|
||||
validIdx := 0
|
||||
for _, item := range obj {
|
||||
if _, ok := seen[item]; !ok {
|
||||
result = append(result, item)
|
||||
if !seen[item] {
|
||||
seen[item] = true
|
||||
obj[validIdx] = item
|
||||
validIdx++
|
||||
}
|
||||
seen[item] = true
|
||||
}
|
||||
return result
|
||||
return obj[:validIdx]
|
||||
}
|
||||
|
||||
// GetTimeAsMsSinceEpoch returns unix timestamp as milliseconds from a time struct
|
||||
|
|
|
@ -26,6 +26,8 @@ type BaseVirtualFolder struct {
|
|||
LastQuotaUpdate int64 `json:"last_quota_update"`
|
||||
// list of usernames associated with this virtual folder
|
||||
Users []string `json:"users,omitempty"`
|
||||
// list of group names associated with this virtual folder
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
// Filesystem configuration details
|
||||
FsConfig Filesystem `json:"filesystem"`
|
||||
}
|
||||
|
@ -44,6 +46,8 @@ func (v *BaseVirtualFolder) GetGCSCredentialsFilePath() string {
|
|||
func (v *BaseVirtualFolder) GetACopy() BaseVirtualFolder {
|
||||
users := make([]string, len(v.Users))
|
||||
copy(users, v.Users)
|
||||
groups := make([]string, len(v.Groups))
|
||||
copy(groups, v.Groups)
|
||||
return BaseVirtualFolder{
|
||||
ID: v.ID,
|
||||
Name: v.Name,
|
||||
|
@ -53,6 +57,7 @@ func (v *BaseVirtualFolder) GetACopy() BaseVirtualFolder {
|
|||
UsedQuotaFiles: v.UsedQuotaFiles,
|
||||
LastQuotaUpdate: v.LastQuotaUpdate,
|
||||
Users: users,
|
||||
Groups: v.Groups,
|
||||
FsConfig: v.FsConfig.GetACopy(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1119,7 +1119,7 @@ func TestCachedUserWithFolders(t *testing.T) {
|
|||
folder, err := dataprovider.GetFolderByName(folderName)
|
||||
assert.NoError(t, err)
|
||||
// updating a used folder should invalidate the cache only if the fs changed
|
||||
err = dataprovider.UpdateFolder(&folder, folder.Users, "", "")
|
||||
err = dataprovider.UpdateFolder(&folder, folder.Users, folder.Groups, "", "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, isCached, _, loginMethod, err = server.authenticate(req, ipAddr)
|
||||
|
@ -1132,7 +1132,7 @@ func TestCachedUserWithFolders(t *testing.T) {
|
|||
}
|
||||
// changing the folder path should invalidate the cache
|
||||
folder.MappedPath = filepath.Join(os.TempDir(), "anotherpath")
|
||||
err = dataprovider.UpdateFolder(&folder, folder.Users, "", "")
|
||||
err = dataprovider.UpdateFolder(&folder, folder.Users, folder.Groups, "", "")
|
||||
assert.NoError(t, err)
|
||||
_, isCached, _, loginMethod, err = server.authenticate(req, ipAddr)
|
||||
assert.NoError(t, err)
|
||||
|
|
Loading…
Reference in a new issue