mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +00:00
refactor custom actions
SFTPGo is now fully auditable, all fs and provider events that change something are notified and can be collected using hooks/plugins. There are some backward incompatible changes for command hooks
This commit is contained in:
parent
64e87d64bd
commit
4aa9686e3b
48 changed files with 966 additions and 536 deletions
|
@ -116,7 +116,7 @@ Command-line flags should be specified in the Subsystem declaration.
|
||||||
if user.HomeDir != filepath.Clean(homedir) && !preserveHomeDir {
|
if user.HomeDir != filepath.Clean(homedir) && !preserveHomeDir {
|
||||||
// update the user
|
// update the user
|
||||||
user.HomeDir = filepath.Clean(homedir)
|
user.HomeDir = filepath.Clean(homedir)
|
||||||
err = dataprovider.UpdateUser(&user)
|
err = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSystem, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(logSender, connectionID, "unable to update user %#v: %v", username, err)
|
logger.Error(logSender, connectionID, "unable to update user %#v: %v", username, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
@ -133,7 +133,7 @@ Command-line flags should be specified in the Subsystem declaration.
|
||||||
user.Password = connectionID
|
user.Password = connectionID
|
||||||
user.Permissions = make(map[string][]string)
|
user.Permissions = make(map[string][]string)
|
||||||
user.Permissions["/"] = []string{dataprovider.PermAny}
|
user.Permissions["/"] = []string{dataprovider.PermAny}
|
||||||
err = dataprovider.AddUser(&user)
|
err = dataprovider.AddUser(&user, dataprovider.ActionExecutorSystem, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(logSender, connectionID, "unable to add user %#v: %v", username, err)
|
logger.Error(logSender, connectionID, "unable to add user %#v: %v", username, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
|
@ -51,8 +51,10 @@ func InitializeActionHandler(handler ActionHandler) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecutePreAction executes a pre-* action and returns the result
|
// ExecutePreAction executes a pre-* action and returns the result
|
||||||
func ExecutePreAction(user *dataprovider.User, operation, filePath, virtualPath, protocol string, fileSize int64, openFlags int) error {
|
func ExecutePreAction(user *dataprovider.User, operation, filePath, virtualPath, protocol, ip string, fileSize int64,
|
||||||
plugin.Handler.NotifyFsEvent(time.Now(), operation, user.Username, filePath, "", "", protocol, fileSize, nil)
|
openFlags int,
|
||||||
|
) error {
|
||||||
|
plugin.Handler.NotifyFsEvent(time.Now(), operation, user.Username, filePath, "", "", protocol, ip, virtualPath, "", fileSize, nil)
|
||||||
if !util.IsStringInSlice(operation, Config.Actions.ExecuteOn) {
|
if !util.IsStringInSlice(operation, Config.Actions.ExecuteOn) {
|
||||||
// for pre-delete we execute the internal handling on error, so we must return errUnconfiguredAction.
|
// for pre-delete we execute the internal handling on error, so we must return errUnconfiguredAction.
|
||||||
// Other pre action will deny the operation on error so if we have no configuration we must return
|
// Other pre action will deny the operation on error so if we have no configuration we must return
|
||||||
|
@ -62,14 +64,19 @@ func ExecutePreAction(user *dataprovider.User, operation, filePath, virtualPath,
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
notification := newActionNotification(user, operation, filePath, virtualPath, "", "", protocol, fileSize, openFlags, nil)
|
notification := newActionNotification(user, operation, filePath, virtualPath, "", "", "", protocol, ip, fileSize,
|
||||||
|
openFlags, nil)
|
||||||
return actionHandler.Handle(notification)
|
return actionHandler.Handle(notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteActionNotification executes the defined hook, if any, for the specified action
|
// ExecuteActionNotification executes the defined hook, if any, for the specified action
|
||||||
func ExecuteActionNotification(user *dataprovider.User, operation, filePath, virtualPath, target, sshCmd, protocol string, fileSize int64, err error) {
|
func ExecuteActionNotification(user *dataprovider.User, operation, filePath, virtualPath, target, virtualTarget, sshCmd,
|
||||||
plugin.Handler.NotifyFsEvent(time.Now(), operation, user.Username, filePath, target, sshCmd, protocol, fileSize, err)
|
protocol, ip string, fileSize int64, err error,
|
||||||
notification := newActionNotification(user, operation, filePath, virtualPath, target, sshCmd, protocol, fileSize, 0, err)
|
) {
|
||||||
|
plugin.Handler.NotifyFsEvent(time.Now(), operation, user.Username, filePath, target, sshCmd, protocol, ip, virtualPath,
|
||||||
|
virtualTarget, fileSize, err)
|
||||||
|
notification := newActionNotification(user, operation, filePath, virtualPath, target, virtualTarget, sshCmd, protocol,
|
||||||
|
ip, fileSize, 0, err)
|
||||||
|
|
||||||
if util.IsStringInSlice(operation, Config.Actions.ExecuteSync) {
|
if util.IsStringInSlice(operation, Config.Actions.ExecuteSync) {
|
||||||
actionHandler.Handle(notification) //nolint:errcheck
|
actionHandler.Handle(notification) //nolint:errcheck
|
||||||
|
@ -86,23 +93,27 @@ type ActionHandler interface {
|
||||||
|
|
||||||
// ActionNotification defines a notification for a Protocol Action.
|
// ActionNotification defines a notification for a Protocol Action.
|
||||||
type ActionNotification struct {
|
type ActionNotification struct {
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
TargetPath string `json:"target_path,omitempty"`
|
TargetPath string `json:"target_path,omitempty"`
|
||||||
SSHCmd string `json:"ssh_cmd,omitempty"`
|
VirtualPath string `json:"virtual_path"`
|
||||||
FileSize int64 `json:"file_size,omitempty"`
|
VirtualTargetPath string `json:"virtual_target_path,omitempty"`
|
||||||
FsProvider int `json:"fs_provider"`
|
SSHCmd string `json:"ssh_cmd,omitempty"`
|
||||||
Bucket string `json:"bucket,omitempty"`
|
FileSize int64 `json:"file_size,omitempty"`
|
||||||
Endpoint string `json:"endpoint,omitempty"`
|
FsProvider int `json:"fs_provider"`
|
||||||
Status int `json:"status"`
|
Bucket string `json:"bucket,omitempty"`
|
||||||
Protocol string `json:"protocol"`
|
Endpoint string `json:"endpoint,omitempty"`
|
||||||
OpenFlags int `json:"open_flags,omitempty"`
|
Status int `json:"status"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
OpenFlags int `json:"open_flags,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func newActionNotification(
|
func newActionNotification(
|
||||||
user *dataprovider.User,
|
user *dataprovider.User,
|
||||||
operation, filePath, virtualPath, target, sshCmd, protocol string,
|
operation, filePath, virtualPath, target, virtualTarget, sshCmd, protocol, ip string,
|
||||||
fileSize int64,
|
fileSize int64,
|
||||||
openFlags int,
|
openFlags int,
|
||||||
err error,
|
err error,
|
||||||
|
@ -134,18 +145,22 @@ func newActionNotification(
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ActionNotification{
|
return &ActionNotification{
|
||||||
Action: operation,
|
Action: operation,
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Path: filePath,
|
Path: filePath,
|
||||||
TargetPath: target,
|
TargetPath: target,
|
||||||
SSHCmd: sshCmd,
|
VirtualPath: virtualPath,
|
||||||
FileSize: fileSize,
|
VirtualTargetPath: virtualTarget,
|
||||||
FsProvider: int(fsConfig.Provider),
|
SSHCmd: sshCmd,
|
||||||
Bucket: bucket,
|
FileSize: fileSize,
|
||||||
Endpoint: endpoint,
|
FsProvider: int(fsConfig.Provider),
|
||||||
Status: status,
|
Bucket: bucket,
|
||||||
Protocol: protocol,
|
Endpoint: endpoint,
|
||||||
OpenFlags: openFlags,
|
Status: status,
|
||||||
|
Protocol: protocol,
|
||||||
|
IP: ip,
|
||||||
|
OpenFlags: openFlags,
|
||||||
|
Timestamp: util.GetTimeAsMsSinceEpoch(time.Now()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,14 +224,14 @@ func (h *defaultActionHandler) handleCommand(notification *ActionNotification) e
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, Config.Actions.Hook, notification.Action, notification.Username, notification.Path, notification.TargetPath, notification.SSHCmd)
|
cmd := exec.CommandContext(ctx, Config.Actions.Hook)
|
||||||
cmd.Env = append(os.Environ(), notificationAsEnvVars(notification)...)
|
cmd.Env = append(os.Environ(), notificationAsEnvVars(notification)...)
|
||||||
|
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
|
|
||||||
logger.Debug(notification.Protocol, "", "executed command %#v with arguments: %#v, %#v, %#v, %#v, %#v, elapsed: %v, error: %v",
|
logger.Debug(notification.Protocol, "", "executed command %#v, elapsed: %v, error: %v",
|
||||||
Config.Actions.Hook, notification.Action, notification.Username, notification.Path, notification.TargetPath, notification.SSHCmd, time.Since(startTime), err)
|
Config.Actions.Hook, time.Since(startTime), err)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -227,6 +242,8 @@ func notificationAsEnvVars(notification *ActionNotification) []string {
|
||||||
fmt.Sprintf("SFTPGO_ACTION_USERNAME=%v", notification.Username),
|
fmt.Sprintf("SFTPGO_ACTION_USERNAME=%v", notification.Username),
|
||||||
fmt.Sprintf("SFTPGO_ACTION_PATH=%v", notification.Path),
|
fmt.Sprintf("SFTPGO_ACTION_PATH=%v", notification.Path),
|
||||||
fmt.Sprintf("SFTPGO_ACTION_TARGET=%v", notification.TargetPath),
|
fmt.Sprintf("SFTPGO_ACTION_TARGET=%v", notification.TargetPath),
|
||||||
|
fmt.Sprintf("SFTPGO_ACTION_VIRTUAL_PATH=%v", notification.VirtualPath),
|
||||||
|
fmt.Sprintf("SFTPGO_ACTION_VIRTUAL_TARGET=%v", notification.VirtualTargetPath),
|
||||||
fmt.Sprintf("SFTPGO_ACTION_SSH_CMD=%v", notification.SSHCmd),
|
fmt.Sprintf("SFTPGO_ACTION_SSH_CMD=%v", notification.SSHCmd),
|
||||||
fmt.Sprintf("SFTPGO_ACTION_FILE_SIZE=%v", notification.FileSize),
|
fmt.Sprintf("SFTPGO_ACTION_FILE_SIZE=%v", notification.FileSize),
|
||||||
fmt.Sprintf("SFTPGO_ACTION_FS_PROVIDER=%v", notification.FsProvider),
|
fmt.Sprintf("SFTPGO_ACTION_FS_PROVIDER=%v", notification.FsProvider),
|
||||||
|
@ -234,6 +251,8 @@ func notificationAsEnvVars(notification *ActionNotification) []string {
|
||||||
fmt.Sprintf("SFTPGO_ACTION_ENDPOINT=%v", notification.Endpoint),
|
fmt.Sprintf("SFTPGO_ACTION_ENDPOINT=%v", notification.Endpoint),
|
||||||
fmt.Sprintf("SFTPGO_ACTION_STATUS=%v", notification.Status),
|
fmt.Sprintf("SFTPGO_ACTION_STATUS=%v", notification.Status),
|
||||||
fmt.Sprintf("SFTPGO_ACTION_PROTOCOL=%v", notification.Protocol),
|
fmt.Sprintf("SFTPGO_ACTION_PROTOCOL=%v", notification.Protocol),
|
||||||
|
fmt.Sprintf("SFTPGO_ACTION_IP=%v", notification.IP),
|
||||||
fmt.Sprintf("SFTPGO_ACTION_OPEN_FLAGS=%v", notification.OpenFlags),
|
fmt.Sprintf("SFTPGO_ACTION_OPEN_FLAGS=%v", notification.OpenFlags),
|
||||||
|
fmt.Sprintf("SFTPGO_ACTION_TIMESTAMP=%v", notification.Timestamp),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,38 +45,38 @@ func TestNewActionNotification(t *testing.T) {
|
||||||
Endpoint: "sftpendpoint",
|
Endpoint: "sftpendpoint",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, 0, errors.New("fake error"))
|
a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "", 123, 0, errors.New("fake error"))
|
||||||
assert.Equal(t, user.Username, a.Username)
|
assert.Equal(t, user.Username, a.Username)
|
||||||
assert.Equal(t, 0, len(a.Bucket))
|
assert.Equal(t, 0, len(a.Bucket))
|
||||||
assert.Equal(t, 0, len(a.Endpoint))
|
assert.Equal(t, 0, len(a.Endpoint))
|
||||||
assert.Equal(t, 0, a.Status)
|
assert.Equal(t, 0, a.Status)
|
||||||
|
|
||||||
user.FsConfig.Provider = sdk.S3FilesystemProvider
|
user.FsConfig.Provider = sdk.S3FilesystemProvider
|
||||||
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSSH, 123, 0, nil)
|
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSSH, "", 123, 0, nil)
|
||||||
assert.Equal(t, "s3bucket", a.Bucket)
|
assert.Equal(t, "s3bucket", a.Bucket)
|
||||||
assert.Equal(t, "endpoint", a.Endpoint)
|
assert.Equal(t, "endpoint", a.Endpoint)
|
||||||
assert.Equal(t, 1, a.Status)
|
assert.Equal(t, 1, a.Status)
|
||||||
|
|
||||||
user.FsConfig.Provider = sdk.GCSFilesystemProvider
|
user.FsConfig.Provider = sdk.GCSFilesystemProvider
|
||||||
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSCP, 123, 0, ErrQuotaExceeded)
|
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSCP, "", 123, 0, ErrQuotaExceeded)
|
||||||
assert.Equal(t, "gcsbucket", a.Bucket)
|
assert.Equal(t, "gcsbucket", a.Bucket)
|
||||||
assert.Equal(t, 0, len(a.Endpoint))
|
assert.Equal(t, 0, len(a.Endpoint))
|
||||||
assert.Equal(t, 2, a.Status)
|
assert.Equal(t, 2, a.Status)
|
||||||
|
|
||||||
user.FsConfig.Provider = sdk.AzureBlobFilesystemProvider
|
user.FsConfig.Provider = sdk.AzureBlobFilesystemProvider
|
||||||
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSCP, 123, 0, nil)
|
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSCP, "", 123, 0, nil)
|
||||||
assert.Equal(t, "azcontainer", a.Bucket)
|
assert.Equal(t, "azcontainer", a.Bucket)
|
||||||
assert.Equal(t, "azendpoint", a.Endpoint)
|
assert.Equal(t, "azendpoint", a.Endpoint)
|
||||||
assert.Equal(t, 1, a.Status)
|
assert.Equal(t, 1, a.Status)
|
||||||
|
|
||||||
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSCP, 123, os.O_APPEND, nil)
|
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSCP, "", 123, os.O_APPEND, nil)
|
||||||
assert.Equal(t, "azcontainer", a.Bucket)
|
assert.Equal(t, "azcontainer", a.Bucket)
|
||||||
assert.Equal(t, "azendpoint", a.Endpoint)
|
assert.Equal(t, "azendpoint", a.Endpoint)
|
||||||
assert.Equal(t, 1, a.Status)
|
assert.Equal(t, 1, a.Status)
|
||||||
assert.Equal(t, os.O_APPEND, a.OpenFlags)
|
assert.Equal(t, os.O_APPEND, a.OpenFlags)
|
||||||
|
|
||||||
user.FsConfig.Provider = sdk.SFTPFilesystemProvider
|
user.FsConfig.Provider = sdk.SFTPFilesystemProvider
|
||||||
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, 0, nil)
|
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "", 123, 0, nil)
|
||||||
assert.Equal(t, "sftpendpoint", a.Endpoint)
|
assert.Equal(t, "sftpendpoint", a.Endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ func TestActionHTTP(t *testing.T) {
|
||||||
Username: "username",
|
Username: "username",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, 0, nil)
|
a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "", 123, 0, nil)
|
||||||
err := actionHandler.Handle(a)
|
err := actionHandler.Handle(a)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
@ -127,11 +127,11 @@ func TestActionCMD(t *testing.T) {
|
||||||
Username: "username",
|
Username: "username",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, 0, nil)
|
a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "", 123, 0, nil)
|
||||||
err = actionHandler.Handle(a)
|
err = actionHandler.Handle(a)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
ExecuteActionNotification(user, OperationSSHCmd, "path", "vpath", "target", "sha1sum", ProtocolSSH, 0, nil)
|
ExecuteActionNotification(user, OperationSSHCmd, "path", "vpath", "target", "vtarget", "sha1sum", ProtocolSSH, "", 0, nil)
|
||||||
|
|
||||||
Config.Actions = actionsCopy
|
Config.Actions = actionsCopy
|
||||||
}
|
}
|
||||||
|
@ -153,7 +153,7 @@ func TestWrongActions(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
a := newActionNotification(user, operationUpload, "", "", "", "", ProtocolSFTP, 123, 0, nil)
|
a := newActionNotification(user, operationUpload, "", "", "", "", "", ProtocolSFTP, "", 123, 0, nil)
|
||||||
err := actionHandler.Handle(a)
|
err := actionHandler.Handle(a)
|
||||||
assert.Error(t, err, "action with bad command must fail")
|
assert.Error(t, err, "action with bad command must fail")
|
||||||
|
|
||||||
|
|
|
@ -84,6 +84,11 @@ func (c *BaseConnection) GetProtocol() string {
|
||||||
return c.protocol
|
return c.protocol
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRemoteIP returns the remote ip address
|
||||||
|
func (c *BaseConnection) GetRemoteIP() string {
|
||||||
|
return util.GetIPFromRemoteAddress(c.remoteAddr)
|
||||||
|
}
|
||||||
|
|
||||||
// SetProtocol sets the protocol for this connection
|
// SetProtocol sets the protocol for this connection
|
||||||
func (c *BaseConnection) SetProtocol(protocol string) {
|
func (c *BaseConnection) SetProtocol(protocol string) {
|
||||||
c.protocol = protocol
|
c.protocol = protocol
|
||||||
|
@ -248,7 +253,7 @@ func (c *BaseConnection) CreateDir(virtualPath string) error {
|
||||||
|
|
||||||
logger.CommandLog(mkdirLogSender, fsPath, "", c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "", -1,
|
logger.CommandLog(mkdirLogSender, fsPath, "", c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "", -1,
|
||||||
c.localAddr, c.remoteAddr)
|
c.localAddr, c.remoteAddr)
|
||||||
ExecuteActionNotification(&c.User, operationMkdir, fsPath, virtualPath, "", "", c.protocol, 0, nil)
|
ExecuteActionNotification(&c.User, operationMkdir, fsPath, virtualPath, "", "", "", c.protocol, c.GetRemoteIP(), 0, nil)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,7 +276,7 @@ func (c *BaseConnection) RemoveFile(fs vfs.Fs, fsPath, virtualPath string, info
|
||||||
}
|
}
|
||||||
|
|
||||||
size := info.Size()
|
size := info.Size()
|
||||||
actionErr := ExecutePreAction(&c.User, operationPreDelete, fsPath, virtualPath, c.protocol, size, 0)
|
actionErr := ExecutePreAction(&c.User, operationPreDelete, fsPath, virtualPath, c.protocol, c.GetRemoteIP(), size, 0)
|
||||||
if actionErr == nil {
|
if actionErr == nil {
|
||||||
c.Log(logger.LevelDebug, "remove for path %#v handled by pre-delete action", fsPath)
|
c.Log(logger.LevelDebug, "remove for path %#v handled by pre-delete action", fsPath)
|
||||||
} else {
|
} else {
|
||||||
|
@ -295,7 +300,7 @@ func (c *BaseConnection) RemoveFile(fs vfs.Fs, fsPath, virtualPath string, info
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if actionErr != nil {
|
if actionErr != nil {
|
||||||
ExecuteActionNotification(&c.User, operationDelete, fsPath, virtualPath, "", "", c.protocol, size, nil)
|
ExecuteActionNotification(&c.User, operationDelete, fsPath, virtualPath, "", "", "", c.protocol, c.GetRemoteIP(), size, nil)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -355,7 +360,7 @@ func (c *BaseConnection) RemoveDir(virtualPath string) error {
|
||||||
|
|
||||||
logger.CommandLog(rmdirLogSender, fsPath, "", c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "", -1,
|
logger.CommandLog(rmdirLogSender, fsPath, "", c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "", -1,
|
||||||
c.localAddr, c.remoteAddr)
|
c.localAddr, c.remoteAddr)
|
||||||
ExecuteActionNotification(&c.User, operationRmdir, fsPath, virtualPath, "", "", c.protocol, 0, nil)
|
ExecuteActionNotification(&c.User, operationRmdir, fsPath, virtualPath, "", "", "", c.protocol, c.GetRemoteIP(), 0, nil)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -416,7 +421,8 @@ func (c *BaseConnection) Rename(virtualSourcePath, virtualTargetPath string) err
|
||||||
c.updateQuotaAfterRename(fsDst, virtualSourcePath, virtualTargetPath, fsTargetPath, initialSize) //nolint:errcheck
|
c.updateQuotaAfterRename(fsDst, virtualSourcePath, virtualTargetPath, fsTargetPath, initialSize) //nolint:errcheck
|
||||||
logger.CommandLog(renameLogSender, fsSourcePath, fsTargetPath, c.User.Username, "", c.ID, c.protocol, -1, -1,
|
logger.CommandLog(renameLogSender, fsSourcePath, fsTargetPath, c.User.Username, "", c.ID, c.protocol, -1, -1,
|
||||||
"", "", "", -1, c.localAddr, c.remoteAddr)
|
"", "", "", -1, c.localAddr, c.remoteAddr)
|
||||||
ExecuteActionNotification(&c.User, operationRename, fsSourcePath, virtualSourcePath, fsTargetPath, "", c.protocol, 0, nil)
|
ExecuteActionNotification(&c.User, operationRename, fsSourcePath, virtualSourcePath, fsTargetPath, virtualTargetPath, "",
|
||||||
|
c.protocol, c.GetRemoteIP(), 0, nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2138,7 +2138,7 @@ func TestDelayedQuotaUpdater(t *testing.T) {
|
||||||
assert.Equal(t, 10, folderGet.UsedQuotaFiles)
|
assert.Equal(t, 10, folderGet.UsedQuotaFiles)
|
||||||
assert.Equal(t, int64(6000), folderGet.UsedQuotaSize)
|
assert.Equal(t, int64(6000), folderGet.UsedQuotaSize)
|
||||||
|
|
||||||
err = dataprovider.DeleteFolder(folder.Name)
|
err = dataprovider.DeleteFolder(folder.Name, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = dataprovider.Close()
|
err = dataprovider.Close()
|
||||||
|
@ -2605,7 +2605,7 @@ func TestBuiltinKeyboardInteractiveAuthentication(t *testing.T) {
|
||||||
Secret: kms.NewPlainSecret(secret),
|
Secret: kms.NewPlainSecret(secret),
|
||||||
Protocols: []string{common.ProtocolSSH},
|
Protocols: []string{common.ProtocolSSH},
|
||||||
}
|
}
|
||||||
err = dataprovider.UpdateUser(&user)
|
err = dataprovider.UpdateUser(&user, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
passcode, err := generateTOTPPasscode(secret, otp.AlgorithmSHA1)
|
passcode, err := generateTOTPPasscode(secret, otp.AlgorithmSHA1)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
@ -243,8 +243,8 @@ func (t *BaseTransfer) Close() error {
|
||||||
if t.transferType == TransferDownload {
|
if t.transferType == TransferDownload {
|
||||||
logger.TransferLog(downloadLogSender, t.fsPath, elapsed, atomic.LoadInt64(&t.BytesSent), t.Connection.User.Username,
|
logger.TransferLog(downloadLogSender, t.fsPath, elapsed, atomic.LoadInt64(&t.BytesSent), t.Connection.User.Username,
|
||||||
t.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode)
|
t.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode)
|
||||||
ExecuteActionNotification(&t.Connection.User, operationDownload, t.fsPath, t.requestPath, "", "", t.Connection.protocol,
|
ExecuteActionNotification(&t.Connection.User, operationDownload, t.fsPath, t.requestPath, "", "", "", t.Connection.protocol,
|
||||||
atomic.LoadInt64(&t.BytesSent), t.ErrTransfer)
|
t.Connection.GetRemoteIP(), atomic.LoadInt64(&t.BytesSent), t.ErrTransfer)
|
||||||
} else {
|
} else {
|
||||||
fileSize := atomic.LoadInt64(&t.BytesReceived) + t.MinWriteOffset
|
fileSize := atomic.LoadInt64(&t.BytesReceived) + t.MinWriteOffset
|
||||||
if statSize, err := t.getUploadFileSize(); err == nil {
|
if statSize, err := t.getUploadFileSize(); err == nil {
|
||||||
|
@ -254,8 +254,8 @@ func (t *BaseTransfer) Close() error {
|
||||||
t.updateQuota(numFiles, fileSize)
|
t.updateQuota(numFiles, fileSize)
|
||||||
logger.TransferLog(uploadLogSender, t.fsPath, elapsed, atomic.LoadInt64(&t.BytesReceived), t.Connection.User.Username,
|
logger.TransferLog(uploadLogSender, t.fsPath, elapsed, atomic.LoadInt64(&t.BytesReceived), t.Connection.User.Username,
|
||||||
t.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode)
|
t.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode)
|
||||||
ExecuteActionNotification(&t.Connection.User, operationUpload, t.fsPath, t.requestPath, "", "", t.Connection.protocol, fileSize,
|
ExecuteActionNotification(&t.Connection.User, operationUpload, t.fsPath, t.requestPath, "", "", "", t.Connection.protocol,
|
||||||
t.ErrTransfer)
|
t.Connection.GetRemoteIP(), fileSize, t.ErrTransfer)
|
||||||
}
|
}
|
||||||
if t.ErrTransfer != nil {
|
if t.ErrTransfer != nil {
|
||||||
t.Connection.Log(logger.LevelWarn, "transfer error: %v, path: %#v", t.ErrTransfer, t.fsPath)
|
t.Connection.Log(logger.LevelWarn, "transfer error: %v, path: %#v", t.ErrTransfer, t.fsPath)
|
||||||
|
|
|
@ -227,9 +227,10 @@ func Init() {
|
||||||
TrackQuota: 1,
|
TrackQuota: 1,
|
||||||
PoolSize: 0,
|
PoolSize: 0,
|
||||||
UsersBaseDir: "",
|
UsersBaseDir: "",
|
||||||
Actions: dataprovider.UserActions{
|
Actions: dataprovider.ObjectsActions{
|
||||||
ExecuteOn: []string{},
|
ExecuteOn: []string{},
|
||||||
Hook: "",
|
ExecuteFor: []string{},
|
||||||
|
Hook: "",
|
||||||
},
|
},
|
||||||
ExternalAuthHook: "",
|
ExternalAuthHook: "",
|
||||||
ExternalAuthScope: 0,
|
ExternalAuthScope: 0,
|
||||||
|
@ -671,29 +672,42 @@ func getRateLimitersFromEnv(idx int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPluginsFromEnv(idx int) {
|
func getKMSPluginFromEnv(idx int, pluginConfig *plugin.Config) bool {
|
||||||
pluginConfig := plugin.Config{}
|
|
||||||
if len(globalConf.PluginsConfig) > idx {
|
|
||||||
pluginConfig = globalConf.PluginsConfig[idx]
|
|
||||||
}
|
|
||||||
|
|
||||||
isSet := false
|
isSet := false
|
||||||
|
|
||||||
pluginType, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__TYPE", idx))
|
kmsScheme, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__KMS_OPTIONS__SCHEME", idx))
|
||||||
if ok {
|
if ok {
|
||||||
pluginConfig.Type = pluginType
|
pluginConfig.KMSOptions.Scheme = kmsScheme
|
||||||
isSet = true
|
isSet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kmsEncStatus, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__KMS_OPTIONS__ENCRYPTED_STATUS", idx))
|
||||||
|
if ok {
|
||||||
|
pluginConfig.KMSOptions.EncryptedStatus = kmsEncStatus
|
||||||
|
isSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return isSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNotifierPluginFromEnv(idx int, pluginConfig *plugin.Config) bool {
|
||||||
|
isSet := false
|
||||||
|
|
||||||
notifierFsEvents, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__FS_EVENTS", idx))
|
notifierFsEvents, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__FS_EVENTS", idx))
|
||||||
if ok {
|
if ok {
|
||||||
pluginConfig.NotifierOptions.FsEvents = notifierFsEvents
|
pluginConfig.NotifierOptions.FsEvents = notifierFsEvents
|
||||||
isSet = true
|
isSet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
notifierUserEvents, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__USER_EVENTS", idx))
|
notifierProviderEvents, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__PROVIDER_EVENTS", idx))
|
||||||
if ok {
|
if ok {
|
||||||
pluginConfig.NotifierOptions.UserEvents = notifierUserEvents
|
pluginConfig.NotifierOptions.ProviderEvents = notifierProviderEvents
|
||||||
|
isSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
notifierProviderObjects, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__PROVIDER_OBJECTS", idx))
|
||||||
|
if ok {
|
||||||
|
pluginConfig.NotifierOptions.ProviderObjects = notifierProviderObjects
|
||||||
isSet = true
|
isSet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -709,14 +723,29 @@ func getPluginsFromEnv(idx int) {
|
||||||
isSet = true
|
isSet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
kmsScheme, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__KMS_OPTIONS__SCHEME", idx))
|
return isSet
|
||||||
if ok {
|
}
|
||||||
pluginConfig.KMSOptions.Scheme = kmsScheme
|
|
||||||
|
func getPluginsFromEnv(idx int) {
|
||||||
|
pluginConfig := plugin.Config{}
|
||||||
|
if len(globalConf.PluginsConfig) > idx {
|
||||||
|
pluginConfig = globalConf.PluginsConfig[idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
kmsEncStatus, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__KMS_OPTIONS__ENCRYPTED_STATUS", idx))
|
isSet := false
|
||||||
|
|
||||||
|
pluginType, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__TYPE", idx))
|
||||||
if ok {
|
if ok {
|
||||||
pluginConfig.KMSOptions.EncryptedStatus = kmsEncStatus
|
pluginConfig.Type = pluginType
|
||||||
|
isSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if getNotifierPluginFromEnv(idx, &pluginConfig) {
|
||||||
|
isSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if getKMSPluginFromEnv(idx, &pluginConfig) {
|
||||||
|
isSet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__CMD", idx))
|
cmd, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__CMD", idx))
|
||||||
|
@ -1130,6 +1159,7 @@ func setViperDefaults() {
|
||||||
viper.SetDefault("data_provider.pool_size", globalConf.ProviderConf.PoolSize)
|
viper.SetDefault("data_provider.pool_size", globalConf.ProviderConf.PoolSize)
|
||||||
viper.SetDefault("data_provider.users_base_dir", globalConf.ProviderConf.UsersBaseDir)
|
viper.SetDefault("data_provider.users_base_dir", globalConf.ProviderConf.UsersBaseDir)
|
||||||
viper.SetDefault("data_provider.actions.execute_on", globalConf.ProviderConf.Actions.ExecuteOn)
|
viper.SetDefault("data_provider.actions.execute_on", globalConf.ProviderConf.Actions.ExecuteOn)
|
||||||
|
viper.SetDefault("data_provider.actions.execute_for", globalConf.ProviderConf.Actions.ExecuteFor)
|
||||||
viper.SetDefault("data_provider.actions.hook", globalConf.ProviderConf.Actions.Hook)
|
viper.SetDefault("data_provider.actions.hook", globalConf.ProviderConf.Actions.Hook)
|
||||||
viper.SetDefault("data_provider.external_auth_hook", globalConf.ProviderConf.ExternalAuthHook)
|
viper.SetDefault("data_provider.external_auth_hook", globalConf.ProviderConf.ExternalAuthHook)
|
||||||
viper.SetDefault("data_provider.external_auth_scope", globalConf.ProviderConf.ExternalAuthScope)
|
viper.SetDefault("data_provider.external_auth_scope", globalConf.ProviderConf.ExternalAuthScope)
|
||||||
|
|
|
@ -420,7 +420,8 @@ func TestPluginsFromEnv(t *testing.T) {
|
||||||
|
|
||||||
os.Setenv("SFTPGO_PLUGINS__0__TYPE", "notifier")
|
os.Setenv("SFTPGO_PLUGINS__0__TYPE", "notifier")
|
||||||
os.Setenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__FS_EVENTS", "upload,download")
|
os.Setenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__FS_EVENTS", "upload,download")
|
||||||
os.Setenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__USER_EVENTS", "add,update")
|
os.Setenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__PROVIDER_EVENTS", "add,update")
|
||||||
|
os.Setenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__PROVIDER_OBJECTS", "user,admin")
|
||||||
os.Setenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__RETRY_MAX_TIME", "2")
|
os.Setenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__RETRY_MAX_TIME", "2")
|
||||||
os.Setenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__RETRY_QUEUE_MAX_SIZE", "1000")
|
os.Setenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__RETRY_QUEUE_MAX_SIZE", "1000")
|
||||||
os.Setenv("SFTPGO_PLUGINS__0__CMD", "plugin_start_cmd")
|
os.Setenv("SFTPGO_PLUGINS__0__CMD", "plugin_start_cmd")
|
||||||
|
@ -432,7 +433,8 @@ func TestPluginsFromEnv(t *testing.T) {
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
os.Unsetenv("SFTPGO_PLUGINS__0__TYPE")
|
os.Unsetenv("SFTPGO_PLUGINS__0__TYPE")
|
||||||
os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__FS_EVENTS")
|
os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__FS_EVENTS")
|
||||||
os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__USER_EVENTS")
|
os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__PROVIDER_EVENTS")
|
||||||
|
os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__PROVIDER_OBJECTS")
|
||||||
os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__RETRY_MAX_TIME")
|
os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__RETRY_MAX_TIME")
|
||||||
os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__RETRY_QUEUE_MAX_SIZE")
|
os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__RETRY_QUEUE_MAX_SIZE")
|
||||||
os.Unsetenv("SFTPGO_PLUGINS__0__CMD")
|
os.Unsetenv("SFTPGO_PLUGINS__0__CMD")
|
||||||
|
@ -453,9 +455,12 @@ func TestPluginsFromEnv(t *testing.T) {
|
||||||
require.Len(t, pluginConf.NotifierOptions.FsEvents, 2)
|
require.Len(t, pluginConf.NotifierOptions.FsEvents, 2)
|
||||||
require.True(t, util.IsStringInSlice("upload", pluginConf.NotifierOptions.FsEvents))
|
require.True(t, util.IsStringInSlice("upload", pluginConf.NotifierOptions.FsEvents))
|
||||||
require.True(t, util.IsStringInSlice("download", pluginConf.NotifierOptions.FsEvents))
|
require.True(t, util.IsStringInSlice("download", pluginConf.NotifierOptions.FsEvents))
|
||||||
require.Len(t, pluginConf.NotifierOptions.UserEvents, 2)
|
require.Len(t, pluginConf.NotifierOptions.ProviderEvents, 2)
|
||||||
require.Equal(t, "add", pluginConf.NotifierOptions.UserEvents[0])
|
require.Equal(t, "add", pluginConf.NotifierOptions.ProviderEvents[0])
|
||||||
require.Equal(t, "update", pluginConf.NotifierOptions.UserEvents[1])
|
require.Equal(t, "update", pluginConf.NotifierOptions.ProviderEvents[1])
|
||||||
|
require.Len(t, pluginConf.NotifierOptions.ProviderObjects, 2)
|
||||||
|
require.Equal(t, "user", pluginConf.NotifierOptions.ProviderObjects[0])
|
||||||
|
require.Equal(t, "admin", pluginConf.NotifierOptions.ProviderObjects[1])
|
||||||
require.Equal(t, 2, pluginConf.NotifierOptions.RetryMaxTime)
|
require.Equal(t, 2, pluginConf.NotifierOptions.RetryMaxTime)
|
||||||
require.Equal(t, 1000, pluginConf.NotifierOptions.RetryQueueMaxSize)
|
require.Equal(t, 1000, pluginConf.NotifierOptions.RetryQueueMaxSize)
|
||||||
require.Equal(t, "plugin_start_cmd", pluginConf.Cmd)
|
require.Equal(t, "plugin_start_cmd", pluginConf.Cmd)
|
||||||
|
@ -488,9 +493,12 @@ func TestPluginsFromEnv(t *testing.T) {
|
||||||
require.Len(t, pluginConf.NotifierOptions.FsEvents, 2)
|
require.Len(t, pluginConf.NotifierOptions.FsEvents, 2)
|
||||||
require.True(t, util.IsStringInSlice("upload", pluginConf.NotifierOptions.FsEvents))
|
require.True(t, util.IsStringInSlice("upload", pluginConf.NotifierOptions.FsEvents))
|
||||||
require.True(t, util.IsStringInSlice("download", pluginConf.NotifierOptions.FsEvents))
|
require.True(t, util.IsStringInSlice("download", pluginConf.NotifierOptions.FsEvents))
|
||||||
require.Len(t, pluginConf.NotifierOptions.UserEvents, 2)
|
require.Len(t, pluginConf.NotifierOptions.ProviderEvents, 2)
|
||||||
require.Equal(t, "add", pluginConf.NotifierOptions.UserEvents[0])
|
require.Equal(t, "add", pluginConf.NotifierOptions.ProviderEvents[0])
|
||||||
require.Equal(t, "update", pluginConf.NotifierOptions.UserEvents[1])
|
require.Equal(t, "update", pluginConf.NotifierOptions.ProviderEvents[1])
|
||||||
|
require.Len(t, pluginConf.NotifierOptions.ProviderObjects, 2)
|
||||||
|
require.Equal(t, "user", pluginConf.NotifierOptions.ProviderObjects[0])
|
||||||
|
require.Equal(t, "admin", pluginConf.NotifierOptions.ProviderObjects[1])
|
||||||
require.Equal(t, 2, pluginConf.NotifierOptions.RetryMaxTime)
|
require.Equal(t, 2, pluginConf.NotifierOptions.RetryMaxTime)
|
||||||
require.Equal(t, 1000, pluginConf.NotifierOptions.RetryQueueMaxSize)
|
require.Equal(t, 1000, pluginConf.NotifierOptions.RetryQueueMaxSize)
|
||||||
require.Equal(t, "plugin_start_cmd1", pluginConf.Cmd)
|
require.Equal(t, "plugin_start_cmd1", pluginConf.Cmd)
|
||||||
|
|
105
dataprovider/actions.go
Normal file
105
dataprovider/actions.go
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
package dataprovider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/drakkan/sftpgo/v2/httpclient"
|
||||||
|
"github.com/drakkan/sftpgo/v2/logger"
|
||||||
|
"github.com/drakkan/sftpgo/v2/sdk/plugin"
|
||||||
|
"github.com/drakkan/sftpgo/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ActionExecutorSelf is used as username for self action, for example a user/admin that updates itself
|
||||||
|
ActionExecutorSelf = "__self__"
|
||||||
|
// ActionExecutorSystem is used as username for actions with no explicit executor associated, for example
|
||||||
|
// adding/updating a user/admin by loading initial data
|
||||||
|
ActionExecutorSystem = "__system__"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
actionObjectUser = "user"
|
||||||
|
actionObjectAdmin = "admin"
|
||||||
|
actionObjectAPIKey = "api_key"
|
||||||
|
)
|
||||||
|
|
||||||
|
func executeAction(operation, executor, ip, objectType, objectName string, object plugin.Renderer) {
|
||||||
|
plugin.Handler.NotifyProviderEvent(time.Now(), operation, executor, objectType, objectName, ip, object)
|
||||||
|
if config.Actions.Hook == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !util.IsStringInSlice(operation, config.Actions.ExecuteOn) ||
|
||||||
|
!util.IsStringInSlice(objectType, config.Actions.ExecuteFor) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
dataAsJSON, err := object.RenderAsJSON(operation != operationDelete)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelWarn, "unable to serialize user as JSON for operation %#v: %v", operation, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(config.Actions.Hook, "http") {
|
||||||
|
var url *url.URL
|
||||||
|
url, err := url.Parse(config.Actions.Hook)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelWarn, "Invalid http_notification_url %#v for operation %#v: %v", config.Actions.Hook, operation, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q := url.Query()
|
||||||
|
q.Add("action", operation)
|
||||||
|
q.Add("username", executor)
|
||||||
|
q.Add("ip", ip)
|
||||||
|
q.Add("object_type", objectType)
|
||||||
|
q.Add("object_name", objectName)
|
||||||
|
q.Add("timestamp", fmt.Sprintf("%v", util.GetTimeAsMsSinceEpoch(time.Now())))
|
||||||
|
url.RawQuery = q.Encode()
|
||||||
|
startTime := time.Now()
|
||||||
|
resp, err := httpclient.RetryablePost(url.String(), "application/json", bytes.NewBuffer(dataAsJSON))
|
||||||
|
respCode := 0
|
||||||
|
if err == nil {
|
||||||
|
respCode = resp.StatusCode
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
providerLog(logger.LevelDebug, "notified operation %#v to URL: %v status code: %v, elapsed: %v err: %v",
|
||||||
|
operation, url.Redacted(), respCode, time.Since(startTime), err)
|
||||||
|
} else {
|
||||||
|
executeNotificationCommand(operation, executor, ip, objectType, objectName, dataAsJSON) //nolint:errcheck // the error is used in test cases only
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeNotificationCommand(operation, executor, ip, objectType, objectName string, objectAsJSON []byte) error {
|
||||||
|
if !filepath.IsAbs(config.Actions.Hook) {
|
||||||
|
err := fmt.Errorf("invalid notification command %#v", config.Actions.Hook)
|
||||||
|
logger.Warn(logSender, "", "unable to execute notification command: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, config.Actions.Hook)
|
||||||
|
cmd.Env = append(os.Environ(),
|
||||||
|
fmt.Sprintf("SFTPGO_PROVIDER_ACTION=%v", operation),
|
||||||
|
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT_TYPE=%v", objectType),
|
||||||
|
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT_NAME=%v", objectName),
|
||||||
|
fmt.Sprintf("SFTPGO_PROVIDER_USERNAME=%v", executor),
|
||||||
|
fmt.Sprintf("SFTPGO_PROVIDER_IP=%v", ip),
|
||||||
|
fmt.Sprintf("SFTPGO_PROVIDER_TIMESTAMP=%v", util.GetTimeAsMsSinceEpoch(time.Now())),
|
||||||
|
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT=%v", string(objectAsJSON)))
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
err := cmd.Run()
|
||||||
|
providerLog(logger.LevelDebug, "executed command %#v, elapsed: %v, error: %v", config.Actions.Hook,
|
||||||
|
time.Since(startTime), err)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package dataprovider
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
@ -15,6 +16,7 @@ import (
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/v2/kms"
|
"github.com/drakkan/sftpgo/v2/kms"
|
||||||
|
"github.com/drakkan/sftpgo/v2/logger"
|
||||||
"github.com/drakkan/sftpgo/v2/mfa"
|
"github.com/drakkan/sftpgo/v2/mfa"
|
||||||
"github.com/drakkan/sftpgo/v2/sdk"
|
"github.com/drakkan/sftpgo/v2/sdk"
|
||||||
"github.com/drakkan/sftpgo/v2/util"
|
"github.com/drakkan/sftpgo/v2/util"
|
||||||
|
@ -288,6 +290,21 @@ func (a *Admin) checkUserAndPass(password, ip string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RenderAsJSON implements the renderer interface used within plugins
|
||||||
|
func (a *Admin) RenderAsJSON(reload bool) ([]byte, error) {
|
||||||
|
if reload {
|
||||||
|
admin, err := provider.adminExists(a.Username)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelWarn, "unable to reload admin before rendering as json: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
admin.HideConfidentialData()
|
||||||
|
return json.Marshal(admin)
|
||||||
|
}
|
||||||
|
a.HideConfidentialData()
|
||||||
|
return json.Marshal(a)
|
||||||
|
}
|
||||||
|
|
||||||
// HideConfidentialData hides admin confidential data
|
// HideConfidentialData hides admin confidential data
|
||||||
func (a *Admin) HideConfidentialData() {
|
func (a *Admin) HideConfidentialData() {
|
||||||
a.Password = ""
|
a.Password = ""
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package dataprovider
|
package dataprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -9,6 +10,7 @@ import (
|
||||||
"github.com/lithammer/shortuuid/v3"
|
"github.com/lithammer/shortuuid/v3"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
"github.com/drakkan/sftpgo/v2/logger"
|
||||||
"github.com/drakkan/sftpgo/v2/util"
|
"github.com/drakkan/sftpgo/v2/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -76,6 +78,21 @@ func (k *APIKey) getACopy() APIKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RenderAsJSON implements the renderer interface used within plugins
|
||||||
|
func (k *APIKey) RenderAsJSON(reload bool) ([]byte, error) {
|
||||||
|
if reload {
|
||||||
|
apiKey, err := provider.apiKeyExists(k.KeyID)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelWarn, "unable to reload api key before rendering as json: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
apiKey.HideConfidentialData()
|
||||||
|
return json.Marshal(apiKey)
|
||||||
|
}
|
||||||
|
k.HideConfidentialData()
|
||||||
|
return json.Marshal(k)
|
||||||
|
}
|
||||||
|
|
||||||
// HideConfidentialData hides admin confidential data
|
// HideConfidentialData hides admin confidential data
|
||||||
func (k *APIKey) HideConfidentialData() {
|
func (k *APIKey) HideConfidentialData() {
|
||||||
k.Key = ""
|
k.Key = ""
|
||||||
|
|
|
@ -204,10 +204,12 @@ type PasswordValidation struct {
|
||||||
Users PasswordValidationRules `json:"users" mapstructure:"users"`
|
Users PasswordValidationRules `json:"users" mapstructure:"users"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserActions defines the action to execute on user create, update, delete.
|
// ObjectsActions defines the action to execute on user create, update, delete for the specified objects
|
||||||
type UserActions struct {
|
type ObjectsActions struct {
|
||||||
// Valid values are add, update, delete. Empty slice to disable
|
// Valid values are add, update, delete. Empty slice to disable
|
||||||
ExecuteOn []string `json:"execute_on" mapstructure:"execute_on"`
|
ExecuteOn []string `json:"execute_on" mapstructure:"execute_on"`
|
||||||
|
// Valid values are user, admin, api_key
|
||||||
|
ExecuteFor []string `json:"execute_for" mapstructure:"execute_for"`
|
||||||
// Absolute path to an external program or an HTTP URL
|
// Absolute path to an external program or an HTTP URL
|
||||||
Hook string `json:"hook" mapstructure:"hook"`
|
Hook string `json:"hook" mapstructure:"hook"`
|
||||||
}
|
}
|
||||||
|
@ -261,9 +263,10 @@ type Config struct {
|
||||||
// a valid absolute path, then the user home dir will be automatically
|
// a valid absolute path, then the user home dir will be automatically
|
||||||
// defined as the path obtained joining the base dir and the username
|
// defined as the path obtained joining the base dir and the username
|
||||||
UsersBaseDir string `json:"users_base_dir" mapstructure:"users_base_dir"`
|
UsersBaseDir string `json:"users_base_dir" mapstructure:"users_base_dir"`
|
||||||
// Actions to execute on user add, update, delete.
|
// Actions to execute on objects add, update, delete.
|
||||||
|
// The supported objects are user, admin, api_key.
|
||||||
// Update action will not be fired for internal updates such as the last login or the user quota fields.
|
// Update action will not be fired for internal updates such as the last login or the user quota fields.
|
||||||
Actions UserActions `json:"actions" mapstructure:"actions"`
|
Actions ObjectsActions `json:"actions" mapstructure:"actions"`
|
||||||
// Absolute path to an external program or an HTTP URL to invoke for users authentication.
|
// Absolute path to an external program or an HTTP URL to invoke for users authentication.
|
||||||
// Leave empty to use builtin authentication.
|
// Leave empty to use builtin authentication.
|
||||||
// If the authentication succeed the user will be automatically added/updated inside the defined data provider.
|
// If the authentication succeed the user will be automatically added/updated inside the defined data provider.
|
||||||
|
@ -926,22 +929,34 @@ func GetUsedVirtualFolderQuota(name string) (int, int64, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddAPIKey adds a new API key
|
// AddAPIKey adds a new API key
|
||||||
func AddAPIKey(apiKey *APIKey) error {
|
func AddAPIKey(apiKey *APIKey, executor, ipAddress string) error {
|
||||||
return provider.addAPIKey(apiKey)
|
err := provider.addAPIKey(apiKey)
|
||||||
|
if err == nil {
|
||||||
|
executeAction(operationAdd, executor, ipAddress, actionObjectAPIKey, apiKey.KeyID, apiKey)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAPIKey updates an existing API key
|
// UpdateAPIKey updates an existing API key
|
||||||
func UpdateAPIKey(apiKey *APIKey) error {
|
func UpdateAPIKey(apiKey *APIKey, executor, ipAddress string) error {
|
||||||
return provider.updateAPIKey(apiKey)
|
err := provider.updateAPIKey(apiKey)
|
||||||
|
if err == nil {
|
||||||
|
executeAction(operationUpdate, executor, ipAddress, actionObjectAPIKey, apiKey.KeyID, apiKey)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteAPIKey deletes an existing API key
|
// DeleteAPIKey deletes an existing API key
|
||||||
func DeleteAPIKey(keyID string) error {
|
func DeleteAPIKey(keyID string, executor, ipAddress string) error {
|
||||||
apiKey, err := provider.apiKeyExists(keyID)
|
apiKey, err := provider.apiKeyExists(keyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return provider.deleteAPIKeys(&apiKey)
|
err = provider.deleteAPIKeys(&apiKey)
|
||||||
|
if err == nil {
|
||||||
|
executeAction(operationDelete, executor, ipAddress, actionObjectAPIKey, apiKey.KeyID, &apiKey)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// APIKeyExists returns the API key with the given ID if it exists
|
// APIKeyExists returns the API key with the given ID if it exists
|
||||||
|
@ -959,7 +974,7 @@ func HasAdmin() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddAdmin adds a new SFTPGo admin
|
// AddAdmin adds a new SFTPGo admin
|
||||||
func AddAdmin(admin *Admin) error {
|
func AddAdmin(admin *Admin, executor, ipAddress string) error {
|
||||||
admin.Filters.RecoveryCodes = nil
|
admin.Filters.RecoveryCodes = nil
|
||||||
admin.Filters.TOTPConfig = TOTPConfig{
|
admin.Filters.TOTPConfig = TOTPConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
|
@ -967,22 +982,31 @@ func AddAdmin(admin *Admin) error {
|
||||||
err := provider.addAdmin(admin)
|
err := provider.addAdmin(admin)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
atomic.StoreInt32(&isAdminCreated, 1)
|
atomic.StoreInt32(&isAdminCreated, 1)
|
||||||
|
executeAction(operationAdd, executor, ipAddress, actionObjectAdmin, admin.Username, admin)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAdmin updates an existing SFTPGo admin
|
// UpdateAdmin updates an existing SFTPGo admin
|
||||||
func UpdateAdmin(admin *Admin) error {
|
func UpdateAdmin(admin *Admin, executor, ipAddress string) error {
|
||||||
return provider.updateAdmin(admin)
|
err := provider.updateAdmin(admin)
|
||||||
|
if err == nil {
|
||||||
|
executeAction(operationUpdate, executor, ipAddress, actionObjectAdmin, admin.Username, admin)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteAdmin deletes an existing SFTPGo admin
|
// DeleteAdmin deletes an existing SFTPGo admin
|
||||||
func DeleteAdmin(username string) error {
|
func DeleteAdmin(username, executor, ipAddress string) error {
|
||||||
admin, err := provider.adminExists(username)
|
admin, err := provider.adminExists(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return provider.deleteAdmin(&admin)
|
err = provider.deleteAdmin(&admin)
|
||||||
|
if err == nil {
|
||||||
|
executeAction(operationDelete, executor, ipAddress, actionObjectAdmin, admin.Username, &admin)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminExists returns the admin with the given username if it exists
|
// AdminExists returns the admin with the given username if it exists
|
||||||
|
@ -996,31 +1020,31 @@ func UserExists(username string) (User, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddUser adds a new SFTPGo user.
|
// AddUser adds a new SFTPGo user.
|
||||||
func AddUser(user *User) error {
|
func AddUser(user *User, executor, ipAddress string) error {
|
||||||
user.Filters.RecoveryCodes = nil
|
user.Filters.RecoveryCodes = nil
|
||||||
user.Filters.TOTPConfig = sdk.TOTPConfig{
|
user.Filters.TOTPConfig = sdk.TOTPConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
}
|
}
|
||||||
err := provider.addUser(user)
|
err := provider.addUser(user)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
executeAction(operationAdd, user)
|
executeAction(operationAdd, executor, ipAddress, actionObjectUser, user.Username, user)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUser updates an existing SFTPGo user.
|
// UpdateUser updates an existing SFTPGo user.
|
||||||
func UpdateUser(user *User) error {
|
func UpdateUser(user *User, executor, ipAddress string) error {
|
||||||
err := provider.updateUser(user)
|
err := provider.updateUser(user)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
webDAVUsersCache.swap(user)
|
webDAVUsersCache.swap(user)
|
||||||
cachedPasswords.Remove(user.Username)
|
cachedPasswords.Remove(user.Username)
|
||||||
executeAction(operationUpdate, user)
|
executeAction(operationUpdate, executor, ipAddress, actionObjectUser, user.Username, user)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteUser deletes an existing SFTPGo user.
|
// DeleteUser deletes an existing SFTPGo user.
|
||||||
func DeleteUser(username string) error {
|
func DeleteUser(username, executor, ipAddress string) error {
|
||||||
user, err := provider.userExists(username)
|
user, err := provider.userExists(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -1030,7 +1054,7 @@ func DeleteUser(username string) error {
|
||||||
RemoveCachedWebDAVUser(user.Username)
|
RemoveCachedWebDAVUser(user.Username)
|
||||||
delayedQuotaUpdater.resetUserQuota(username)
|
delayedQuotaUpdater.resetUserQuota(username)
|
||||||
cachedPasswords.Remove(username)
|
cachedPasswords.Remove(username)
|
||||||
executeAction(operationDelete, &user)
|
executeAction(operationDelete, executor, ipAddress, actionObjectUser, user.Username, &user)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1063,7 +1087,7 @@ func AddFolder(folder *vfs.BaseVirtualFolder) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateFolder updates the specified virtual folder
|
// UpdateFolder updates the specified virtual folder
|
||||||
func UpdateFolder(folder *vfs.BaseVirtualFolder, users []string) error {
|
func UpdateFolder(folder *vfs.BaseVirtualFolder, users []string, executor, ipAddress string) error {
|
||||||
err := provider.updateFolder(folder)
|
err := provider.updateFolder(folder)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
|
@ -1071,7 +1095,7 @@ func UpdateFolder(folder *vfs.BaseVirtualFolder, users []string) error {
|
||||||
u, err := provider.userExists(user)
|
u, err := provider.userExists(user)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
webDAVUsersCache.swap(&u)
|
webDAVUsersCache.swap(&u)
|
||||||
executeAction(operationUpdate, &u)
|
executeAction(operationUpdate, executor, ipAddress, actionObjectUser, u.Username, &u)
|
||||||
} else {
|
} else {
|
||||||
RemoveCachedWebDAVUser(user)
|
RemoveCachedWebDAVUser(user)
|
||||||
}
|
}
|
||||||
|
@ -1081,7 +1105,7 @@ func UpdateFolder(folder *vfs.BaseVirtualFolder, users []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteFolder deletes an existing folder.
|
// DeleteFolder deletes an existing folder.
|
||||||
func DeleteFolder(folderName string) error {
|
func DeleteFolder(folderName, executor, ipAddress string) error {
|
||||||
folder, err := provider.getFolderByName(folderName)
|
folder, err := provider.getFolderByName(folderName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -1090,6 +1114,10 @@ func DeleteFolder(folderName string) error {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, user := range folder.Users {
|
for _, user := range folder.Users {
|
||||||
provider.setUpdatedAt(user)
|
provider.setUpdatedAt(user)
|
||||||
|
u, err := provider.userExists(user)
|
||||||
|
if err == nil {
|
||||||
|
executeAction(operationUpdate, executor, ipAddress, actionObjectUser, u.Username, &u)
|
||||||
|
}
|
||||||
RemoveCachedWebDAVUser(user)
|
RemoveCachedWebDAVUser(user)
|
||||||
}
|
}
|
||||||
delayedQuotaUpdater.resetFolderQuota(folderName)
|
delayedQuotaUpdater.resetFolderQuota(folderName)
|
||||||
|
@ -2809,66 +2837,3 @@ func getUserAndJSONForHook(username string) (User, []byte, error) {
|
||||||
func providerLog(level logger.LogLevel, format string, v ...interface{}) {
|
func providerLog(level logger.LogLevel, format string, v ...interface{}) {
|
||||||
logger.Log(level, logSender, "", format, v...)
|
logger.Log(level, logSender, "", format, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeNotificationCommand(operation string, commandArgs []string, userAsJSON []byte) error {
|
|
||||||
if !filepath.IsAbs(config.Actions.Hook) {
|
|
||||||
err := fmt.Errorf("invalid notification command %#v", config.Actions.Hook)
|
|
||||||
logger.Warn(logSender, "", "unable to execute notification command: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, config.Actions.Hook, commandArgs...)
|
|
||||||
cmd.Env = append(os.Environ(),
|
|
||||||
fmt.Sprintf("SFTPGO_USER_ACTION=%v", operation),
|
|
||||||
fmt.Sprintf("SFTPGO_USER=%v", string(userAsJSON)))
|
|
||||||
|
|
||||||
startTime := time.Now()
|
|
||||||
err := cmd.Run()
|
|
||||||
providerLog(logger.LevelDebug, "executed command %#v with arguments: %+v, elapsed: %v, error: %v",
|
|
||||||
config.Actions.Hook, commandArgs, time.Since(startTime), err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func executeAction(operation string, user *User) {
|
|
||||||
plugin.Handler.NotifyUserEvent(time.Now(), operation, user)
|
|
||||||
if !util.IsStringInSlice(operation, config.Actions.ExecuteOn) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if config.Actions.Hook == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
user.PrepareForRendering()
|
|
||||||
userAsJSON, err := user.RenderAsJSON(operation != operationDelete)
|
|
||||||
if err != nil {
|
|
||||||
providerLog(logger.LevelWarn, "unable to serialize user as JSON for operation %#v: %v", operation, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(config.Actions.Hook, "http") {
|
|
||||||
var url *url.URL
|
|
||||||
url, err := url.Parse(config.Actions.Hook)
|
|
||||||
if err != nil {
|
|
||||||
providerLog(logger.LevelWarn, "Invalid http_notification_url %#v for operation %#v: %v", config.Actions.Hook, operation, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
q := url.Query()
|
|
||||||
q.Add("action", operation)
|
|
||||||
url.RawQuery = q.Encode()
|
|
||||||
startTime := time.Now()
|
|
||||||
resp, err := httpclient.RetryablePost(url.String(), "application/json", bytes.NewBuffer(userAsJSON))
|
|
||||||
respCode := 0
|
|
||||||
if err == nil {
|
|
||||||
respCode = resp.StatusCode
|
|
||||||
resp.Body.Close()
|
|
||||||
}
|
|
||||||
providerLog(logger.LevelDebug, "notified operation %#v to URL: %v status code: %v, elapsed: %v err: %v",
|
|
||||||
operation, url.Redacted(), respCode, time.Since(startTime), err)
|
|
||||||
} else {
|
|
||||||
executeNotificationCommand(operation, user.getNotificationFieldsAsSlice(operation), userAsJSON) //nolint:errcheck // the error is used in test cases only
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
|
@ -1104,13 +1104,13 @@ func (p *MemoryProvider) restoreAPIKeys(dump *BackupData) error {
|
||||||
apiKey := apiKey // pin
|
apiKey := apiKey // pin
|
||||||
if err == nil {
|
if err == nil {
|
||||||
apiKey.ID = k.ID
|
apiKey.ID = k.ID
|
||||||
err = UpdateAPIKey(&apiKey)
|
err = UpdateAPIKey(&apiKey, ActionExecutorSystem, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelWarn, "error updating API key %#v: %v", apiKey.KeyID, err)
|
providerLog(logger.LevelWarn, "error updating API key %#v: %v", apiKey.KeyID, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err = AddAPIKey(&apiKey)
|
err = AddAPIKey(&apiKey, ActionExecutorSystem, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelWarn, "error adding API key %#v: %v", apiKey.KeyID, err)
|
providerLog(logger.LevelWarn, "error adding API key %#v: %v", apiKey.KeyID, err)
|
||||||
return err
|
return err
|
||||||
|
@ -1126,13 +1126,13 @@ func (p *MemoryProvider) restoreAdmins(dump *BackupData) error {
|
||||||
admin := admin // pin
|
admin := admin // pin
|
||||||
if err == nil {
|
if err == nil {
|
||||||
admin.ID = a.ID
|
admin.ID = a.ID
|
||||||
err = UpdateAdmin(&admin)
|
err = UpdateAdmin(&admin, ActionExecutorSystem, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelWarn, "error updating admin %#v: %v", admin.Username, err)
|
providerLog(logger.LevelWarn, "error updating admin %#v: %v", admin.Username, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err = AddAdmin(&admin)
|
err = AddAdmin(&admin, ActionExecutorSystem, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelWarn, "error adding admin %#v: %v", admin.Username, err)
|
providerLog(logger.LevelWarn, "error adding admin %#v: %v", admin.Username, err)
|
||||||
return err
|
return err
|
||||||
|
@ -1148,7 +1148,7 @@ func (p *MemoryProvider) restoreFolders(dump *BackupData) error {
|
||||||
f, err := p.getFolderByName(folder.Name)
|
f, err := p.getFolderByName(folder.Name)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
folder.ID = f.ID
|
folder.ID = f.ID
|
||||||
err = UpdateFolder(&folder, f.Users)
|
err = UpdateFolder(&folder, f.Users, ActionExecutorSystem, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelWarn, "error updating folder %#v: %v", folder.Name, err)
|
providerLog(logger.LevelWarn, "error updating folder %#v: %v", folder.Name, err)
|
||||||
return err
|
return err
|
||||||
|
@ -1171,13 +1171,13 @@ func (p *MemoryProvider) restoreUsers(dump *BackupData) error {
|
||||||
u, err := p.userExists(user.Username)
|
u, err := p.userExists(user.Username)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
user.ID = u.ID
|
user.ID = u.ID
|
||||||
err = UpdateUser(&user)
|
err = UpdateUser(&user, ActionExecutorSystem, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelWarn, "error updating user %#v: %v", user.Username, err)
|
providerLog(logger.LevelWarn, "error updating user %#v: %v", user.Username, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err = AddUser(&user)
|
err = AddUser(&user, ActionExecutorSystem, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelWarn, "error adding user %#v: %v", user.Username, err)
|
providerLog(logger.LevelWarn, "error adding user %#v: %v", user.Username, err)
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -249,6 +249,7 @@ func (u *User) PrepareForRendering() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasRedactedSecret returns true if the user has a redacted secret
|
||||||
func (u *User) hasRedactedSecret() bool {
|
func (u *User) hasRedactedSecret() bool {
|
||||||
if u.FsConfig.HasRedactedSecret() {
|
if u.FsConfig.HasRedactedSecret() {
|
||||||
return true
|
return true
|
||||||
|
@ -1109,17 +1110,6 @@ func (u *User) getACopy() User {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) getNotificationFieldsAsSlice(action string) []string {
|
|
||||||
return []string{action, u.Username,
|
|
||||||
strconv.FormatInt(u.ID, 10),
|
|
||||||
strconv.FormatInt(int64(u.Status), 10),
|
|
||||||
strconv.FormatInt(u.ExpirationDate, 10),
|
|
||||||
u.HomeDir,
|
|
||||||
strconv.FormatInt(int64(u.UID), 10),
|
|
||||||
strconv.FormatInt(int64(u.GID), 10),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEncryptionAdditionalData returns the additional data to use for AEAD
|
// GetEncryptionAdditionalData returns the additional data to use for AEAD
|
||||||
func (u *User) GetEncryptionAdditionalData() string {
|
func (u *User) GetEncryptionAdditionalData() string {
|
||||||
return u.Username
|
return u.Username
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
# Custom Actions
|
# Custom Actions
|
||||||
|
|
||||||
|
SFTPGo can notify filesystem and provider events using custom actions. A custom action can be an external program or an HTTP URL.
|
||||||
|
|
||||||
|
## Filesystem events
|
||||||
|
|
||||||
The `actions` struct inside the `common` configuration section allows to configure the actions for file operations and SSH commands.
|
The `actions` struct inside the `common` configuration section allows to configure the actions for file operations and SSH commands.
|
||||||
The `hook` can be defined as the absolute path of your program or an HTTP URL.
|
The `hook` can be defined as the absolute path of your program or an HTTP URL.
|
||||||
|
|
||||||
|
@ -16,7 +20,7 @@ The following `actions` are supported:
|
||||||
- `rmdir`
|
- `rmdir`
|
||||||
- `ssh_cmd`
|
- `ssh_cmd`
|
||||||
|
|
||||||
The `upload` condition includes both uploads to new files and overwrite of existing files. If an upload is aborted for quota limits SFTPGo tries to remove the partial file, so if the notification reports a zero size file and a quota exceeded error the file has been deleted. The `ssh_cmd` condition will be triggered after a command is successfully executed via SSH. `scp` will trigger the `download` and `upload` conditions and not `ssh_cmd`.
|
The `upload` condition includes both uploads to new files and overwrite of existing ones. If an upload is aborted for quota limits SFTPGo tries to remove the partial file, so if the notification reports a zero size file and a quota exceeded error the file has been deleted. The `ssh_cmd` condition will be triggered after a command is successfully executed via SSH. `scp` will trigger the `download` and `upload` conditions and not `ssh_cmd`.
|
||||||
For cloud backends directories are virtual, they are created implicitly when you upload a file and are implicitly removed when the last file within a directory is removed. The `mkdir` and `rmdir` notifications are sent only when a directory is explicitly created or removed.
|
For cloud backends directories are virtual, they are created implicitly when you upload a file and are implicitly removed when the last file within a directory is removed. The `mkdir` and `rmdir` notifications are sent only when a directory is explicitly created or removed.
|
||||||
|
|
||||||
The notification will indicate if an error is detected and so, for example, a partial file is uploaded.
|
The notification will indicate if an error is detected and so, for example, a partial file is uploaded.
|
||||||
|
@ -25,20 +29,14 @@ The `pre-delete` action, if defined, will be called just before files deletion.
|
||||||
|
|
||||||
The `pre-download` and `pre-upload` actions, will be called before downloads and uploads. If the external command completes with a zero exit status or the HTTP notification response code is `200` then SFTPGo allows the operation, otherwise the client will get a permission denied error.
|
The `pre-download` and `pre-upload` actions, will be called before downloads and uploads. If the external command completes with a zero exit status or the HTTP notification response code is `200` then SFTPGo allows the operation, otherwise the client will get a permission denied error.
|
||||||
|
|
||||||
If the `hook` defines a path to an external program, then this program is invoked with the following arguments:
|
If the `hook` defines a path to an external program, then this program can read the following environment variables:
|
||||||
|
|
||||||
- `action`, string, supported action
|
- `SFTPGO_ACTION`, supported action
|
||||||
- `username`
|
|
||||||
- `path` is the full filesystem path, can be empty for some ssh commands
|
|
||||||
- `target_path`, non-empty for `rename` action and for `sftpgo-copy` SSH command
|
|
||||||
- `ssh_cmd`, non-empty for `ssh_cmd` action
|
|
||||||
|
|
||||||
The external program can also read the following environment variables:
|
|
||||||
|
|
||||||
- `SFTPGO_ACTION`
|
|
||||||
- `SFTPGO_ACTION_USERNAME`
|
- `SFTPGO_ACTION_USERNAME`
|
||||||
- `SFTPGO_ACTION_PATH`
|
- `SFTPGO_ACTION_PATH`, is the full filesystem path, can be empty for some ssh commands
|
||||||
- `SFTPGO_ACTION_TARGET`, non-empty for `rename` `SFTPGO_ACTION`
|
- `SFTPGO_ACTION_TARGET`, full filesystem path, non-empty for `rename` `SFTPGO_ACTION` and for some SSH commands
|
||||||
|
- `SFTPGO_ACTION_VIRTUAL_PATH`, virtual path, seen by SFTPGo users
|
||||||
|
- `SFTPGO_ACTION_VIRTUAL_TARGET`, virtual target path, seen by SFTPGo users
|
||||||
- `SFTPGO_ACTION_SSH_CMD`, non-empty for `ssh_cmd` `SFTPGO_ACTION`
|
- `SFTPGO_ACTION_SSH_CMD`, non-empty for `ssh_cmd` `SFTPGO_ACTION`
|
||||||
- `SFTPGO_ACTION_FILE_SIZE`, non-zero for `pre-upload`,`upload`, `download` and `delete` actions if the file size is greater than `0`
|
- `SFTPGO_ACTION_FILE_SIZE`, non-zero for `pre-upload`,`upload`, `download` and `delete` actions if the file size is greater than `0`
|
||||||
- `SFTPGO_ACTION_FS_PROVIDER`, `0` for local filesystem, `1` for S3 backend, `2` for Google Cloud Storage (GCS) backend, `3` for Azure Blob Storage backend, `4` for local encrypted backend, `5` for SFTP backend
|
- `SFTPGO_ACTION_FS_PROVIDER`, `0` for local filesystem, `1` for S3 backend, `2` for Google Cloud Storage (GCS) backend, `3` for Azure Blob Storage backend, `4` for local encrypted backend, `5` for SFTP backend
|
||||||
|
@ -46,58 +44,66 @@ The external program can also read the following environment variables:
|
||||||
- `SFTPGO_ACTION_ENDPOINT`, non-empty for S3, SFTP and Azure backend if configured. For Azure this is the endpoint, if configured
|
- `SFTPGO_ACTION_ENDPOINT`, non-empty for S3, SFTP and Azure backend if configured. For Azure this is the endpoint, if configured
|
||||||
- `SFTPGO_ACTION_STATUS`, integer. Status for `upload`, `download` and `ssh_cmd` actions. 0 means a generic error occurred. 1 means no error, 2 means quota exceeded error
|
- `SFTPGO_ACTION_STATUS`, integer. Status for `upload`, `download` and `ssh_cmd` actions. 0 means a generic error occurred. 1 means no error, 2 means quota exceeded error
|
||||||
- `SFTPGO_ACTION_PROTOCOL`, string. Possible values are `SSH`, `SFTP`, `SCP`, `FTP`, `DAV`, `HTTP`, `DataRetention`
|
- `SFTPGO_ACTION_PROTOCOL`, string. Possible values are `SSH`, `SFTP`, `SCP`, `FTP`, `DAV`, `HTTP`, `DataRetention`
|
||||||
|
- `SFTPGO_ACTION_IP`, the action was executed from this IP address
|
||||||
- `SFTPGO_ACTION_OPEN_FLAGS`, integer. File open flags, can be non-zero for `pre-upload` action. If `SFTPGO_ACTION_FILE_SIZE` is greater than zero and `SFTPGO_ACTION_OPEN_FLAGS&512 == 0` the target file will not be truncated
|
- `SFTPGO_ACTION_OPEN_FLAGS`, integer. File open flags, can be non-zero for `pre-upload` action. If `SFTPGO_ACTION_FILE_SIZE` is greater than zero and `SFTPGO_ACTION_OPEN_FLAGS&512 == 0` the target file will not be truncated
|
||||||
|
- `SFTPGO_ACTION_TIMESTAMP`, int64. Event timestamp as milliseconds since epoch
|
||||||
|
|
||||||
Previous global environment variables aren't cleared when the script is called.
|
Previous global environment variables aren't cleared when the script is called.
|
||||||
The program must finish within 30 seconds.
|
The program must finish within 30 seconds.
|
||||||
|
|
||||||
If the `hook` defines an HTTP URL then this URL will be invoked as HTTP POST. The request body will contain a JSON serialized struct with the following fields:
|
If the `hook` defines an HTTP URL then this URL will be invoked as HTTP POST. The request body will contain a JSON serialized struct with the following fields:
|
||||||
|
|
||||||
- `action`
|
- `action`, string
|
||||||
- `username`
|
- `username`, string
|
||||||
- `path`
|
- `path`, string
|
||||||
- `target_path`, included for `rename` action and `sftpgo-copy` SSH command
|
- `target_path`, string, included for `rename` action and `sftpgo-copy` SSH command
|
||||||
- `ssh_cmd`, included for `ssh_cmd` action
|
- `virtual_path`, string, virtual path, seen by SFTPGo users
|
||||||
- `file_size`, included for `pre-upload`, `upload`, `download`, `delete` actions if the file size is greater than `0`
|
- `virtual_target_path`, string, virtual target path, seen by SFTPGo users
|
||||||
- `fs_provider`, `0` for local filesystem, `1` for S3 backend, `2` for Google Cloud Storage (GCS) backend, `3` for Azure Blob Storage backend, `4` for local encrypted backend, `5` for SFTP backend
|
- `ssh_cmd`, string, included for `ssh_cmd` action
|
||||||
- `bucket`, inlcuded for S3, GCS and Azure backends
|
- `file_size`, int64, included for `pre-upload`, `upload`, `download`, `delete` actions if the file size is greater than `0`
|
||||||
- `endpoint`, included for S3, SFTP and Azure backend if configured. For Azure this is the endpoint, if configured
|
- `fs_provider`, integer, `0` for local filesystem, `1` for S3 backend, `2` for Google Cloud Storage (GCS) backend, `3` for Azure Blob Storage backend, `4` for local encrypted backend, `5` for SFTP backend
|
||||||
|
- `bucket`, string, inlcuded for S3, GCS and Azure backends
|
||||||
|
- `endpoint`, string, included for S3, SFTP and Azure backend if configured
|
||||||
- `status`, integer. Status for `upload`, `download` and `ssh_cmd` actions. 0 means a generic error occurred. 1 means no error, 2 means quota exceeded error
|
- `status`, integer. Status for `upload`, `download` and `ssh_cmd` actions. 0 means a generic error occurred. 1 means no error, 2 means quota exceeded error
|
||||||
- `protocol`, string. Possible values are `SSH`, `SFTP`, `SCP`, `FTP`, `DAV`, `HTTP`, `DataRetention`
|
- `protocol`, string. Possible values are `SSH`, `SFTP`, `SCP`, `FTP`, `DAV`, `HTTP`, `DataRetention`
|
||||||
|
- `ip`, string. The action was executed from this IP address
|
||||||
- `open_flags`, integer. File open flags, can be non-zero for `pre-upload` action. If `file_size` is greater than zero and `file_size&512 == 0` the target file will not be truncated
|
- `open_flags`, integer. File open flags, can be non-zero for `pre-upload` action. If `file_size` is greater than zero and `file_size&512 == 0` the target file will not be truncated
|
||||||
|
- `timestamp`, int64. Event timestamp as milliseconds since epoch
|
||||||
|
|
||||||
The HTTP hook will use the global configuration for HTTP clients and will respect the retry configurations.
|
The HTTP hook will use the global configuration for HTTP clients and will respect the retry configurations.
|
||||||
|
|
||||||
The `pre-*` actions are always executed synchronously while the other ones are asynchronous. You can specify the actions to run synchronously via the `execute_sync` configuration key. Executing an action synchronously means that SFTPGo will not return a result code to the client (which is waiting for it) until your hook have completed its execution. If your hook takes a long time to complete this could cause a timeout on the client side, which wouldn't receive the server response in a timely manner and eventually drop the connection.
|
The `pre-*` actions are always executed synchronously while the other ones are asynchronous. You can specify the actions to run synchronously via the `execute_sync` configuration key. Executing an action synchronously means that SFTPGo will not return a result code to the client (which is waiting for it) until your hook have completed its execution. If your hook takes a long time to complete this could cause a timeout on the client side, which wouldn't receive the server response in a timely manner and eventually drop the connection.
|
||||||
|
|
||||||
The `actions` struct inside the `data_provider` configuration section allows you to configure actions on user add, update, delete.
|
## Provider events
|
||||||
|
|
||||||
|
The `actions` struct inside the `data_provider` configuration section allows you to configure actions on data provider objects add, update, delete.
|
||||||
|
|
||||||
|
The supported object types are:
|
||||||
|
|
||||||
|
- `user`
|
||||||
|
- `admin`
|
||||||
|
- `api_key`
|
||||||
|
|
||||||
Actions will not be fired for internal updates, such as the last login or the user quota fields, or after external authentication.
|
Actions will not be fired for internal updates, such as the last login or the user quota fields, or after external authentication.
|
||||||
|
|
||||||
If the `hook` defines a path to an external program, then this program is invoked with the following arguments:
|
If the `hook` defines a path to an external program, then this program can read the following environment variables:
|
||||||
|
|
||||||
- `action`, string, possible values are: `add`, `update`, `delete`
|
- `SFTPGO_PROVIDER_ACTION`, supported values are `add`, `update`, `delete`
|
||||||
- `username`
|
- `SFTPGO_PROVIDER_OBJECT_TYPE`, affetected object type
|
||||||
- `ID`
|
- `SFTPGO_PROVIDER_OBJECT_NAME`, unique identifier for the affected object, for example username or key id
|
||||||
- `status`
|
- `SFTPGO_PROVIDER_USERNAME`, the username that executed the action. There are two special usernames: `__self__` identifies a user/admin that updates itself and `__system__` identifies an action that does not have an explicit executor associated with it, for example users/admins can be added/updated by loading them from initial data
|
||||||
- `expiration_date`
|
- `SFTPGO_PROVIDER_IP`, the action was executed from this IP address
|
||||||
- `home_dir`
|
- `SFTPGO_PROVIDER_TIMESTAMP`, event timestamp as milliseconds since epoch
|
||||||
- `uid`
|
- `SFTPGO_PROVIDER_OBJECT`, object serialized as JSON with sensitive fields removed
|
||||||
- `gid`
|
|
||||||
|
|
||||||
The external program can also read the following environment variables:
|
|
||||||
|
|
||||||
- `SFTPGO_USER_ACTION`
|
|
||||||
- `SFTPGO_USER`, user serialized as JSON with sensitive fields removed
|
|
||||||
|
|
||||||
Previous global environment variables aren't cleared when the script is called.
|
Previous global environment variables aren't cleared when the script is called.
|
||||||
The program must finish within 15 seconds.
|
The program must finish within 15 seconds.
|
||||||
|
|
||||||
If the `hook` defines an HTTP URL then this URL will be invoked as HTTP POST. The action is added to the query string, for example `<hook>?action=update`, and the user is sent serialized as JSON inside the POST body with sensitive fields removed.
|
If the `hook` defines an HTTP URL then this URL will be invoked as HTTP POST. The action, username, ip, object_type and object_name and timestamp are added to the query string, for example `<hook>?action=update&username=admin&ip=127.0.0.1&object_type=user&object_name=user1×tamp=1633860803249`, and the full object is sent serialized as JSON inside the POST body with sensitive fields removed.
|
||||||
|
|
||||||
The HTTP hook will use the global configuration for HTTP clients and will respect the retry configurations.
|
The HTTP hook will use the global configuration for HTTP clients and will respect the retry configurations.
|
||||||
|
|
||||||
The structure for SFTPGo users can be found within the [OpenAPI schema](../httpd/schema/openapi.yaml).
|
The structure for SFTPGo objects can be found within the [OpenAPI schema](../httpd/schema/openapi.yaml).
|
||||||
|
|
||||||
## Pub/Sub services
|
## Pub/Sub services
|
||||||
|
|
||||||
|
|
|
@ -181,6 +181,7 @@ The configuration file contains the following sections:
|
||||||
- `users_base_dir`, string. Users default base directory. If no home dir is defined while adding a new user, and this value is a valid absolute path, then the user home dir will be automatically defined as the path obtained joining the base dir and the username
|
- `users_base_dir`, string. Users default base directory. If no home dir is defined while adding a new user, and this value is a valid absolute path, then the user home dir will be automatically defined as the path obtained joining the base dir and the username
|
||||||
- `actions`, struct. It contains the command to execute and/or the HTTP URL to notify and the trigger conditions. See [Custom Actions](./custom-actions.md) for more details
|
- `actions`, struct. It contains the command to execute and/or the HTTP URL to notify and the trigger conditions. See [Custom Actions](./custom-actions.md) for more details
|
||||||
- `execute_on`, list of strings. Valid values are `add`, `update`, `delete`. `update` action will not be fired for internal updates such as the last login or the user quota fields.
|
- `execute_on`, list of strings. Valid values are `add`, `update`, `delete`. `update` action will not be fired for internal updates such as the last login or the user quota fields.
|
||||||
|
- `execute_for`, list of strings. Defines the provider objects that trigger the action. Valid values are `user`, `admin`, `api_key`.
|
||||||
- `hook`, string. Absolute path to the command to execute or HTTP URL to notify.
|
- `hook`, string. Absolute path to the command to execute or HTTP URL to notify.
|
||||||
- `external_auth_hook`, string. Absolute path to an external program or an HTTP URL to invoke for users authentication. See [External Authentication](./external-auth.md) for more details. Leave empty to disable.
|
- `external_auth_hook`, string. Absolute path to an external program or an HTTP URL to invoke for users authentication. See [External Authentication](./external-auth.md) for more details. Leave empty to disable.
|
||||||
- `external_auth_scope`, integer. 0 means all supported authentication scopes (passwords, public keys and keyboard interactive). 1 means passwords only. 2 means public keys only. 4 means key keyboard interactive only. 8 means TLS certificate. The flags can be combined, for example 6 means public keys and keyboard interactive
|
- `external_auth_scope`, integer. 0 means all supported authentication scopes (passwords, public keys and keyboard interactive). 1 means passwords only. 2 means public keys only. 4 means key keyboard interactive only. 8 means TLS certificate. The flags can be combined, for example 6 means public keys and keyboard interactive
|
||||||
|
@ -276,7 +277,8 @@ The configuration file contains the following sections:
|
||||||
- `type`, string. Defines the plugin type. Supported types: `notifier`, `kms`, `auth`.
|
- `type`, string. Defines the plugin type. Supported types: `notifier`, `kms`, `auth`.
|
||||||
- `notifier_options`, struct. Defines the options for notifier plugins.
|
- `notifier_options`, struct. Defines the options for notifier plugins.
|
||||||
- `fs_events`, list of strings. Defines the filesystem events that will be notified to this plugin.
|
- `fs_events`, list of strings. Defines the filesystem events that will be notified to this plugin.
|
||||||
- `user_events`, list of strings. Defines the user events that will be notified to this plugin.
|
- `provider_events`, list of strings. Defines the provider events that will be notified to this plugin.
|
||||||
|
- `provider_objects`, list if strings. Defines the provider objects that will be notified to this plugin.
|
||||||
- `retry_max_time`, integer. Defines the maximum number of seconds an event can be late. SFTPGo adds a timestamp to each event and add to an internal queue any events that a the plugin fails to handle (the plugin returns an error or it is not running). If a plugin fails to handle an event that is too late, based on this configuration, it will be discarded. SFTPGo will try to resend queued events every 30 seconds. 0 means no retry.
|
- `retry_max_time`, integer. Defines the maximum number of seconds an event can be late. SFTPGo adds a timestamp to each event and add to an internal queue any events that a the plugin fails to handle (the plugin returns an error or it is not running). If a plugin fails to handle an event that is too late, based on this configuration, it will be discarded. SFTPGo will try to resend queued events every 30 seconds. 0 means no retry.
|
||||||
- `retry_queue_max_size`, integer. Defines the maximum number of events that the internal queue can hold. Once the queue is full, the events that cannot be sent to the plugin will be discarded. 0 means no limit.
|
- `retry_queue_max_size`, integer. Defines the maximum number of events that the internal queue can hold. Once the queue is full, the events that cannot be sent to the plugin will be discarded. 0 means no limit.
|
||||||
- `kms_options`, struct. Defines the options for kms plugins.
|
- `kms_options`, struct. Defines the options for kms plugins.
|
||||||
|
@ -287,7 +289,7 @@ The configuration file contains the following sections:
|
||||||
- `cmd`, string. Path to the plugin executable.
|
- `cmd`, string. Path to the plugin executable.
|
||||||
- `args`, list of strings. Optional arguments to pass to the plugin executable.
|
- `args`, list of strings. Optional arguments to pass to the plugin executable.
|
||||||
- `sha256sum`, string. SHA256 checksum for the plugin executable. If not empty it will be used to verify the integrity of the executable.
|
- `sha256sum`, string. SHA256 checksum for the plugin executable. If not empty it will be used to verify the integrity of the executable.
|
||||||
- `auto_mtls`, boolean. If enabled the client and the server automatically negotiate mTLS for transport authentication. This ensures that only the original client will be allowed to connect to the server, and all other connections will be rejected. The client will also refuse to connect to any server that isn't the original instance started by the client.
|
- `auto_mtls`, boolean. If enabled the client and the server automatically negotiate mutual TLS for transport authentication. This ensures that only the original client will be allowed to connect to the server, and all other connections will be rejected. The client will also refuse to connect to any server that isn't the original instance started by the client.
|
||||||
|
|
||||||
Please note that the plugin system is experimental, the exposed configuration parameters and interfaces may change in a backward incompatible way in future.
|
Please note that the plugin system is experimental, the exposed configuration parameters and interfaces may change in a backward incompatible way in future.
|
||||||
|
|
||||||
|
|
|
@ -610,7 +610,7 @@ func TestMultiFactorAuth(t *testing.T) {
|
||||||
Secret: kms.NewPlainSecret(secret),
|
Secret: kms.NewPlainSecret(secret),
|
||||||
Protocols: []string{common.ProtocolFTP},
|
Protocols: []string{common.ProtocolFTP},
|
||||||
}
|
}
|
||||||
err = dataprovider.UpdateUser(&user)
|
err = dataprovider.UpdateUser(&user, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
user.Password = defaultPassword
|
user.Password = defaultPassword
|
||||||
|
@ -971,7 +971,7 @@ func TestMaxConnections(t *testing.T) {
|
||||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||||
|
|
||||||
user := getTestUser()
|
user := getTestUser()
|
||||||
err := dataprovider.AddUser(&user)
|
err := dataprovider.AddUser(&user, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user.Password = ""
|
user.Password = ""
|
||||||
client, err := getFTPClient(user, true, nil)
|
client, err := getFTPClient(user, true, nil)
|
||||||
|
@ -983,7 +983,7 @@ func TestMaxConnections(t *testing.T) {
|
||||||
err = client.Quit()
|
err = client.Quit()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
err = dataprovider.DeleteUser(user.Username)
|
err = dataprovider.DeleteUser(user.Username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1001,7 +1001,7 @@ func TestMaxPerHostConnections(t *testing.T) {
|
||||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||||
|
|
||||||
user := getTestUser()
|
user := getTestUser()
|
||||||
err := dataprovider.AddUser(&user)
|
err := dataprovider.AddUser(&user, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user.Password = ""
|
user.Password = ""
|
||||||
client, err := getFTPClient(user, true, nil)
|
client, err := getFTPClient(user, true, nil)
|
||||||
|
@ -1013,7 +1013,7 @@ func TestMaxPerHostConnections(t *testing.T) {
|
||||||
err = client.Quit()
|
err = client.Quit()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
err = dataprovider.DeleteUser(user.Username)
|
err = dataprovider.DeleteUser(user.Username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1070,7 +1070,7 @@ func TestRateLimiter(t *testing.T) {
|
||||||
assert.Contains(t, err.Error(), "banned client IP")
|
assert.Contains(t, err.Error(), "banned client IP")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(user.Username)
|
err = dataprovider.DeleteUser(user.Username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1112,7 +1112,7 @@ func TestDefender(t *testing.T) {
|
||||||
assert.Contains(t, err.Error(), "banned client IP")
|
assert.Contains(t, err.Error(), "banned client IP")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(user.Username)
|
err = dataprovider.DeleteUser(user.Username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
@ -325,7 +325,8 @@ func (c *Connection) downloadFile(fs vfs.Fs, fsPath, ftpPath string, offset int6
|
||||||
return nil, c.GetPermissionDeniedError()
|
return nil, c.GetPermissionDeniedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := common.ExecutePreAction(&c.User, common.OperationPreDownload, fsPath, ftpPath, c.GetProtocol(), 0, 0); err != nil {
|
if err := common.ExecutePreAction(&c.User, common.OperationPreDownload, fsPath, ftpPath, c.GetProtocol(), c.GetRemoteIP(),
|
||||||
|
0, 0); err != nil {
|
||||||
c.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", ftpPath, err)
|
c.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", ftpPath, err)
|
||||||
return nil, c.GetPermissionDeniedError()
|
return nil, c.GetPermissionDeniedError()
|
||||||
}
|
}
|
||||||
|
@ -387,7 +388,8 @@ func (c *Connection) handleFTPUploadToNewFile(fs vfs.Fs, resolvedPath, filePath,
|
||||||
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
||||||
return nil, ftpserver.ErrStorageExceeded
|
return nil, ftpserver.ErrStorageExceeded
|
||||||
}
|
}
|
||||||
if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), 0, 0); err != nil {
|
if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(),
|
||||||
|
c.GetRemoteIP(), 0, 0); err != nil {
|
||||||
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||||
return nil, fmt.Errorf("%w, denied by pre-upload action", ftpserver.ErrFileNameNotAllowed)
|
return nil, fmt.Errorf("%w, denied by pre-upload action", ftpserver.ErrFileNameNotAllowed)
|
||||||
}
|
}
|
||||||
|
@ -432,7 +434,7 @@ func (c *Connection) handleFTPUploadToExistingFile(fs vfs.Fs, flags int, resolve
|
||||||
c.Log(logger.LevelDebug, "unable to get max write size: %v", err)
|
c.Log(logger.LevelDebug, "unable to get max write size: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), fileSize, flags); err != nil {
|
if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), c.GetRemoteIP(), fileSize, flags); err != nil {
|
||||||
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||||
return nil, fmt.Errorf("%w, denied by pre-upload action", ftpserver.ErrFileNameNotAllowed)
|
return nil, fmt.Errorf("%w, denied by pre-upload action", ftpserver.ErrFileNameNotAllowed)
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,13 +50,18 @@ func renderAdmin(w http.ResponseWriter, r *http.Request, username string, status
|
||||||
|
|
||||||
func addAdmin(w http.ResponseWriter, r *http.Request) {
|
func addAdmin(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
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 admin dataprovider.Admin
|
var admin dataprovider.Admin
|
||||||
err := render.DecodeJSON(r.Body, &admin)
|
err = render.DecodeJSON(r.Body, &admin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = dataprovider.AddAdmin(&admin)
|
err = dataprovider.AddAdmin(&admin, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
@ -66,8 +71,12 @@ func addAdmin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func disableAdmin2FA(w http.ResponseWriter, r *http.Request) {
|
func disableAdmin2FA(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
username := getURLParam(r, "username")
|
claims, err := getTokenClaims(r)
|
||||||
admin, err := dataprovider.AdminExists(username)
|
if err != nil || claims.Username == "" {
|
||||||
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
admin, err := dataprovider.AdminExists(getURLParam(r, "username"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
@ -76,7 +85,7 @@ func disableAdmin2FA(w http.ResponseWriter, r *http.Request) {
|
||||||
admin.Filters.TOTPConfig = dataprovider.TOTPConfig{
|
admin.Filters.TOTPConfig = dataprovider.TOTPConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
}
|
}
|
||||||
if err := dataprovider.UpdateAdmin(&admin); err != nil {
|
if err := dataprovider.UpdateAdmin(&admin, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -127,7 +136,7 @@ func updateAdmin(w http.ResponseWriter, r *http.Request) {
|
||||||
admin.Username = username
|
admin.Username = username
|
||||||
admin.Filters.TOTPConfig = totpConfig
|
admin.Filters.TOTPConfig = totpConfig
|
||||||
admin.Filters.RecoveryCodes = recoveryCodes
|
admin.Filters.RecoveryCodes = recoveryCodes
|
||||||
if err := dataprovider.UpdateAdmin(&admin); err != nil {
|
if err := dataprovider.UpdateAdmin(&admin, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -147,7 +156,7 @@ func deleteAdmin(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dataprovider.DeleteAdmin(username)
|
err = dataprovider.DeleteAdmin(username, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
@ -198,7 +207,7 @@ func updateAdminProfile(w http.ResponseWriter, r *http.Request) {
|
||||||
admin.Email = req.Email
|
admin.Email = req.Email
|
||||||
admin.Description = req.Description
|
admin.Description = req.Description
|
||||||
admin.Filters.AllowAPIKeyAuth = req.AllowAPIKeyAuth
|
admin.Filters.AllowAPIKeyAuth = req.AllowAPIKeyAuth
|
||||||
if err := dataprovider.UpdateAdmin(&admin); err != nil {
|
if err := dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -247,7 +256,7 @@ func doChangeAdminPassword(r *http.Request, currentPassword, newPassword, confir
|
||||||
|
|
||||||
admin.Password = newPassword
|
admin.Password = newPassword
|
||||||
|
|
||||||
return dataprovider.UpdateAdmin(&admin)
|
return dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTokenClaims(r *http.Request) (jwtTokenClaims, error) {
|
func getTokenClaims(r *http.Request) (jwtTokenClaims, error) {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"github.com/go-chi/render"
|
"github.com/go-chi/render"
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||||
|
"github.com/drakkan/sftpgo/v2/util"
|
||||||
"github.com/drakkan/sftpgo/v2/vfs"
|
"github.com/drakkan/sftpgo/v2/vfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -43,7 +44,11 @@ func addFolder(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func updateFolder(w http.ResponseWriter, r *http.Request) {
|
func updateFolder(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
var err error
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
name := getURLParam(r, "name")
|
name := getURLParam(r, "name")
|
||||||
folder, err := dataprovider.GetFolderByName(name)
|
folder, err := dataprovider.GetFolderByName(name)
|
||||||
|
@ -76,7 +81,7 @@ func updateFolder(w http.ResponseWriter, r *http.Request) {
|
||||||
folder.FsConfig.SetEmptySecretsIfNil()
|
folder.FsConfig.SetEmptySecretsIfNil()
|
||||||
updateEncryptedSecrets(&folder.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl, currentGCSCredentials,
|
updateEncryptedSecrets(&folder.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl, currentGCSCredentials,
|
||||||
currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey)
|
currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey)
|
||||||
err = dataprovider.UpdateFolder(&folder, users)
|
err = dataprovider.UpdateFolder(&folder, users, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
@ -107,8 +112,13 @@ func getFolderByName(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func deleteFolder(w http.ResponseWriter, r *http.Request) {
|
func deleteFolder(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
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")
|
name := getURLParam(r, "name")
|
||||||
err := dataprovider.DeleteFolder(name)
|
err = dataprovider.DeleteFolder(name, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
|
|
@ -341,7 +341,7 @@ func setUserPublicKeys(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
user.PublicKeys = publicKeys
|
user.PublicKeys = publicKeys
|
||||||
err = dataprovider.UpdateUser(&user)
|
err = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
@ -404,7 +404,7 @@ func updateUserProfile(w http.ResponseWriter, r *http.Request) {
|
||||||
user.Email = req.Email
|
user.Email = req.Email
|
||||||
user.Description = req.Description
|
user.Description = req.Description
|
||||||
}
|
}
|
||||||
if err := dataprovider.UpdateUser(&user); err != nil {
|
if err := dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -449,5 +449,5 @@ func doChangeUserPassword(r *http.Request, currentPassword, newPassword, confirm
|
||||||
}
|
}
|
||||||
user.Password = newPassword
|
user.Password = newPassword
|
||||||
|
|
||||||
return dataprovider.UpdateUser(&user)
|
return dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/go-chi/render"
|
"github.com/go-chi/render"
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||||
|
"github.com/drakkan/sftpgo/v2/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getAPIKeys(w http.ResponseWriter, r *http.Request) {
|
func getAPIKeys(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -40,8 +41,13 @@ func getAPIKeyByID(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func addAPIKey(w http.ResponseWriter, r *http.Request) {
|
func addAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
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 apiKey dataprovider.APIKey
|
var apiKey dataprovider.APIKey
|
||||||
err := render.DecodeJSON(r.Body, &apiKey)
|
err = render.DecodeJSON(r.Body, &apiKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
@ -49,7 +55,7 @@ func addAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||||
apiKey.ID = 0
|
apiKey.ID = 0
|
||||||
apiKey.KeyID = ""
|
apiKey.KeyID = ""
|
||||||
apiKey.Key = ""
|
apiKey.Key = ""
|
||||||
err = dataprovider.AddAPIKey(&apiKey)
|
err = dataprovider.AddAPIKey(&apiKey, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
@ -65,6 +71,11 @@ func addAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func updateAPIKey(w http.ResponseWriter, r *http.Request) {
|
func updateAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
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
|
||||||
|
}
|
||||||
keyID := getURLParam(r, "id")
|
keyID := getURLParam(r, "id")
|
||||||
apiKey, err := dataprovider.APIKeyExists(keyID)
|
apiKey, err := dataprovider.APIKeyExists(keyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -79,7 +90,7 @@ func updateAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
apiKey.KeyID = keyID
|
apiKey.KeyID = keyID
|
||||||
if err := dataprovider.UpdateAPIKey(&apiKey); err != nil {
|
if err := dataprovider.UpdateAPIKey(&apiKey, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -89,8 +100,13 @@ func updateAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||||
func deleteAPIKey(w http.ResponseWriter, r *http.Request) {
|
func deleteAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
keyID := getURLParam(r, "id")
|
keyID := getURLParam(r, "id")
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err := dataprovider.DeleteAPIKey(keyID)
|
err = dataprovider.DeleteAPIKey(keyID, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
|
|
@ -97,6 +97,11 @@ func dumpData(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func loadDataFromRequest(w http.ResponseWriter, r *http.Request) {
|
func loadDataFromRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, MaxRestoreSize)
|
r.Body = http.MaxBytesReader(w, r.Body, MaxRestoreSize)
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
_, scanQuota, mode, err := getLoaddataOptions(r)
|
_, scanQuota, mode, err := getLoaddataOptions(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||||
|
@ -111,7 +116,7 @@ func loadDataFromRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := restoreBackup(content, "", scanQuota, mode); err != nil {
|
if err := restoreBackup(content, "", scanQuota, mode, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
}
|
}
|
||||||
sendAPIResponse(w, r, err, "Data restored", http.StatusOK)
|
sendAPIResponse(w, r, err, "Data restored", http.StatusOK)
|
||||||
|
@ -119,6 +124,11 @@ func loadDataFromRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func loadData(w http.ResponseWriter, r *http.Request) {
|
func loadData(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
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
|
||||||
|
}
|
||||||
inputFile, scanQuota, mode, err := getLoaddataOptions(r)
|
inputFile, scanQuota, mode, err := getLoaddataOptions(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||||
|
@ -144,31 +154,31 @@ func loadData(w http.ResponseWriter, r *http.Request) {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := restoreBackup(content, inputFile, scanQuota, mode); err != nil {
|
if err := restoreBackup(content, inputFile, scanQuota, mode, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
}
|
}
|
||||||
sendAPIResponse(w, r, err, "Data restored", http.StatusOK)
|
sendAPIResponse(w, r, err, "Data restored", http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreBackup(content []byte, inputFile string, scanQuota, mode int) error {
|
func restoreBackup(content []byte, inputFile string, scanQuota, mode int, executor, ipAddress string) error {
|
||||||
dump, err := dataprovider.ParseDumpData(content)
|
dump, err := dataprovider.ParseDumpData(content)
|
||||||
if err != nil {
|
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); err != nil {
|
if err = RestoreFolders(dump.Folders, inputFile, mode, scanQuota, executor, ipAddress); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = RestoreUsers(dump.Users, inputFile, mode, scanQuota); err != nil {
|
if err = RestoreUsers(dump.Users, inputFile, mode, scanQuota, executor, ipAddress); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = RestoreAdmins(dump.Admins, inputFile, mode); err != nil {
|
if err = RestoreAdmins(dump.Admins, inputFile, mode, executor, ipAddress); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = RestoreAPIKeys(dump.APIKeys, inputFile, mode); err != nil {
|
if err = RestoreAPIKeys(dump.APIKeys, inputFile, mode, executor, ipAddress); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,7 +214,7 @@ func getLoaddataOptions(r *http.Request) (string, int, int, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestoreFolders restores the specified folders
|
// RestoreFolders restores the specified folders
|
||||||
func RestoreFolders(folders []vfs.BaseVirtualFolder, inputFile string, mode, scanQuota int) error {
|
func RestoreFolders(folders []vfs.BaseVirtualFolder, inputFile string, mode, scanQuota int, executor, ipAddress string) error {
|
||||||
for _, folder := range folders {
|
for _, folder := range folders {
|
||||||
folder := folder // pin
|
folder := folder // pin
|
||||||
f, err := dataprovider.GetFolderByName(folder.Name)
|
f, err := dataprovider.GetFolderByName(folder.Name)
|
||||||
|
@ -214,7 +224,7 @@ func RestoreFolders(folders []vfs.BaseVirtualFolder, inputFile string, mode, sca
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
folder.ID = f.ID
|
folder.ID = f.ID
|
||||||
err = dataprovider.UpdateFolder(&folder, f.Users)
|
err = dataprovider.UpdateFolder(&folder, f.Users, executor, ipAddress)
|
||||||
logger.Debug(logSender, "", "restoring existing folder: %+v, dump file: %#v, error: %v", folder, inputFile, err)
|
logger.Debug(logSender, "", "restoring existing folder: %+v, dump file: %#v, error: %v", folder, inputFile, err)
|
||||||
} else {
|
} else {
|
||||||
folder.Users = nil
|
folder.Users = nil
|
||||||
|
@ -235,7 +245,7 @@ func RestoreFolders(folders []vfs.BaseVirtualFolder, inputFile string, mode, sca
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestoreAPIKeys restores the specified API keys
|
// RestoreAPIKeys restores the specified API keys
|
||||||
func RestoreAPIKeys(apiKeys []dataprovider.APIKey, inputFile string, mode int) error {
|
func RestoreAPIKeys(apiKeys []dataprovider.APIKey, inputFile string, mode int, executor, ipAddress string) error {
|
||||||
for _, apiKey := range apiKeys {
|
for _, apiKey := range apiKeys {
|
||||||
apiKey := apiKey // pin
|
apiKey := apiKey // pin
|
||||||
if apiKey.Key == "" {
|
if apiKey.Key == "" {
|
||||||
|
@ -249,11 +259,11 @@ func RestoreAPIKeys(apiKeys []dataprovider.APIKey, inputFile string, mode int) e
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
apiKey.ID = k.ID
|
apiKey.ID = k.ID
|
||||||
err = dataprovider.UpdateAPIKey(&apiKey)
|
err = dataprovider.UpdateAPIKey(&apiKey, executor, ipAddress)
|
||||||
apiKey.Key = redactedSecret
|
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, inputFile, err)
|
||||||
} else {
|
} else {
|
||||||
err = dataprovider.AddAPIKey(&apiKey)
|
err = dataprovider.AddAPIKey(&apiKey, executor, ipAddress)
|
||||||
apiKey.Key = redactedSecret
|
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, inputFile, err)
|
||||||
}
|
}
|
||||||
|
@ -265,7 +275,7 @@ func RestoreAPIKeys(apiKeys []dataprovider.APIKey, inputFile string, mode int) e
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestoreAdmins restores the specified admins
|
// RestoreAdmins restores the specified admins
|
||||||
func RestoreAdmins(admins []dataprovider.Admin, inputFile string, mode int) error {
|
func RestoreAdmins(admins []dataprovider.Admin, inputFile string, mode int, executor, ipAddress string) error {
|
||||||
for _, admin := range admins {
|
for _, admin := range admins {
|
||||||
admin := admin // pin
|
admin := admin // pin
|
||||||
a, err := dataprovider.AdminExists(admin.Username)
|
a, err := dataprovider.AdminExists(admin.Username)
|
||||||
|
@ -275,11 +285,11 @@ func RestoreAdmins(admins []dataprovider.Admin, inputFile string, mode int) erro
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
admin.ID = a.ID
|
admin.ID = a.ID
|
||||||
err = dataprovider.UpdateAdmin(&admin)
|
err = dataprovider.UpdateAdmin(&admin, executor, ipAddress)
|
||||||
admin.Password = redactedSecret
|
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, inputFile, err)
|
||||||
} else {
|
} else {
|
||||||
err = dataprovider.AddAdmin(&admin)
|
err = dataprovider.AddAdmin(&admin, executor, ipAddress)
|
||||||
admin.Password = redactedSecret
|
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, inputFile, err)
|
||||||
}
|
}
|
||||||
|
@ -292,7 +302,7 @@ func RestoreAdmins(admins []dataprovider.Admin, inputFile string, mode int) erro
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestoreUsers restores the specified users
|
// RestoreUsers restores the specified users
|
||||||
func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota int) error {
|
func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota int, executor, ipAddress string) error {
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
user := user // pin
|
user := user // pin
|
||||||
u, err := dataprovider.UserExists(user.Username)
|
u, err := dataprovider.UserExists(user.Username)
|
||||||
|
@ -302,14 +312,14 @@ func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota i
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
user.ID = u.ID
|
user.ID = u.ID
|
||||||
err = dataprovider.UpdateUser(&user)
|
err = dataprovider.UpdateUser(&user, executor, ipAddress)
|
||||||
user.Password = redactedSecret
|
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, inputFile, err)
|
||||||
if mode == 2 && err == nil {
|
if mode == 2 && err == nil {
|
||||||
disconnectUser(user.Username)
|
disconnectUser(user.Username)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err = dataprovider.AddUser(&user)
|
err = dataprovider.AddUser(&user, executor, ipAddress)
|
||||||
user.Password = redactedSecret
|
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, inputFile, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -177,7 +177,7 @@ func generateRecoveryCodes(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.Filters.RecoveryCodes = accountRecoveryCodes
|
user.Filters.RecoveryCodes = accountRecoveryCodes
|
||||||
if err := dataprovider.UpdateUser(&user); err != nil {
|
if err := dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -188,7 +188,7 @@ func generateRecoveryCodes(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
admin.Filters.RecoveryCodes = accountRecoveryCodes
|
admin.Filters.RecoveryCodes = accountRecoveryCodes
|
||||||
if err := dataprovider.UpdateAdmin(&admin); err != nil {
|
if err := dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -218,7 +218,7 @@ func saveUserTOTPConfig(username string, r *http.Request, recoveryCodes []sdk.Re
|
||||||
if user.CountUnusedRecoveryCodes() < 5 && user.Filters.TOTPConfig.Enabled {
|
if user.CountUnusedRecoveryCodes() < 5 && user.Filters.TOTPConfig.Enabled {
|
||||||
user.Filters.RecoveryCodes = recoveryCodes
|
user.Filters.RecoveryCodes = recoveryCodes
|
||||||
}
|
}
|
||||||
return dataprovider.UpdateUser(&user)
|
return dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveAdminTOTPConfig(username string, r *http.Request, recoveryCodes []sdk.RecoveryCode) error {
|
func saveAdminTOTPConfig(username string, r *http.Request, recoveryCodes []sdk.RecoveryCode) error {
|
||||||
|
@ -238,5 +238,5 @@ func saveAdminTOTPConfig(username string, r *http.Request, recoveryCodes []sdk.R
|
||||||
if admin.Filters.TOTPConfig.Secret == nil || !admin.Filters.TOTPConfig.Secret.IsPlain() {
|
if admin.Filters.TOTPConfig.Secret == nil || !admin.Filters.TOTPConfig.Secret.IsPlain() {
|
||||||
admin.Filters.TOTPConfig.Secret = currentTOTPSecret
|
admin.Filters.TOTPConfig.Secret = currentTOTPSecret
|
||||||
}
|
}
|
||||||
return dataprovider.UpdateAdmin(&admin)
|
return dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package httpd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -13,6 +12,7 @@ import (
|
||||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||||
"github.com/drakkan/sftpgo/v2/kms"
|
"github.com/drakkan/sftpgo/v2/kms"
|
||||||
"github.com/drakkan/sftpgo/v2/sdk"
|
"github.com/drakkan/sftpgo/v2/sdk"
|
||||||
|
"github.com/drakkan/sftpgo/v2/util"
|
||||||
"github.com/drakkan/sftpgo/v2/vfs"
|
"github.com/drakkan/sftpgo/v2/vfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -54,49 +54,19 @@ func renderUser(w http.ResponseWriter, r *http.Request, username string, status
|
||||||
|
|
||||||
func addUser(w http.ResponseWriter, r *http.Request) {
|
func addUser(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
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 user dataprovider.User
|
var user dataprovider.User
|
||||||
err := render.DecodeJSON(r.Body, &user)
|
err = render.DecodeJSON(r.Body, &user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.SetEmptySecretsIfNil()
|
err = dataprovider.AddUser(&user, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
switch user.FsConfig.Provider {
|
|
||||||
case sdk.S3FilesystemProvider:
|
|
||||||
if user.FsConfig.S3Config.AccessSecret.IsRedacted() {
|
|
||||||
sendAPIResponse(w, r, errors.New("invalid access_secret"), "", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case sdk.GCSFilesystemProvider:
|
|
||||||
if user.FsConfig.GCSConfig.Credentials.IsRedacted() {
|
|
||||||
sendAPIResponse(w, r, errors.New("invalid credentials"), "", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case sdk.AzureBlobFilesystemProvider:
|
|
||||||
if user.FsConfig.AzBlobConfig.AccountKey.IsRedacted() {
|
|
||||||
sendAPIResponse(w, r, errors.New("invalid account_key"), "", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if user.FsConfig.AzBlobConfig.SASURL.IsRedacted() {
|
|
||||||
sendAPIResponse(w, r, errors.New("invalid sas_url"), "", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case sdk.CryptedFilesystemProvider:
|
|
||||||
if user.FsConfig.CryptConfig.Passphrase.IsRedacted() {
|
|
||||||
sendAPIResponse(w, r, errors.New("invalid passphrase"), "", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case sdk.SFTPFilesystemProvider:
|
|
||||||
if user.FsConfig.SFTPConfig.Password.IsRedacted() {
|
|
||||||
sendAPIResponse(w, r, errors.New("invalid SFTP password"), "", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if user.FsConfig.SFTPConfig.PrivateKey.IsRedacted() {
|
|
||||||
sendAPIResponse(w, r, errors.New("invalid SFTP private key"), "", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = dataprovider.AddUser(&user)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
@ -106,6 +76,11 @@ func addUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func disableUser2FA(w http.ResponseWriter, r *http.Request) {
|
func disableUser2FA(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
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
|
||||||
|
}
|
||||||
username := getURLParam(r, "username")
|
username := getURLParam(r, "username")
|
||||||
user, err := dataprovider.UserExists(username)
|
user, err := dataprovider.UserExists(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -116,7 +91,7 @@ func disableUser2FA(w http.ResponseWriter, r *http.Request) {
|
||||||
user.Filters.TOTPConfig = sdk.TOTPConfig{
|
user.Filters.TOTPConfig = sdk.TOTPConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
}
|
}
|
||||||
if err := dataprovider.UpdateUser(&user); err != nil {
|
if err := dataprovider.UpdateUser(&user, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -125,7 +100,11 @@ func disableUser2FA(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func updateUser(w http.ResponseWriter, r *http.Request) {
|
func updateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
var err error
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
username := getURLParam(r, "username")
|
username := getURLParam(r, "username")
|
||||||
disconnect := 0
|
disconnect := 0
|
||||||
|
@ -179,7 +158,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
updateEncryptedSecrets(&user.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl,
|
updateEncryptedSecrets(&user.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl,
|
||||||
currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey)
|
currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey)
|
||||||
err = dataprovider.UpdateUser(&user)
|
err = dataprovider.UpdateUser(&user, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
@ -192,8 +171,13 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func deleteUser(w http.ResponseWriter, r *http.Request) {
|
func deleteUser(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
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
|
||||||
|
}
|
||||||
username := getURLParam(r, "username")
|
username := getURLParam(r, "username")
|
||||||
err := dataprovider.DeleteUser(username)
|
err = dataprovider.DeleteUser(username, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
|
|
@ -99,7 +99,7 @@ func (c *Connection) getFileReader(name string, offset int64, method string) (io
|
||||||
}
|
}
|
||||||
|
|
||||||
if method != http.MethodHead {
|
if method != http.MethodHead {
|
||||||
if err := common.ExecutePreAction(&c.User, common.OperationPreDownload, p, name, c.GetProtocol(), 0, 0); err != nil {
|
if err := common.ExecutePreAction(&c.User, common.OperationPreDownload, p, name, c.GetProtocol(), c.GetRemoteIP(), 0, 0); err != nil {
|
||||||
c.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", name, err)
|
c.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", name, err)
|
||||||
return nil, c.GetPermissionDeniedError()
|
return nil, c.GetPermissionDeniedError()
|
||||||
}
|
}
|
||||||
|
@ -174,7 +174,7 @@ func (c *Connection) handleUploadFile(fs vfs.Fs, resolvedPath, filePath, request
|
||||||
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
||||||
return nil, common.ErrQuotaExceeded
|
return nil, common.ErrQuotaExceeded
|
||||||
}
|
}
|
||||||
err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), fileSize, os.O_TRUNC)
|
err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), c.GetRemoteIP(), fileSize, os.O_TRUNC)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||||
return nil, c.GetPermissionDeniedError()
|
return nil, c.GetPermissionDeniedError()
|
||||||
|
|
|
@ -980,7 +980,7 @@ func TestChangeAdminPassword(t *testing.T) {
|
||||||
admin, err := dataprovider.AdminExists(defaultTokenAuthUser)
|
admin, err := dataprovider.AdminExists(defaultTokenAuthUser)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
admin.Password = defaultTokenAuthPass
|
admin.Password = defaultTokenAuthPass
|
||||||
err = dataprovider.UpdateAdmin(&admin)
|
err = dataprovider.UpdateAdmin(&admin, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1506,8 +1506,8 @@ func TestUserRedactedPassword(t *testing.T) {
|
||||||
u.FsConfig.S3Config.StorageClass = "Standard"
|
u.FsConfig.S3Config.StorageClass = "Standard"
|
||||||
_, resp, err := httpdtest.AddUser(u, http.StatusBadRequest)
|
_, resp, err := httpdtest.AddUser(u, http.StatusBadRequest)
|
||||||
assert.NoError(t, err, string(resp))
|
assert.NoError(t, err, string(resp))
|
||||||
assert.Contains(t, string(resp), "invalid access_secret")
|
assert.Contains(t, string(resp), "cannot save a user with a redacted secret")
|
||||||
err = dataprovider.AddUser(&u)
|
err = dataprovider.AddUser(&u, "", "")
|
||||||
if assert.Error(t, err) {
|
if assert.Error(t, err) {
|
||||||
assert.Contains(t, err.Error(), "cannot save a user with a redacted secret")
|
assert.Contains(t, err.Error(), "cannot save a user with a redacted secret")
|
||||||
}
|
}
|
||||||
|
@ -1534,7 +1534,7 @@ func TestUserRedactedPassword(t *testing.T) {
|
||||||
|
|
||||||
user.Password = defaultPassword
|
user.Password = defaultPassword
|
||||||
user.VirtualFolders = append(user.VirtualFolders, vfolder)
|
user.VirtualFolders = append(user.VirtualFolders, vfolder)
|
||||||
err = dataprovider.UpdateUser(&user)
|
err = dataprovider.UpdateUser(&user, "", "")
|
||||||
if assert.Error(t, err) {
|
if assert.Error(t, err) {
|
||||||
assert.Contains(t, err.Error(), "cannot save a user with a redacted secret")
|
assert.Contains(t, err.Error(), "cannot save a user with a redacted secret")
|
||||||
}
|
}
|
||||||
|
@ -3547,7 +3547,7 @@ func TestSaveErrors(t *testing.T) {
|
||||||
Protocols: []string{common.ProtocolSSH, common.ProtocolHTTP},
|
Protocols: []string{common.ProtocolSSH, common.ProtocolHTTP},
|
||||||
}
|
}
|
||||||
user.Filters.RecoveryCodes = recoveryCodes
|
user.Filters.RecoveryCodes = recoveryCodes
|
||||||
err = dataprovider.UpdateUser(&user)
|
err = dataprovider.UpdateUser(&user, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -3568,7 +3568,7 @@ func TestSaveErrors(t *testing.T) {
|
||||||
Secret: kms.NewPlainSecret(secret),
|
Secret: kms.NewPlainSecret(secret),
|
||||||
}
|
}
|
||||||
admin.Filters.RecoveryCodes = recoveryCodes
|
admin.Filters.RecoveryCodes = recoveryCodes
|
||||||
err = dataprovider.UpdateAdmin(&admin)
|
err = dataprovider.UpdateAdmin(&admin, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)
|
admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -5694,7 +5694,7 @@ func TestMFAInvalidSecret(t *testing.T) {
|
||||||
Used: false,
|
Used: false,
|
||||||
Secret: kms.NewSecret(kms.SecretStatusSecretBox, "payload", "key", user.Username),
|
Secret: kms.NewSecret(kms.SecretStatusSecretBox, "payload", "key", user.Username),
|
||||||
})
|
})
|
||||||
err = dataprovider.UpdateUser(&user)
|
err = dataprovider.UpdateUser(&user, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, user2FARecoveryCodesPath, nil)
|
req, err := http.NewRequest(http.MethodGet, user2FARecoveryCodesPath, nil)
|
||||||
|
@ -5766,7 +5766,7 @@ func TestMFAInvalidSecret(t *testing.T) {
|
||||||
Used: false,
|
Used: false,
|
||||||
Secret: kms.NewSecret(kms.SecretStatusSecretBox, "payload", "key", user.Username),
|
Secret: kms.NewSecret(kms.SecretStatusSecretBox, "payload", "key", user.Username),
|
||||||
})
|
})
|
||||||
err = dataprovider.UpdateAdmin(&admin)
|
err = dataprovider.UpdateAdmin(&admin, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
csrfToken, err = getCSRFToken(httpBaseURL + webLoginPath)
|
csrfToken, err = getCSRFToken(httpBaseURL + webLoginPath)
|
||||||
|
@ -6592,7 +6592,7 @@ func TestAdminHandlingWithAPIKeys(t *testing.T) {
|
||||||
dbAdmin, err := dataprovider.AdminExists(defaultTokenAuthUser)
|
dbAdmin, err := dataprovider.AdminExists(defaultTokenAuthUser)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
dbAdmin.Filters.AllowAPIKeyAuth = false
|
dbAdmin.Filters.AllowAPIKeyAuth = false
|
||||||
err = dataprovider.UpdateAdmin(&dbAdmin)
|
err = dataprovider.UpdateAdmin(&dbAdmin, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
sysAdmin, _, err = httpdtest.GetAdminByUsername(defaultTokenAuthUser, http.StatusOK)
|
sysAdmin, _, err = httpdtest.GetAdminByUsername(defaultTokenAuthUser, http.StatusOK)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -9669,7 +9669,7 @@ func TestWebAdminSetupMock(t *testing.T) {
|
||||||
admins, err := dataprovider.GetAdmins(100, 0, dataprovider.OrderASC)
|
admins, err := dataprovider.GetAdmins(100, 0, dataprovider.OrderASC)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
for _, admin := range admins {
|
for _, admin := range admins {
|
||||||
err = dataprovider.DeleteAdmin(admin.Username)
|
err = dataprovider.DeleteAdmin(admin.Username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
// close the provider and initializes it without creating the default admin
|
// close the provider and initializes it without creating the default admin
|
||||||
|
|
|
@ -430,6 +430,96 @@ func TestInvalidToken(t *testing.T) {
|
||||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
loadData(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
loadDataFromRequest(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
addUser(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
disableUser2FA(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
updateUser(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
deleteUser(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
handleWebRestore(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
handleWebAddUserPost(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
handleWebUpdateUserPost(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
updateFolder(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
deleteFolder(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
handleWebUpdateFolderPost(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
addAdmin(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
disableAdmin2FA(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
addAPIKey(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
updateAPIKey(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
deleteAPIKey(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
handleWebAddAdminPost(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "invalid token claims")
|
||||||
|
|
||||||
server := httpdServer{}
|
server := httpdServer{}
|
||||||
server.initializeRouter()
|
server.initializeRouter()
|
||||||
rr = httptest.NewRecorder()
|
rr = httptest.NewRecorder()
|
||||||
|
@ -493,7 +583,7 @@ func TestRetentionInvalidTokenClaims(t *testing.T) {
|
||||||
user.Permissions = make(map[string][]string)
|
user.Permissions = make(map[string][]string)
|
||||||
user.Permissions["/"] = []string{dataprovider.PermAny}
|
user.Permissions["/"] = []string{dataprovider.PermAny}
|
||||||
user.Filters.AllowAPIKeyAuth = true
|
user.Filters.AllowAPIKeyAuth = true
|
||||||
err := dataprovider.AddUser(&user)
|
err := dataprovider.AddUser(&user, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
folderRetention := []common.FolderRetention{
|
folderRetention := []common.FolderRetention{
|
||||||
{
|
{
|
||||||
|
@ -516,7 +606,7 @@ func TestRetentionInvalidTokenClaims(t *testing.T) {
|
||||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(username)
|
err = dataprovider.DeleteUser(username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -691,7 +781,7 @@ func TestCreateTokenError(t *testing.T) {
|
||||||
user.Permissions = make(map[string][]string)
|
user.Permissions = make(map[string][]string)
|
||||||
user.Permissions["/"] = []string{dataprovider.PermAny}
|
user.Permissions["/"] = []string{dataprovider.PermAny}
|
||||||
user.Filters.AllowAPIKeyAuth = true
|
user.Filters.AllowAPIKeyAuth = true
|
||||||
err = dataprovider.AddUser(&user)
|
err = dataprovider.AddUser(&user, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
rr = httptest.NewRecorder()
|
rr = httptest.NewRecorder()
|
||||||
|
@ -708,20 +798,20 @@ func TestCreateTokenError(t *testing.T) {
|
||||||
err = authenticateUserWithAPIKey(username, "", server.tokenAuth, req)
|
err = authenticateUserWithAPIKey(username, "", server.tokenAuth, req)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(username)
|
err = dataprovider.DeleteUser(username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
admin.Username += "1"
|
admin.Username += "1"
|
||||||
admin.Status = 1
|
admin.Status = 1
|
||||||
admin.Filters.AllowAPIKeyAuth = true
|
admin.Filters.AllowAPIKeyAuth = true
|
||||||
admin.Permissions = []string{dataprovider.PermAdminAny}
|
admin.Permissions = []string{dataprovider.PermAdminAny}
|
||||||
err = dataprovider.AddAdmin(&admin)
|
err = dataprovider.AddAdmin(&admin, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = authenticateAdminWithAPIKey(admin.Username, "", server.tokenAuth, req)
|
err = authenticateAdminWithAPIKey(admin.Username, "", server.tokenAuth, req)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
err = dataprovider.DeleteAdmin(admin.Username)
|
err = dataprovider.DeleteAdmin(admin.Username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -858,7 +948,7 @@ func TestCookieExpiration(t *testing.T) {
|
||||||
assert.Empty(t, cookie)
|
assert.Empty(t, cookie)
|
||||||
|
|
||||||
admin.Status = 0
|
admin.Status = 0
|
||||||
err = dataprovider.AddAdmin(&admin)
|
err = dataprovider.AddAdmin(&admin, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
req, _ = http.NewRequest(http.MethodGet, tokenPath, nil)
|
req, _ = http.NewRequest(http.MethodGet, tokenPath, nil)
|
||||||
ctx = jwtauth.NewContext(req.Context(), token, nil)
|
ctx = jwtauth.NewContext(req.Context(), token, nil)
|
||||||
|
@ -868,7 +958,7 @@ func TestCookieExpiration(t *testing.T) {
|
||||||
|
|
||||||
admin.Status = 1
|
admin.Status = 1
|
||||||
admin.Filters.AllowList = []string{"172.16.1.0/24"}
|
admin.Filters.AllowList = []string{"172.16.1.0/24"}
|
||||||
err = dataprovider.UpdateAdmin(&admin)
|
err = dataprovider.UpdateAdmin(&admin, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
req, _ = http.NewRequest(http.MethodGet, tokenPath, nil)
|
req, _ = http.NewRequest(http.MethodGet, tokenPath, nil)
|
||||||
ctx = jwtauth.NewContext(req.Context(), token, nil)
|
ctx = jwtauth.NewContext(req.Context(), token, nil)
|
||||||
|
@ -900,7 +990,7 @@ func TestCookieExpiration(t *testing.T) {
|
||||||
cookie = rr.Header().Get("Set-Cookie")
|
cookie = rr.Header().Get("Set-Cookie")
|
||||||
assert.True(t, strings.HasPrefix(cookie, "jwt="))
|
assert.True(t, strings.HasPrefix(cookie, "jwt="))
|
||||||
|
|
||||||
err = dataprovider.DeleteAdmin(admin.Username)
|
err = dataprovider.DeleteAdmin(admin.Username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
// now check client cookie expiration
|
// now check client cookie expiration
|
||||||
username := "client"
|
username := "client"
|
||||||
|
@ -932,7 +1022,7 @@ func TestCookieExpiration(t *testing.T) {
|
||||||
cookie = rr.Header().Get("Set-Cookie")
|
cookie = rr.Header().Get("Set-Cookie")
|
||||||
assert.Empty(t, cookie)
|
assert.Empty(t, cookie)
|
||||||
// the password will be hashed and so the signature will change
|
// the password will be hashed and so the signature will change
|
||||||
err = dataprovider.AddUser(&user)
|
err = dataprovider.AddUser(&user, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
req, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)
|
req, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)
|
||||||
ctx = jwtauth.NewContext(req.Context(), token, nil)
|
ctx = jwtauth.NewContext(req.Context(), token, nil)
|
||||||
|
@ -943,7 +1033,7 @@ func TestCookieExpiration(t *testing.T) {
|
||||||
user, err = dataprovider.UserExists(user.Username)
|
user, err = dataprovider.UserExists(user.Username)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user.Filters.AllowedIP = []string{"172.16.4.0/24"}
|
user.Filters.AllowedIP = []string{"172.16.4.0/24"}
|
||||||
err = dataprovider.UpdateUser(&user)
|
err = dataprovider.UpdateUser(&user, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
user, err = dataprovider.UserExists(user.Username)
|
user, err = dataprovider.UserExists(user.Username)
|
||||||
|
@ -971,7 +1061,7 @@ func TestCookieExpiration(t *testing.T) {
|
||||||
cookie = rr.Header().Get("Set-Cookie")
|
cookie = rr.Header().Get("Set-Cookie")
|
||||||
assert.NotEmpty(t, cookie)
|
assert.NotEmpty(t, cookie)
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(user.Username)
|
err = dataprovider.DeleteUser(user.Username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1239,7 +1329,7 @@ func TestProxyHeaders(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := dataprovider.AddAdmin(&admin)
|
err := dataprovider.AddAdmin(&admin, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
testIP := "10.29.1.9"
|
testIP := "10.29.1.9"
|
||||||
|
@ -1327,7 +1417,7 @@ func TestProxyHeaders(t *testing.T) {
|
||||||
cookie = rr.Header().Get("Set-Cookie")
|
cookie = rr.Header().Get("Set-Cookie")
|
||||||
assert.NotContains(t, cookie, "Secure")
|
assert.NotContains(t, cookie, "Secure")
|
||||||
|
|
||||||
err = dataprovider.DeleteAdmin(username)
|
err = dataprovider.DeleteAdmin(username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -231,7 +231,7 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.Filters.RecoveryCodes[idx].Used = true
|
user.Filters.RecoveryCodes[idx].Used = true
|
||||||
err = dataprovider.UpdateUser(&user)
|
err = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn(logSender, "", "unable to set the recovery code %#v as used: %v", recoveryCode, err)
|
logger.Warn(logSender, "", "unable to set the recovery code %#v as used: %v", recoveryCode, err)
|
||||||
renderClientInternalServerErrorPage(w, r, errors.New("unable to set the recovery code as used"))
|
renderClientInternalServerErrorPage(w, r, errors.New("unable to set the recovery code as used"))
|
||||||
|
@ -332,7 +332,7 @@ func (s *httpdServer) handleWebAdminTwoFactorRecoveryPost(w http.ResponseWriter,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
admin.Filters.RecoveryCodes[idx].Used = true
|
admin.Filters.RecoveryCodes[idx].Used = true
|
||||||
err = dataprovider.UpdateAdmin(&admin)
|
err = dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn(logSender, "", "unable to set the recovery code %#v as used: %v", recoveryCode, err)
|
logger.Warn(logSender, "", "unable to set the recovery code %#v as used: %v", recoveryCode, err)
|
||||||
renderInternalServerErrorPage(w, r, errors.New("unable to set the recovery code as used"))
|
renderInternalServerErrorPage(w, r, errors.New("unable to set the recovery code as used"))
|
||||||
|
@ -472,7 +472,7 @@ func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Req
|
||||||
Status: 1,
|
Status: 1,
|
||||||
Permissions: []string{dataprovider.PermAdminAny},
|
Permissions: []string{dataprovider.PermAdminAny},
|
||||||
}
|
}
|
||||||
err = dataprovider.AddAdmin(&admin)
|
err = dataprovider.AddAdmin(&admin, username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderAdminSetupPage(w, r, username, err.Error())
|
renderAdminSetupPage(w, r, username, err.Error())
|
||||||
return
|
return
|
||||||
|
|
|
@ -1201,7 +1201,7 @@ func handleWebAdminProfilePost(w http.ResponseWriter, r *http.Request) {
|
||||||
admin.Filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
|
admin.Filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
|
||||||
admin.Email = r.Form.Get("email")
|
admin.Email = r.Form.Get("email")
|
||||||
admin.Description = r.Form.Get("description")
|
admin.Description = r.Form.Get("description")
|
||||||
err = dataprovider.UpdateAdmin(&admin)
|
err = dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderProfilePage(w, r, err.Error())
|
renderProfilePage(w, r, err.Error())
|
||||||
return
|
return
|
||||||
|
@ -1225,7 +1225,12 @@ func handleWebMaintenance(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func handleWebRestore(w http.ResponseWriter, r *http.Request) {
|
func handleWebRestore(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, MaxRestoreSize)
|
r.Body = http.MaxBytesReader(w, r.Body, MaxRestoreSize)
|
||||||
err := r.ParseMultipartForm(MaxRestoreSize)
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
renderBadRequestPage(w, r, errors.New("invalid token claims"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.ParseMultipartForm(MaxRestoreSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderMaintenancePage(w, r, err.Error())
|
renderMaintenancePage(w, r, err.Error())
|
||||||
return
|
return
|
||||||
|
@ -1260,7 +1265,7 @@ func handleWebRestore(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := restoreBackup(backupContent, "", scanQuota, restoreMode); err != nil {
|
if err := restoreBackup(backupContent, "", scanQuota, restoreMode, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
|
||||||
renderMaintenancePage(w, r, err.Error())
|
renderMaintenancePage(w, r, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1327,6 +1332,11 @@ func handleWebUpdateAdminGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func handleWebAddAdminPost(w http.ResponseWriter, r *http.Request) {
|
func handleWebAddAdminPost(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
renderBadRequestPage(w, r, errors.New("invalid token claims"))
|
||||||
|
return
|
||||||
|
}
|
||||||
admin, err := getAdminFromPostFields(r)
|
admin, err := getAdminFromPostFields(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderAddUpdateAdminPage(w, r, &admin, err.Error(), true)
|
renderAddUpdateAdminPage(w, r, &admin, err.Error(), true)
|
||||||
|
@ -1336,7 +1346,7 @@ func handleWebAddAdminPost(w http.ResponseWriter, r *http.Request) {
|
||||||
renderForbiddenPage(w, r, err.Error())
|
renderForbiddenPage(w, r, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = dataprovider.AddAdmin(&admin)
|
err = dataprovider.AddAdmin(&admin, claims.Username, util.GetIPFromRemoteAddress(r.Method))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderAddUpdateAdminPage(w, r, &admin, err.Error(), true)
|
renderAddUpdateAdminPage(w, r, &admin, err.Error(), true)
|
||||||
return
|
return
|
||||||
|
@ -1388,7 +1398,7 @@ func handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = dataprovider.UpdateAdmin(&updatedAdmin)
|
err = dataprovider.UpdateAdmin(&updatedAdmin, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderAddUpdateAdminPage(w, r, &admin, err.Error(), false)
|
renderAddUpdateAdminPage(w, r, &admin, err.Error(), false)
|
||||||
return
|
return
|
||||||
|
@ -1595,6 +1605,11 @@ func handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func handleWebAddUserPost(w http.ResponseWriter, r *http.Request) {
|
func handleWebAddUserPost(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
renderBadRequestPage(w, r, errors.New("invalid token claims"))
|
||||||
|
return
|
||||||
|
}
|
||||||
user, err := getUserFromPostFields(r)
|
user, err := getUserFromPostFields(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderUserPage(w, r, &user, userPageModeAdd, err.Error())
|
renderUserPage(w, r, &user, userPageModeAdd, err.Error())
|
||||||
|
@ -1604,7 +1619,7 @@ func handleWebAddUserPost(w http.ResponseWriter, r *http.Request) {
|
||||||
renderForbiddenPage(w, r, err.Error())
|
renderForbiddenPage(w, r, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = dataprovider.AddUser(&user)
|
err = dataprovider.AddUser(&user, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
|
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
|
||||||
} else {
|
} else {
|
||||||
|
@ -1614,6 +1629,11 @@ func handleWebAddUserPost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
|
func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
renderBadRequestPage(w, r, errors.New("invalid token claims"))
|
||||||
|
return
|
||||||
|
}
|
||||||
username := getURLParam(r, "username")
|
username := getURLParam(r, "username")
|
||||||
user, err := dataprovider.UserExists(username)
|
user, err := dataprovider.UserExists(username)
|
||||||
if _, ok := err.(*util.RecordNotFoundError); ok {
|
if _, ok := err.(*util.RecordNotFoundError); ok {
|
||||||
|
@ -1644,7 +1664,7 @@ func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
|
||||||
user.FsConfig.AzBlobConfig.SASURL, user.FsConfig.GCSConfig.Credentials, user.FsConfig.CryptConfig.Passphrase,
|
user.FsConfig.AzBlobConfig.SASURL, user.FsConfig.GCSConfig.Credentials, user.FsConfig.CryptConfig.Passphrase,
|
||||||
user.FsConfig.SFTPConfig.Password, user.FsConfig.SFTPConfig.PrivateKey)
|
user.FsConfig.SFTPConfig.Password, user.FsConfig.SFTPConfig.PrivateKey)
|
||||||
|
|
||||||
err = dataprovider.UpdateUser(&updatedUser)
|
err = dataprovider.UpdateUser(&updatedUser, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if len(r.Form.Get("disconnect")) > 0 {
|
if len(r.Form.Get("disconnect")) > 0 {
|
||||||
disconnectUser(user.Username)
|
disconnectUser(user.Username)
|
||||||
|
@ -1724,6 +1744,11 @@ func handleWebUpdateFolderGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func handleWebUpdateFolderPost(w http.ResponseWriter, r *http.Request) {
|
func handleWebUpdateFolderPost(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
renderBadRequestPage(w, r, errors.New("invalid token claims"))
|
||||||
|
return
|
||||||
|
}
|
||||||
name := getURLParam(r, "name")
|
name := getURLParam(r, "name")
|
||||||
folder, err := dataprovider.GetFolderByName(name)
|
folder, err := dataprovider.GetFolderByName(name)
|
||||||
if _, ok := err.(*util.RecordNotFoundError); ok {
|
if _, ok := err.(*util.RecordNotFoundError); ok {
|
||||||
|
@ -1760,7 +1785,7 @@ func handleWebUpdateFolderPost(w http.ResponseWriter, r *http.Request) {
|
||||||
folder.FsConfig.AzBlobConfig.SASURL, folder.FsConfig.GCSConfig.Credentials, folder.FsConfig.CryptConfig.Passphrase,
|
folder.FsConfig.AzBlobConfig.SASURL, folder.FsConfig.GCSConfig.Credentials, folder.FsConfig.CryptConfig.Passphrase,
|
||||||
folder.FsConfig.SFTPConfig.Password, folder.FsConfig.SFTPConfig.PrivateKey)
|
folder.FsConfig.SFTPConfig.Password, folder.FsConfig.SFTPConfig.PrivateKey)
|
||||||
|
|
||||||
err = dataprovider.UpdateFolder(updatedFolder, folder.Users)
|
err = dataprovider.UpdateFolder(updatedFolder, folder.Users, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderFolderPage(w, r, folder, folderPageModeUpdate, err.Error())
|
renderFolderPage(w, r, folder, folderPageModeUpdate, err.Error())
|
||||||
return
|
return
|
||||||
|
|
|
@ -700,7 +700,7 @@ func handleWebClientProfilePost(w http.ResponseWriter, r *http.Request) {
|
||||||
user.Email = r.Form.Get("email")
|
user.Email = r.Form.Get("email")
|
||||||
user.Description = r.Form.Get("description")
|
user.Description = r.Form.Get("description")
|
||||||
}
|
}
|
||||||
err = dataprovider.UpdateUser(&user)
|
err = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderClientProfilePage(w, r, err.Error())
|
renderClientProfilePage(w, r, err.Error())
|
||||||
return
|
return
|
||||||
|
|
|
@ -20,7 +20,8 @@ import (
|
||||||
// NotifierConfig defines configuration parameters for notifiers plugins
|
// NotifierConfig defines configuration parameters for notifiers plugins
|
||||||
type NotifierConfig struct {
|
type NotifierConfig struct {
|
||||||
FsEvents []string `json:"fs_events" mapstructure:"fs_events"`
|
FsEvents []string `json:"fs_events" mapstructure:"fs_events"`
|
||||||
UserEvents []string `json:"user_events" mapstructure:"user_events"`
|
ProviderEvents []string `json:"provider_events" mapstructure:"provider_events"`
|
||||||
|
ProviderObjects []string `json:"provider_objects" mapstructure:"provider_objects"`
|
||||||
RetryMaxTime int `json:"retry_max_time" mapstructure:"retry_max_time"`
|
RetryMaxTime int `json:"retry_max_time" mapstructure:"retry_max_time"`
|
||||||
RetryQueueMaxSize int `json:"retry_queue_max_size" mapstructure:"retry_queue_max_size"`
|
RetryQueueMaxSize int `json:"retry_queue_max_size" mapstructure:"retry_queue_max_size"`
|
||||||
}
|
}
|
||||||
|
@ -29,7 +30,7 @@ func (c *NotifierConfig) hasActions() bool {
|
||||||
if len(c.FsEvents) > 0 {
|
if len(c.FsEvents) > 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if len(c.UserEvents) > 0 {
|
if len(c.ProviderEvents) > 0 && len(c.ProviderObjects) > 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -37,11 +38,13 @@ func (c *NotifierConfig) hasActions() bool {
|
||||||
|
|
||||||
type eventsQueue struct {
|
type eventsQueue struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
fsEvents []*proto.FsEvent
|
fsEvents []*proto.FsEvent
|
||||||
userEvents []*proto.UserEvent
|
providerEvents []*proto.ProviderEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *eventsQueue) addFsEvent(timestamp time.Time, action, username, fsPath, fsTargetPath, sshCmd, protocol string, fileSize int64, status int) {
|
func (q *eventsQueue) addFsEvent(timestamp time.Time, action, username, fsPath, fsTargetPath, sshCmd, protocol, ip string,
|
||||||
|
fileSize int64, status int,
|
||||||
|
) {
|
||||||
q.Lock()
|
q.Lock()
|
||||||
defer q.Unlock()
|
defer q.Unlock()
|
||||||
|
|
||||||
|
@ -54,18 +57,25 @@ func (q *eventsQueue) addFsEvent(timestamp time.Time, action, username, fsPath,
|
||||||
SshCmd: sshCmd,
|
SshCmd: sshCmd,
|
||||||
FileSize: fileSize,
|
FileSize: fileSize,
|
||||||
Protocol: protocol,
|
Protocol: protocol,
|
||||||
|
Ip: ip,
|
||||||
Status: int32(status),
|
Status: int32(status),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *eventsQueue) addUserEvent(timestamp time.Time, action string, userAsJSON []byte) {
|
func (q *eventsQueue) addProviderEvent(timestamp time.Time, action, username, objectType, objectName, ip string,
|
||||||
|
objectAsJSON []byte,
|
||||||
|
) {
|
||||||
q.Lock()
|
q.Lock()
|
||||||
defer q.Unlock()
|
defer q.Unlock()
|
||||||
|
|
||||||
q.userEvents = append(q.userEvents, &proto.UserEvent{
|
q.providerEvents = append(q.providerEvents, &proto.ProviderEvent{
|
||||||
Timestamp: timestamppb.New(timestamp),
|
Timestamp: timestamppb.New(timestamp),
|
||||||
Action: action,
|
Action: action,
|
||||||
User: userAsJSON,
|
ObjectType: objectType,
|
||||||
|
Username: username,
|
||||||
|
Ip: ip,
|
||||||
|
ObjectName: objectName,
|
||||||
|
ObjectData: objectAsJSON,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,17 +94,17 @@ func (q *eventsQueue) popFsEvent() *proto.FsEvent {
|
||||||
return ev
|
return ev
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *eventsQueue) popUserEvent() *proto.UserEvent {
|
func (q *eventsQueue) popProviderEvent() *proto.ProviderEvent {
|
||||||
q.Lock()
|
q.Lock()
|
||||||
defer q.Unlock()
|
defer q.Unlock()
|
||||||
|
|
||||||
if len(q.userEvents) == 0 {
|
if len(q.providerEvents) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
truncLen := len(q.userEvents) - 1
|
truncLen := len(q.providerEvents) - 1
|
||||||
ev := q.userEvents[truncLen]
|
ev := q.providerEvents[truncLen]
|
||||||
q.userEvents[truncLen] = nil
|
q.providerEvents[truncLen] = nil
|
||||||
q.userEvents = q.userEvents[:truncLen]
|
q.providerEvents = q.providerEvents[:truncLen]
|
||||||
|
|
||||||
return ev
|
return ev
|
||||||
}
|
}
|
||||||
|
@ -103,7 +113,7 @@ func (q *eventsQueue) getSize() int {
|
||||||
q.RLock()
|
q.RLock()
|
||||||
defer q.RUnlock()
|
defer q.RUnlock()
|
||||||
|
|
||||||
return len(q.userEvents) + len(q.fsEvents)
|
return len(q.providerEvents) + len(q.fsEvents)
|
||||||
}
|
}
|
||||||
|
|
||||||
type notifierPlugin struct {
|
type notifierPlugin struct {
|
||||||
|
@ -194,7 +204,7 @@ func (p *notifierPlugin) canQueueEvent(timestamp time.Time) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *notifierPlugin) notifyFsAction(timestamp time.Time, action, username, fsPath, fsTargetPath, sshCmd,
|
func (p *notifierPlugin) notifyFsAction(timestamp time.Time, action, username, fsPath, fsTargetPath, sshCmd,
|
||||||
protocol string, fileSize int64, errAction error) {
|
protocol, ip, virtualPath, virtualTargetPath string, fileSize int64, errAction error) {
|
||||||
if !util.IsStringInSlice(action, p.config.NotifierOptions.FsEvents) {
|
if !util.IsStringInSlice(action, p.config.NotifierOptions.FsEvents) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -204,40 +214,47 @@ func (p *notifierPlugin) notifyFsAction(timestamp time.Time, action, username, f
|
||||||
if errAction != nil {
|
if errAction != nil {
|
||||||
status = 0
|
status = 0
|
||||||
}
|
}
|
||||||
p.sendFsEvent(timestamp, action, username, fsPath, fsTargetPath, sshCmd, protocol, fileSize, status)
|
p.sendFsEvent(timestamp, action, username, fsPath, fsTargetPath, sshCmd, protocol, ip, virtualPath, virtualTargetPath,
|
||||||
|
fileSize, status)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *notifierPlugin) notifyUserAction(timestamp time.Time, action string, user Renderer) {
|
func (p *notifierPlugin) notifyProviderAction(timestamp time.Time, action, username, objectType, objectName, ip string,
|
||||||
if !util.IsStringInSlice(action, p.config.NotifierOptions.UserEvents) {
|
object Renderer,
|
||||||
|
) {
|
||||||
|
if !util.IsStringInSlice(action, p.config.NotifierOptions.ProviderEvents) ||
|
||||||
|
!util.IsStringInSlice(objectType, p.config.NotifierOptions.ProviderObjects) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
userAsJSON, err := user.RenderAsJSON(action != "delete")
|
objectAsJSON, err := object.RenderAsJSON(action != "delete")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn(logSender, "", "unable to render user as json for action %v: %v", action, err)
|
logger.Warn(logSender, "", "unable to render user as json for action %v: %v", action, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
p.sendUserEvent(timestamp, action, userAsJSON)
|
p.sendProviderEvent(timestamp, action, username, objectType, objectName, ip, objectAsJSON)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *notifierPlugin) sendFsEvent(timestamp time.Time, action, username, fsPath, fsTargetPath, sshCmd,
|
func (p *notifierPlugin) sendFsEvent(timestamp time.Time, action, username, fsPath, fsTargetPath, sshCmd,
|
||||||
protocol string, fileSize int64, status int) {
|
protocol, ip, virtualPath, virtualTargetPath string, fileSize int64, status int) {
|
||||||
if err := p.notifier.NotifyFsEvent(timestamp, action, username, fsPath, fsTargetPath, sshCmd, protocol, fileSize, status); err != nil {
|
if err := p.notifier.NotifyFsEvent(timestamp, action, username, fsPath, fsTargetPath, sshCmd, protocol, ip,
|
||||||
|
virtualPath, virtualTargetPath, fileSize, status); err != nil {
|
||||||
logger.Warn(logSender, "", "unable to send fs action notification to plugin %v: %v", p.config.Cmd, err)
|
logger.Warn(logSender, "", "unable to send fs action notification to plugin %v: %v", p.config.Cmd, err)
|
||||||
if p.canQueueEvent(timestamp) {
|
if p.canQueueEvent(timestamp) {
|
||||||
p.queue.addFsEvent(timestamp, action, username, fsPath, fsTargetPath, sshCmd, protocol, fileSize, status)
|
p.queue.addFsEvent(timestamp, action, username, fsPath, fsTargetPath, sshCmd, protocol, ip, fileSize, status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *notifierPlugin) sendUserEvent(timestamp time.Time, action string, userAsJSON []byte) {
|
func (p *notifierPlugin) sendProviderEvent(timestamp time.Time, action, username, objectType, objectName, ip string,
|
||||||
if err := p.notifier.NotifyUserEvent(timestamp, action, userAsJSON); err != nil {
|
objectAsJSON []byte,
|
||||||
|
) {
|
||||||
|
if err := p.notifier.NotifyProviderEvent(timestamp, action, username, objectType, objectName, ip, objectAsJSON); err != nil {
|
||||||
logger.Warn(logSender, "", "unable to send user action notification to plugin %v: %v", p.config.Cmd, err)
|
logger.Warn(logSender, "", "unable to send user action notification to plugin %v: %v", p.config.Cmd, err)
|
||||||
if p.canQueueEvent(timestamp) {
|
if p.canQueueEvent(timestamp) {
|
||||||
p.queue.addUserEvent(timestamp, action, userAsJSON)
|
p.queue.addProviderEvent(timestamp, action, username, objectType, objectName, ip, objectAsJSON)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -251,14 +268,15 @@ func (p *notifierPlugin) sendQueuedEvents() {
|
||||||
fsEv := p.queue.popFsEvent()
|
fsEv := p.queue.popFsEvent()
|
||||||
for fsEv != nil {
|
for fsEv != nil {
|
||||||
go p.sendFsEvent(fsEv.Timestamp.AsTime(), fsEv.Action, fsEv.Username, fsEv.FsPath, fsEv.FsTargetPath,
|
go p.sendFsEvent(fsEv.Timestamp.AsTime(), fsEv.Action, fsEv.Username, fsEv.FsPath, fsEv.FsTargetPath,
|
||||||
fsEv.SshCmd, fsEv.Protocol, fsEv.FileSize, int(fsEv.Status))
|
fsEv.SshCmd, fsEv.Protocol, fsEv.Ip, fsEv.VirtualPath, fsEv.VirtualTargetPath, fsEv.FileSize, int(fsEv.Status))
|
||||||
fsEv = p.queue.popFsEvent()
|
fsEv = p.queue.popFsEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
userEv := p.queue.popUserEvent()
|
providerEv := p.queue.popProviderEvent()
|
||||||
for userEv != nil {
|
for providerEv != nil {
|
||||||
go p.sendUserEvent(userEv.Timestamp.AsTime(), userEv.Action, userEv.User)
|
go p.sendProviderEvent(providerEv.Timestamp.AsTime(), providerEv.Action, providerEv.Username, providerEv.ObjectType,
|
||||||
userEv = p.queue.popUserEvent()
|
providerEv.ObjectName, providerEv.Ip, providerEv.ObjectData)
|
||||||
|
providerEv = p.queue.popProviderEvent()
|
||||||
}
|
}
|
||||||
logger.Debug(logSender, "", "queued events sent for notifier %#v, new events size: %v", p.config.Cmd, p.queue.getSize())
|
logger.Debug(logSender, "", "queued events sent for notifier %#v, new events size: %v", p.config.Cmd, p.queue.getSize())
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,34 +20,43 @@ type GRPCClient struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyFsEvent implements the Notifier interface
|
// NotifyFsEvent implements the Notifier interface
|
||||||
func (c *GRPCClient) NotifyFsEvent(timestamp time.Time, action, username, fsPath, fsTargetPath, sshCmd, protocol string, fileSize int64, status int) error {
|
func (c *GRPCClient) NotifyFsEvent(timestamp time.Time, action, username, fsPath, fsTargetPath, sshCmd, protocol, ip,
|
||||||
|
virtualPath, virtualTargetPath string, fileSize int64, status int,
|
||||||
|
) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
_, err := c.client.SendFsEvent(ctx, &proto.FsEvent{
|
_, err := c.client.SendFsEvent(ctx, &proto.FsEvent{
|
||||||
Timestamp: timestamppb.New(timestamp),
|
Timestamp: timestamppb.New(timestamp),
|
||||||
Action: action,
|
Action: action,
|
||||||
Username: username,
|
Username: username,
|
||||||
FsPath: fsPath,
|
FsPath: fsPath,
|
||||||
FsTargetPath: fsTargetPath,
|
FsTargetPath: fsTargetPath,
|
||||||
SshCmd: sshCmd,
|
SshCmd: sshCmd,
|
||||||
FileSize: fileSize,
|
FileSize: fileSize,
|
||||||
Protocol: protocol,
|
Protocol: protocol,
|
||||||
Status: int32(status),
|
Ip: ip,
|
||||||
|
Status: int32(status),
|
||||||
|
VirtualPath: virtualPath,
|
||||||
|
VirtualTargetPath: virtualTargetPath,
|
||||||
})
|
})
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyUserEvent implements the Notifier interface
|
// NotifyProviderEvent implements the Notifier interface
|
||||||
func (c *GRPCClient) NotifyUserEvent(timestamp time.Time, action string, user []byte) error {
|
func (c *GRPCClient) NotifyProviderEvent(timestamp time.Time, action, username, objectType, objectName, ip string, object []byte) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
_, err := c.client.SendUserEvent(ctx, &proto.UserEvent{
|
_, err := c.client.SendProviderEvent(ctx, &proto.ProviderEvent{
|
||||||
Timestamp: timestamppb.New(timestamp),
|
Timestamp: timestamppb.New(timestamp),
|
||||||
Action: action,
|
Action: action,
|
||||||
User: user,
|
ObjectType: objectType,
|
||||||
|
Username: username,
|
||||||
|
Ip: ip,
|
||||||
|
ObjectName: objectName,
|
||||||
|
ObjectData: object,
|
||||||
})
|
})
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
@ -61,12 +70,13 @@ type GRPCServer struct {
|
||||||
// SendFsEvent implements the serve side fs notify method
|
// SendFsEvent implements the serve side fs notify method
|
||||||
func (s *GRPCServer) SendFsEvent(ctx context.Context, req *proto.FsEvent) (*emptypb.Empty, error) {
|
func (s *GRPCServer) SendFsEvent(ctx context.Context, req *proto.FsEvent) (*emptypb.Empty, error) {
|
||||||
err := s.Impl.NotifyFsEvent(req.Timestamp.AsTime(), req.Action, req.Username, req.FsPath, req.FsTargetPath, req.SshCmd,
|
err := s.Impl.NotifyFsEvent(req.Timestamp.AsTime(), req.Action, req.Username, req.FsPath, req.FsTargetPath, req.SshCmd,
|
||||||
req.Protocol, req.FileSize, int(req.Status))
|
req.Protocol, req.Ip, req.VirtualPath, req.VirtualTargetPath, req.FileSize, int(req.Status))
|
||||||
return &emptypb.Empty{}, err
|
return &emptypb.Empty{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendUserEvent implements the serve side user notify method
|
// SendProviderEvent implements the serve side provider event notify method
|
||||||
func (s *GRPCServer) SendUserEvent(ctx context.Context, req *proto.UserEvent) (*emptypb.Empty, error) {
|
func (s *GRPCServer) SendProviderEvent(ctx context.Context, req *proto.ProviderEvent) (*emptypb.Empty, error) {
|
||||||
err := s.Impl.NotifyUserEvent(req.Timestamp.AsTime(), req.Action, req.User)
|
err := s.Impl.NotifyProviderEvent(req.Timestamp.AsTime(), req.Action, req.Username, req.ObjectType, req.ObjectName,
|
||||||
|
req.Ip, req.ObjectData)
|
||||||
return &emptypb.Empty{}, err
|
return &emptypb.Empty{}, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,9 +33,9 @@ var PluginMap = map[string]plugin.Plugin{
|
||||||
|
|
||||||
// Notifier defines the interface for notifiers plugins
|
// Notifier defines the interface for notifiers plugins
|
||||||
type Notifier interface {
|
type Notifier interface {
|
||||||
NotifyFsEvent(timestamp time.Time, action, username, fsPath, fsTargetPath, sshCmd, protocol string,
|
NotifyFsEvent(timestamp time.Time, action, username, fsPath, fsTargetPath, sshCmd, protocol, ip,
|
||||||
fileSize int64, status int) error
|
virtualPath, virtualTargetPath string, fileSize int64, status int) error
|
||||||
NotifyUserEvent(timestamp time.Time, action string, user []byte) error
|
NotifyProviderEvent(timestamp time.Time, action, username, objectType, objectName, ip string, object []byte) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plugin defines the implementation to serve/connect to a notifier plugin
|
// Plugin defines the implementation to serve/connect to a notifier plugin
|
||||||
|
|
|
@ -31,15 +31,18 @@ type FsEvent struct {
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
|
Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
|
||||||
Action string `protobuf:"bytes,2,opt,name=action,proto3" json:"action,omitempty"`
|
Action string `protobuf:"bytes,2,opt,name=action,proto3" json:"action,omitempty"`
|
||||||
Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"`
|
Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"`
|
||||||
FsPath string `protobuf:"bytes,4,opt,name=fs_path,json=fsPath,proto3" json:"fs_path,omitempty"`
|
FsPath string `protobuf:"bytes,4,opt,name=fs_path,json=fsPath,proto3" json:"fs_path,omitempty"`
|
||||||
FsTargetPath string `protobuf:"bytes,5,opt,name=fs_target_path,json=fsTargetPath,proto3" json:"fs_target_path,omitempty"`
|
FsTargetPath string `protobuf:"bytes,5,opt,name=fs_target_path,json=fsTargetPath,proto3" json:"fs_target_path,omitempty"`
|
||||||
SshCmd string `protobuf:"bytes,6,opt,name=ssh_cmd,json=sshCmd,proto3" json:"ssh_cmd,omitempty"`
|
SshCmd string `protobuf:"bytes,6,opt,name=ssh_cmd,json=sshCmd,proto3" json:"ssh_cmd,omitempty"`
|
||||||
FileSize int64 `protobuf:"varint,7,opt,name=file_size,json=fileSize,proto3" json:"file_size,omitempty"`
|
FileSize int64 `protobuf:"varint,7,opt,name=file_size,json=fileSize,proto3" json:"file_size,omitempty"`
|
||||||
Protocol string `protobuf:"bytes,8,opt,name=protocol,proto3" json:"protocol,omitempty"`
|
Protocol string `protobuf:"bytes,8,opt,name=protocol,proto3" json:"protocol,omitempty"`
|
||||||
Status int32 `protobuf:"varint,9,opt,name=status,proto3" json:"status,omitempty"`
|
Status int32 `protobuf:"varint,9,opt,name=status,proto3" json:"status,omitempty"`
|
||||||
|
Ip string `protobuf:"bytes,10,opt,name=ip,proto3" json:"ip,omitempty"`
|
||||||
|
VirtualPath string `protobuf:"bytes,11,opt,name=virtual_path,json=virtualPath,proto3" json:"virtual_path,omitempty"`
|
||||||
|
VirtualTargetPath string `protobuf:"bytes,12,opt,name=virtual_target_path,json=virtualTargetPath,proto3" json:"virtual_target_path,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *FsEvent) Reset() {
|
func (x *FsEvent) Reset() {
|
||||||
|
@ -137,18 +140,43 @@ func (x *FsEvent) GetStatus() int32 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserEvent struct {
|
func (x *FsEvent) GetIp() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Ip
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FsEvent) GetVirtualPath() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.VirtualPath
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FsEvent) GetVirtualTargetPath() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.VirtualTargetPath
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderEvent struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
|
Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
|
||||||
Action string `protobuf:"bytes,2,opt,name=action,proto3" json:"action,omitempty"`
|
Action string `protobuf:"bytes,2,opt,name=action,proto3" json:"action,omitempty"`
|
||||||
User []byte `protobuf:"bytes,3,opt,name=user,proto3" json:"user,omitempty"` // SFTPGo user JSON serialized
|
ObjectType string `protobuf:"bytes,3,opt,name=object_type,json=objectType,proto3" json:"object_type,omitempty"`
|
||||||
|
Username string `protobuf:"bytes,4,opt,name=username,proto3" json:"username,omitempty"`
|
||||||
|
Ip string `protobuf:"bytes,5,opt,name=ip,proto3" json:"ip,omitempty"`
|
||||||
|
ObjectName string `protobuf:"bytes,6,opt,name=object_name,json=objectName,proto3" json:"object_name,omitempty"`
|
||||||
|
ObjectData []byte `protobuf:"bytes,7,opt,name=object_data,json=objectData,proto3" json:"object_data,omitempty"` // object JSON serialized
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *UserEvent) Reset() {
|
func (x *ProviderEvent) Reset() {
|
||||||
*x = UserEvent{}
|
*x = ProviderEvent{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_notifier_proto_notifier_proto_msgTypes[1]
|
mi := &file_notifier_proto_notifier_proto_msgTypes[1]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
@ -156,13 +184,13 @@ func (x *UserEvent) Reset() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *UserEvent) String() string {
|
func (x *ProviderEvent) String() string {
|
||||||
return protoimpl.X.MessageStringOf(x)
|
return protoimpl.X.MessageStringOf(x)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*UserEvent) ProtoMessage() {}
|
func (*ProviderEvent) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *UserEvent) ProtoReflect() protoreflect.Message {
|
func (x *ProviderEvent) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_notifier_proto_notifier_proto_msgTypes[1]
|
mi := &file_notifier_proto_notifier_proto_msgTypes[1]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
@ -174,28 +202,56 @@ func (x *UserEvent) ProtoReflect() protoreflect.Message {
|
||||||
return mi.MessageOf(x)
|
return mi.MessageOf(x)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deprecated: Use UserEvent.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ProviderEvent.ProtoReflect.Descriptor instead.
|
||||||
func (*UserEvent) Descriptor() ([]byte, []int) {
|
func (*ProviderEvent) Descriptor() ([]byte, []int) {
|
||||||
return file_notifier_proto_notifier_proto_rawDescGZIP(), []int{1}
|
return file_notifier_proto_notifier_proto_rawDescGZIP(), []int{1}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *UserEvent) GetTimestamp() *timestamppb.Timestamp {
|
func (x *ProviderEvent) GetTimestamp() *timestamppb.Timestamp {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.Timestamp
|
return x.Timestamp
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *UserEvent) GetAction() string {
|
func (x *ProviderEvent) GetAction() string {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.Action
|
return x.Action
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *UserEvent) GetUser() []byte {
|
func (x *ProviderEvent) GetObjectType() string {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.User
|
return x.ObjectType
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ProviderEvent) GetUsername() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Username
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ProviderEvent) GetIp() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Ip
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ProviderEvent) GetObjectName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ObjectName
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ProviderEvent) GetObjectData() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.ObjectData
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -209,7 +265,7 @@ var file_notifier_proto_notifier_proto_rawDesc = []byte{
|
||||||
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
|
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
|
||||||
0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f,
|
0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f,
|
||||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70,
|
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70,
|
||||||
0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa0, 0x02, 0x0a, 0x07, 0x46, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74,
|
0x72, 0x6f, 0x74, 0x6f, 0x22, 0x83, 0x03, 0x0a, 0x07, 0x46, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74,
|
||||||
0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20,
|
0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20,
|
||||||
0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
||||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52,
|
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52,
|
||||||
|
@ -227,24 +283,39 @@ var file_notifier_proto_notifier_proto_rawDesc = []byte{
|
||||||
0x69, 0x7a, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18,
|
0x69, 0x7a, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18,
|
||||||
0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12,
|
0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12,
|
||||||
0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52,
|
0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52,
|
||||||
0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x71, 0x0a, 0x09, 0x55, 0x73, 0x65, 0x72, 0x45,
|
0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x0a, 0x20,
|
||||||
0x76, 0x65, 0x6e, 0x74, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
|
0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12, 0x21, 0x0a, 0x0c, 0x76, 0x69, 0x72, 0x74, 0x75,
|
||||||
0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
0x61, 0x6c, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x76,
|
||||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
|
0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x50, 0x61, 0x74, 0x68, 0x12, 0x2e, 0x0a, 0x13, 0x76, 0x69,
|
||||||
0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x16,
|
0x72, 0x74, 0x75, 0x61, 0x6c, 0x5f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x70, 0x61, 0x74,
|
||||||
0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
|
0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c,
|
||||||
0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x03,
|
0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x50, 0x61, 0x74, 0x68, 0x22, 0xf0, 0x01, 0x0a, 0x0d, 0x50,
|
||||||
0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x32, 0x7c, 0x0a, 0x08, 0x4e, 0x6f,
|
0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x38, 0x0a, 0x09,
|
||||||
0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x35, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x46, 0x73,
|
0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||||
0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x73,
|
0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
|
||||||
0x45, 0x76, 0x65, 0x6e, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
|
0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d,
|
||||||
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x39, 0x0a,
|
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
|
||||||
0x0d, 0x53, 0x65, 0x6e, 0x64, 0x55, 0x73, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x10,
|
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f,
|
||||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74,
|
0x0a, 0x0b, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20,
|
||||||
0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
|
0x01, 0x28, 0x09, 0x52, 0x0a, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12,
|
||||||
0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x1b, 0x5a, 0x19, 0x73, 0x64, 0x6b, 0x2f,
|
0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28,
|
||||||
0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x2f,
|
0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69,
|
||||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12, 0x1f, 0x0a, 0x0b, 0x6f,
|
||||||
|
0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09,
|
||||||
|
0x52, 0x0a, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b,
|
||||||
|
0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x01, 0x28,
|
||||||
|
0x0c, 0x52, 0x0a, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x44, 0x61, 0x74, 0x61, 0x32, 0x84, 0x01,
|
||||||
|
0x0a, 0x08, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x35, 0x0a, 0x0b, 0x53, 0x65,
|
||||||
|
0x6e, 0x64, 0x46, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||||
|
0x6f, 0x2e, 0x46, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
||||||
|
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
|
||||||
|
0x79, 0x12, 0x41, 0x0a, 0x11, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65,
|
||||||
|
0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50,
|
||||||
|
0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x1a, 0x16, 0x2e, 0x67,
|
||||||
|
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
|
||||||
|
0x6d, 0x70, 0x74, 0x79, 0x42, 0x1b, 0x5a, 0x19, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x6c, 0x75, 0x67,
|
||||||
|
0x69, 0x6e, 0x2f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74,
|
||||||
|
0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -262,17 +333,17 @@ func file_notifier_proto_notifier_proto_rawDescGZIP() []byte {
|
||||||
var file_notifier_proto_notifier_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
var file_notifier_proto_notifier_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||||
var file_notifier_proto_notifier_proto_goTypes = []interface{}{
|
var file_notifier_proto_notifier_proto_goTypes = []interface{}{
|
||||||
(*FsEvent)(nil), // 0: proto.FsEvent
|
(*FsEvent)(nil), // 0: proto.FsEvent
|
||||||
(*UserEvent)(nil), // 1: proto.UserEvent
|
(*ProviderEvent)(nil), // 1: proto.ProviderEvent
|
||||||
(*timestamppb.Timestamp)(nil), // 2: google.protobuf.Timestamp
|
(*timestamppb.Timestamp)(nil), // 2: google.protobuf.Timestamp
|
||||||
(*emptypb.Empty)(nil), // 3: google.protobuf.Empty
|
(*emptypb.Empty)(nil), // 3: google.protobuf.Empty
|
||||||
}
|
}
|
||||||
var file_notifier_proto_notifier_proto_depIdxs = []int32{
|
var file_notifier_proto_notifier_proto_depIdxs = []int32{
|
||||||
2, // 0: proto.FsEvent.timestamp:type_name -> google.protobuf.Timestamp
|
2, // 0: proto.FsEvent.timestamp:type_name -> google.protobuf.Timestamp
|
||||||
2, // 1: proto.UserEvent.timestamp:type_name -> google.protobuf.Timestamp
|
2, // 1: proto.ProviderEvent.timestamp:type_name -> google.protobuf.Timestamp
|
||||||
0, // 2: proto.Notifier.SendFsEvent:input_type -> proto.FsEvent
|
0, // 2: proto.Notifier.SendFsEvent:input_type -> proto.FsEvent
|
||||||
1, // 3: proto.Notifier.SendUserEvent:input_type -> proto.UserEvent
|
1, // 3: proto.Notifier.SendProviderEvent:input_type -> proto.ProviderEvent
|
||||||
3, // 4: proto.Notifier.SendFsEvent:output_type -> google.protobuf.Empty
|
3, // 4: proto.Notifier.SendFsEvent:output_type -> google.protobuf.Empty
|
||||||
3, // 5: proto.Notifier.SendUserEvent:output_type -> google.protobuf.Empty
|
3, // 5: proto.Notifier.SendProviderEvent:output_type -> google.protobuf.Empty
|
||||||
4, // [4:6] is the sub-list for method output_type
|
4, // [4:6] is the sub-list for method output_type
|
||||||
2, // [2:4] is the sub-list for method input_type
|
2, // [2:4] is the sub-list for method input_type
|
||||||
2, // [2:2] is the sub-list for extension type_name
|
2, // [2:2] is the sub-list for extension type_name
|
||||||
|
@ -299,7 +370,7 @@ func file_notifier_proto_notifier_proto_init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_notifier_proto_notifier_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
file_notifier_proto_notifier_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||||
switch v := v.(*UserEvent); i {
|
switch v := v.(*ProviderEvent); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
|
@ -344,7 +415,7 @@ const _ = grpc.SupportPackageIsVersion6
|
||||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
|
||||||
type NotifierClient interface {
|
type NotifierClient interface {
|
||||||
SendFsEvent(ctx context.Context, in *FsEvent, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
SendFsEvent(ctx context.Context, in *FsEvent, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
SendUserEvent(ctx context.Context, in *UserEvent, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
SendProviderEvent(ctx context.Context, in *ProviderEvent, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type notifierClient struct {
|
type notifierClient struct {
|
||||||
|
@ -364,9 +435,9 @@ func (c *notifierClient) SendFsEvent(ctx context.Context, in *FsEvent, opts ...g
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *notifierClient) SendUserEvent(ctx context.Context, in *UserEvent, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
func (c *notifierClient) SendProviderEvent(ctx context.Context, in *ProviderEvent, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||||
out := new(emptypb.Empty)
|
out := new(emptypb.Empty)
|
||||||
err := c.cc.Invoke(ctx, "/proto.Notifier/SendUserEvent", in, out, opts...)
|
err := c.cc.Invoke(ctx, "/proto.Notifier/SendProviderEvent", in, out, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -376,7 +447,7 @@ func (c *notifierClient) SendUserEvent(ctx context.Context, in *UserEvent, opts
|
||||||
// NotifierServer is the server API for Notifier service.
|
// NotifierServer is the server API for Notifier service.
|
||||||
type NotifierServer interface {
|
type NotifierServer interface {
|
||||||
SendFsEvent(context.Context, *FsEvent) (*emptypb.Empty, error)
|
SendFsEvent(context.Context, *FsEvent) (*emptypb.Empty, error)
|
||||||
SendUserEvent(context.Context, *UserEvent) (*emptypb.Empty, error)
|
SendProviderEvent(context.Context, *ProviderEvent) (*emptypb.Empty, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnimplementedNotifierServer can be embedded to have forward compatible implementations.
|
// UnimplementedNotifierServer can be embedded to have forward compatible implementations.
|
||||||
|
@ -386,8 +457,8 @@ type UnimplementedNotifierServer struct {
|
||||||
func (*UnimplementedNotifierServer) SendFsEvent(context.Context, *FsEvent) (*emptypb.Empty, error) {
|
func (*UnimplementedNotifierServer) SendFsEvent(context.Context, *FsEvent) (*emptypb.Empty, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method SendFsEvent not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method SendFsEvent not implemented")
|
||||||
}
|
}
|
||||||
func (*UnimplementedNotifierServer) SendUserEvent(context.Context, *UserEvent) (*emptypb.Empty, error) {
|
func (*UnimplementedNotifierServer) SendProviderEvent(context.Context, *ProviderEvent) (*emptypb.Empty, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method SendUserEvent not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method SendProviderEvent not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterNotifierServer(s *grpc.Server, srv NotifierServer) {
|
func RegisterNotifierServer(s *grpc.Server, srv NotifierServer) {
|
||||||
|
@ -412,20 +483,20 @@ func _Notifier_SendFsEvent_Handler(srv interface{}, ctx context.Context, dec fun
|
||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func _Notifier_SendUserEvent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _Notifier_SendProviderEvent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(UserEvent)
|
in := new(ProviderEvent)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if interceptor == nil {
|
if interceptor == nil {
|
||||||
return srv.(NotifierServer).SendUserEvent(ctx, in)
|
return srv.(NotifierServer).SendProviderEvent(ctx, in)
|
||||||
}
|
}
|
||||||
info := &grpc.UnaryServerInfo{
|
info := &grpc.UnaryServerInfo{
|
||||||
Server: srv,
|
Server: srv,
|
||||||
FullMethod: "/proto.Notifier/SendUserEvent",
|
FullMethod: "/proto.Notifier/SendProviderEvent",
|
||||||
}
|
}
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
return srv.(NotifierServer).SendUserEvent(ctx, req.(*UserEvent))
|
return srv.(NotifierServer).SendProviderEvent(ctx, req.(*ProviderEvent))
|
||||||
}
|
}
|
||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
@ -439,8 +510,8 @@ var _Notifier_serviceDesc = grpc.ServiceDesc{
|
||||||
Handler: _Notifier_SendFsEvent_Handler,
|
Handler: _Notifier_SendFsEvent_Handler,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
MethodName: "SendUserEvent",
|
MethodName: "SendProviderEvent",
|
||||||
Handler: _Notifier_SendUserEvent_Handler,
|
Handler: _Notifier_SendProviderEvent_Handler,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{},
|
Streams: []grpc.StreamDesc{},
|
||||||
|
|
|
@ -16,15 +16,22 @@ message FsEvent {
|
||||||
int64 file_size = 7;
|
int64 file_size = 7;
|
||||||
string protocol = 8;
|
string protocol = 8;
|
||||||
int32 status = 9;
|
int32 status = 9;
|
||||||
|
string ip = 10;
|
||||||
|
string virtual_path = 11;
|
||||||
|
string virtual_target_path = 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
message UserEvent {
|
message ProviderEvent {
|
||||||
google.protobuf.Timestamp timestamp = 1;
|
google.protobuf.Timestamp timestamp = 1;
|
||||||
string action = 2;
|
string action = 2;
|
||||||
bytes user = 3; // SFTPGo user JSON serialized
|
string object_type = 3;
|
||||||
|
string username = 4;
|
||||||
|
string ip = 5;
|
||||||
|
string object_name = 6;
|
||||||
|
bytes object_data = 7; // object JSON serialized
|
||||||
}
|
}
|
||||||
|
|
||||||
service Notifier {
|
service Notifier {
|
||||||
rpc SendFsEvent(FsEvent) returns (google.protobuf.Empty);
|
rpc SendFsEvent(FsEvent) returns (google.protobuf.Empty);
|
||||||
rpc SendUserEvent(UserEvent) returns (google.protobuf.Empty);
|
rpc SendProviderEvent(ProviderEvent) returns (google.protobuf.Empty);
|
||||||
}
|
}
|
|
@ -166,23 +166,27 @@ func (m *Manager) validateConfigs() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyFsEvent sends the fs event notifications using any defined notifier plugins
|
// NotifyFsEvent sends the fs event notifications using any defined notifier plugins
|
||||||
func (m *Manager) NotifyFsEvent(timestamp time.Time, action, username, fsPath, fsTargetPath, sshCmd, protocol string,
|
func (m *Manager) NotifyFsEvent(timestamp time.Time, action, username, fsPath, fsTargetPath, sshCmd, protocol, ip,
|
||||||
fileSize int64, err error) {
|
virtualPath, virtualTargetPath string, fileSize int64, err error,
|
||||||
|
) {
|
||||||
m.notifLock.RLock()
|
m.notifLock.RLock()
|
||||||
defer m.notifLock.RUnlock()
|
defer m.notifLock.RUnlock()
|
||||||
|
|
||||||
for _, n := range m.notifiers {
|
for _, n := range m.notifiers {
|
||||||
n.notifyFsAction(timestamp, action, username, fsPath, fsTargetPath, sshCmd, protocol, fileSize, err)
|
n.notifyFsAction(timestamp, action, username, fsPath, fsTargetPath, sshCmd, protocol, ip, virtualPath, virtualTargetPath,
|
||||||
|
fileSize, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyUserEvent sends the user event notifications using any defined notifier plugins
|
// NotifyProviderEvent sends the provider event notifications using any defined notifier plugins
|
||||||
func (m *Manager) NotifyUserEvent(timestamp time.Time, action string, user Renderer) {
|
func (m *Manager) NotifyProviderEvent(timestamp time.Time, action, username, objectType, objectName, ip string,
|
||||||
|
object Renderer,
|
||||||
|
) {
|
||||||
m.notifLock.RLock()
|
m.notifLock.RLock()
|
||||||
defer m.notifLock.RUnlock()
|
defer m.notifLock.RUnlock()
|
||||||
|
|
||||||
for _, n := range m.notifiers {
|
for _, n := range m.notifiers {
|
||||||
n.notifyUserAction(timestamp, action, user)
|
n.notifyProviderAction(timestamp, action, username, objectType, objectName, ip, object)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -129,7 +129,7 @@ func (s *Service) Start() error {
|
||||||
|
|
||||||
if s.PortableMode == 1 {
|
if s.PortableMode == 1 {
|
||||||
// create the user for portable mode
|
// create the user for portable mode
|
||||||
err = dataprovider.AddUser(&s.PortableUser)
|
err = dataprovider.AddUser(&s.PortableUser, dataprovider.ActionExecutorSystem, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorToConsole("error adding portable user: %v", err)
|
logger.ErrorToConsole("error adding portable user: %v", err)
|
||||||
return err
|
return err
|
||||||
|
@ -300,19 +300,19 @@ func (s *Service) loadInitialData() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) restoreDump(dump *dataprovider.BackupData) error {
|
func (s *Service) restoreDump(dump *dataprovider.BackupData) error {
|
||||||
err := httpd.RestoreFolders(dump.Folders, s.LoadDataFrom, s.LoadDataMode, s.LoadDataQuotaScan)
|
err := httpd.RestoreFolders(dump.Folders, s.LoadDataFrom, s.LoadDataMode, s.LoadDataQuotaScan, dataprovider.ActionExecutorSystem, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to restore folders from file %#v: %v", s.LoadDataFrom, err)
|
return fmt.Errorf("unable to restore folders from file %#v: %v", s.LoadDataFrom, err)
|
||||||
}
|
}
|
||||||
err = httpd.RestoreUsers(dump.Users, s.LoadDataFrom, s.LoadDataMode, s.LoadDataQuotaScan)
|
err = httpd.RestoreUsers(dump.Users, s.LoadDataFrom, s.LoadDataMode, s.LoadDataQuotaScan, dataprovider.ActionExecutorSystem, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to restore users from file %#v: %v", s.LoadDataFrom, err)
|
return fmt.Errorf("unable to restore users from file %#v: %v", s.LoadDataFrom, err)
|
||||||
}
|
}
|
||||||
err = httpd.RestoreAdmins(dump.Admins, s.LoadDataFrom, s.LoadDataMode)
|
err = httpd.RestoreAdmins(dump.Admins, s.LoadDataFrom, s.LoadDataMode, dataprovider.ActionExecutorSystem, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to restore admins from file %#v: %v", s.LoadDataFrom, err)
|
return fmt.Errorf("unable to restore admins from file %#v: %v", s.LoadDataFrom, err)
|
||||||
}
|
}
|
||||||
err = httpd.RestoreAPIKeys(dump.APIKeys, s.LoadDataFrom, s.LoadDataMode)
|
err = httpd.RestoreAPIKeys(dump.APIKeys, s.LoadDataFrom, s.LoadDataMode, dataprovider.ActionExecutorSystem, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to restore API keys from file %#v: %v", s.LoadDataFrom, err)
|
return fmt.Errorf("unable to restore API keys from file %#v: %v", s.LoadDataFrom, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ func (c *Connection) Fileread(request *sftp.Request) (io.ReaderAt, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := common.ExecutePreAction(&c.User, common.OperationPreDownload, p, request.Filepath, c.GetProtocol(), 0, 0); err != nil {
|
if err := common.ExecutePreAction(&c.User, common.OperationPreDownload, p, request.Filepath, c.GetProtocol(), c.GetRemoteIP(), 0, 0); err != nil {
|
||||||
c.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", request.Filepath, err)
|
c.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", request.Filepath, err)
|
||||||
return nil, c.GetPermissionDeniedError()
|
return nil, c.GetPermissionDeniedError()
|
||||||
}
|
}
|
||||||
|
@ -349,7 +349,7 @@ func (c *Connection) handleSFTPUploadToNewFile(fs vfs.Fs, resolvedPath, filePath
|
||||||
return nil, c.GetQuotaExceededError()
|
return nil, c.GetQuotaExceededError()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), 0, 0); err != nil {
|
if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), c.GetRemoteIP(), 0, 0); err != nil {
|
||||||
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||||
return nil, c.GetPermissionDeniedError()
|
return nil, c.GetPermissionDeniedError()
|
||||||
}
|
}
|
||||||
|
@ -396,7 +396,7 @@ func (c *Connection) handleSFTPUploadToExistingFile(fs vfs.Fs, pflags sftp.FileO
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), fileSize, osFlags); err != nil {
|
if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), c.GetRemoteIP(), fileSize, osFlags); err != nil {
|
||||||
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||||
return nil, c.GetPermissionDeniedError()
|
return nil, c.GetPermissionDeniedError()
|
||||||
}
|
}
|
||||||
|
|
|
@ -219,7 +219,8 @@ func (c *scpCommand) handleUploadFile(fs vfs.Fs, resolvedPath, filePath string,
|
||||||
c.sendErrorMessage(fs, err)
|
c.sendErrorMessage(fs, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err := common.ExecutePreAction(&c.connection.User, common.OperationPreUpload, resolvedPath, requestPath, c.connection.GetProtocol(), fileSize, os.O_TRUNC)
|
err := common.ExecutePreAction(&c.connection.User, common.OperationPreUpload, resolvedPath, requestPath,
|
||||||
|
c.connection.GetProtocol(), c.connection.GetRemoteIP(), fileSize, os.O_TRUNC)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.connection.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
c.connection.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||||
err = c.connection.GetPermissionDeniedError()
|
err = c.connection.GetPermissionDeniedError()
|
||||||
|
@ -514,7 +515,7 @@ func (c *scpCommand) handleDownload(filePath string) error {
|
||||||
return common.ErrPermissionDenied
|
return common.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := common.ExecutePreAction(&c.connection.User, common.OperationPreDownload, p, filePath, c.connection.GetProtocol(), 0, 0); err != nil {
|
if err := common.ExecutePreAction(&c.connection.User, common.OperationPreDownload, p, filePath, c.connection.GetProtocol(), c.connection.GetRemoteIP(), 0, 0); err != nil {
|
||||||
c.connection.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", filePath, err)
|
c.connection.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", filePath, err)
|
||||||
c.sendErrorMessage(fs, common.ErrPermissionDenied)
|
c.sendErrorMessage(fs, common.ErrPermissionDenied)
|
||||||
return common.ErrPermissionDenied
|
return common.ErrPermissionDenied
|
||||||
|
|
|
@ -633,7 +633,7 @@ func TestDefender(t *testing.T) {
|
||||||
_, _, err = getSftpClient(user, usePubKey)
|
_, _, err = getSftpClient(user, usePubKey)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(user.Username)
|
err = dataprovider.DeleteUser(user.Username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -3194,7 +3194,7 @@ func TestMaxConnections(t *testing.T) {
|
||||||
|
|
||||||
usePubKey := true
|
usePubKey := true
|
||||||
user := getTestUser(usePubKey)
|
user := getTestUser(usePubKey)
|
||||||
err := dataprovider.AddUser(&user)
|
err := dataprovider.AddUser(&user, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user.Password = ""
|
user.Password = ""
|
||||||
conn, client, err := getSftpClient(user, usePubKey)
|
conn, client, err := getSftpClient(user, usePubKey)
|
||||||
|
@ -3210,7 +3210,7 @@ func TestMaxConnections(t *testing.T) {
|
||||||
err = conn.Close()
|
err = conn.Close()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
err = dataprovider.DeleteUser(user.Username)
|
err = dataprovider.DeleteUser(user.Username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -3229,7 +3229,7 @@ func TestMaxPerHostConnections(t *testing.T) {
|
||||||
|
|
||||||
usePubKey := true
|
usePubKey := true
|
||||||
user := getTestUser(usePubKey)
|
user := getTestUser(usePubKey)
|
||||||
err := dataprovider.AddUser(&user)
|
err := dataprovider.AddUser(&user, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user.Password = ""
|
user.Password = ""
|
||||||
conn, client, err := getSftpClient(user, usePubKey)
|
conn, client, err := getSftpClient(user, usePubKey)
|
||||||
|
@ -3245,7 +3245,7 @@ func TestMaxPerHostConnections(t *testing.T) {
|
||||||
err = conn.Close()
|
err = conn.Close()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
err = dataprovider.DeleteUser(user.Username)
|
err = dataprovider.DeleteUser(user.Username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
@ -717,11 +717,13 @@ func (c *sshCommand) sendErrorResponse(err error) error {
|
||||||
|
|
||||||
func (c *sshCommand) sendExitStatus(err error) {
|
func (c *sshCommand) sendExitStatus(err error) {
|
||||||
status := uint32(0)
|
status := uint32(0)
|
||||||
cmdPath := c.getDestPath()
|
vCmdPath := c.getDestPath()
|
||||||
|
cmdPath := ""
|
||||||
targetPath := ""
|
targetPath := ""
|
||||||
|
vTargetPath := ""
|
||||||
if c.command == "sftpgo-copy" {
|
if c.command == "sftpgo-copy" {
|
||||||
targetPath = cmdPath
|
vTargetPath = vCmdPath
|
||||||
cmdPath = c.getSourcePath()
|
vCmdPath = c.getSourcePath()
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status = uint32(1)
|
status = uint32(1)
|
||||||
|
@ -737,20 +739,20 @@ func (c *sshCommand) sendExitStatus(err error) {
|
||||||
// for scp we notify single uploads/downloads
|
// for scp we notify single uploads/downloads
|
||||||
if c.command != scpCmdName {
|
if c.command != scpCmdName {
|
||||||
metric.SSHCommandCompleted(err)
|
metric.SSHCommandCompleted(err)
|
||||||
if cmdPath != "" {
|
if vCmdPath != "" {
|
||||||
_, p, errFs := c.connection.GetFsAndResolvedPath(cmdPath)
|
_, p, errFs := c.connection.GetFsAndResolvedPath(vCmdPath)
|
||||||
if errFs == nil {
|
if errFs == nil {
|
||||||
cmdPath = p
|
cmdPath = p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if targetPath != "" {
|
if vTargetPath != "" {
|
||||||
_, p, errFs := c.connection.GetFsAndResolvedPath(targetPath)
|
_, p, errFs := c.connection.GetFsAndResolvedPath(vTargetPath)
|
||||||
if errFs == nil {
|
if errFs == nil {
|
||||||
targetPath = p
|
targetPath = p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
common.ExecuteActionNotification(&c.connection.User, common.OperationSSHCmd, cmdPath, c.getDestPath(), targetPath, c.command,
|
common.ExecuteActionNotification(&c.connection.User, common.OperationSSHCmd, cmdPath, vCmdPath, targetPath,
|
||||||
common.ProtocolSSH, 0, err)
|
vTargetPath, c.command, common.ProtocolSSH, c.connection.GetRemoteIP(), 0, err)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
logger.CommandLog(sshCommandLogSender, cmdPath, targetPath, c.connection.User.Username, "", c.connection.ID,
|
logger.CommandLog(sshCommandLogSender, cmdPath, targetPath, c.connection.User.Username, "", c.connection.ID,
|
||||||
common.ProtocolSSH, -1, -1, "", "", c.connection.command, -1, c.connection.GetLocalAddress(),
|
common.ProtocolSSH, -1, -1, "", "", c.connection.command, -1, c.connection.GetLocalAddress(),
|
||||||
|
|
|
@ -160,6 +160,7 @@
|
||||||
"users_base_dir": "",
|
"users_base_dir": "",
|
||||||
"actions": {
|
"actions": {
|
||||||
"execute_on": [],
|
"execute_on": [],
|
||||||
|
"execute_for": [],
|
||||||
"hook": ""
|
"hook": ""
|
||||||
},
|
},
|
||||||
"external_auth_hook": "",
|
"external_auth_hook": "",
|
||||||
|
|
|
@ -187,6 +187,9 @@ func (f *Filesystem) HasRedactedSecret() bool {
|
||||||
if f.AzBlobConfig.AccountKey.IsRedacted() {
|
if f.AzBlobConfig.AccountKey.IsRedacted() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if f.AzBlobConfig.SASURL.IsRedacted() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
case sdk.CryptedFilesystemProvider:
|
case sdk.CryptedFilesystemProvider:
|
||||||
if f.CryptConfig.Passphrase.IsRedacted() {
|
if f.CryptConfig.Passphrase.IsRedacted() {
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -148,7 +148,7 @@ func (f *webDavFile) Read(p []byte) (n int, err error) {
|
||||||
return 0, f.Connection.GetPermissionDeniedError()
|
return 0, f.Connection.GetPermissionDeniedError()
|
||||||
}
|
}
|
||||||
err := common.ExecutePreAction(&f.Connection.User, common.OperationPreDownload, f.GetFsPath(), f.GetVirtualPath(),
|
err := common.ExecutePreAction(&f.Connection.User, common.OperationPreDownload, f.GetFsPath(), f.GetVirtualPath(),
|
||||||
f.Connection.GetProtocol(), 0, 0)
|
f.Connection.GetProtocol(), f.Connection.GetRemoteIP(), 0, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.Connection.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", f.GetVirtualPath(), err)
|
f.Connection.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", f.GetVirtualPath(), err)
|
||||||
return 0, f.Connection.GetPermissionDeniedError()
|
return 0, f.Connection.GetPermissionDeniedError()
|
||||||
|
|
|
@ -199,7 +199,7 @@ func (c *Connection) handleUploadToNewFile(fs vfs.Fs, resolvedPath, filePath, re
|
||||||
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
||||||
return nil, common.ErrQuotaExceeded
|
return nil, common.ErrQuotaExceeded
|
||||||
}
|
}
|
||||||
if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), 0, 0); err != nil {
|
if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), c.GetRemoteIP(), 0, 0); err != nil {
|
||||||
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||||
return nil, c.GetPermissionDeniedError()
|
return nil, c.GetPermissionDeniedError()
|
||||||
}
|
}
|
||||||
|
@ -228,7 +228,8 @@ func (c *Connection) handleUploadToExistingFile(fs vfs.Fs, resolvedPath, filePat
|
||||||
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
||||||
return nil, common.ErrQuotaExceeded
|
return nil, common.ErrQuotaExceeded
|
||||||
}
|
}
|
||||||
if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), fileSize, os.O_TRUNC); err != nil {
|
if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), c.GetRemoteIP(),
|
||||||
|
fileSize, os.O_TRUNC); err != nil {
|
||||||
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||||
return nil, c.GetPermissionDeniedError()
|
return nil, c.GetPermissionDeniedError()
|
||||||
}
|
}
|
||||||
|
|
|
@ -935,7 +935,7 @@ func TestBasicUsersCache(t *testing.T) {
|
||||||
}
|
}
|
||||||
u.Permissions = make(map[string][]string)
|
u.Permissions = make(map[string][]string)
|
||||||
u.Permissions["/"] = []string{dataprovider.PermAny}
|
u.Permissions["/"] = []string{dataprovider.PermAny}
|
||||||
err := dataprovider.AddUser(&u)
|
err := dataprovider.AddUser(&u, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user, err := dataprovider.UserExists(u.Username)
|
user, err := dataprovider.UserExists(u.Username)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1007,7 +1007,7 @@ func TestBasicUsersCache(t *testing.T) {
|
||||||
assert.False(t, cachedUser.IsExpired())
|
assert.False(t, cachedUser.IsExpired())
|
||||||
}
|
}
|
||||||
// cache is not invalidated after a user modification if the fs does not change
|
// cache is not invalidated after a user modification if the fs does not change
|
||||||
err = dataprovider.UpdateUser(&user)
|
err = dataprovider.UpdateUser(&user, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
|
@ -1020,7 +1020,7 @@ func TestBasicUsersCache(t *testing.T) {
|
||||||
VirtualPath: "/vdir",
|
VirtualPath: "/vdir",
|
||||||
})
|
})
|
||||||
|
|
||||||
err = dataprovider.UpdateUser(&user)
|
err = dataprovider.UpdateUser(&user, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
||||||
assert.False(t, ok)
|
assert.False(t, ok)
|
||||||
|
@ -1032,12 +1032,12 @@ func TestBasicUsersCache(t *testing.T) {
|
||||||
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
// cache is invalidated after user deletion
|
// cache is invalidated after user deletion
|
||||||
err = dataprovider.DeleteUser(user.Username)
|
err = dataprovider.DeleteUser(user.Username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
||||||
assert.False(t, ok)
|
assert.False(t, ok)
|
||||||
|
|
||||||
err = dataprovider.DeleteFolder(folderName)
|
err = dataprovider.DeleteFolder(folderName, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = os.RemoveAll(u.GetHomeDir())
|
err = os.RemoveAll(u.GetHomeDir())
|
||||||
|
@ -1066,7 +1066,7 @@ func TestCachedUserWithFolders(t *testing.T) {
|
||||||
},
|
},
|
||||||
VirtualPath: "/vpath",
|
VirtualPath: "/vpath",
|
||||||
})
|
})
|
||||||
err := dataprovider.AddUser(&u)
|
err := dataprovider.AddUser(&u, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user, err := dataprovider.UserExists(u.Username)
|
user, err := dataprovider.UserExists(u.Username)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1119,7 +1119,7 @@ func TestCachedUserWithFolders(t *testing.T) {
|
||||||
folder, err := dataprovider.GetFolderByName(folderName)
|
folder, err := dataprovider.GetFolderByName(folderName)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
// updating a used folder should invalidate the cache only if the fs changed
|
// 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, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
_, isCached, _, loginMethod, err = server.authenticate(req, ipAddr)
|
_, isCached, _, loginMethod, err = server.authenticate(req, ipAddr)
|
||||||
|
@ -1132,7 +1132,7 @@ func TestCachedUserWithFolders(t *testing.T) {
|
||||||
}
|
}
|
||||||
// changing the folder path should invalidate the cache
|
// changing the folder path should invalidate the cache
|
||||||
folder.MappedPath = filepath.Join(os.TempDir(), "anotherpath")
|
folder.MappedPath = filepath.Join(os.TempDir(), "anotherpath")
|
||||||
err = dataprovider.UpdateFolder(&folder, folder.Users)
|
err = dataprovider.UpdateFolder(&folder, folder.Users, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
_, isCached, _, loginMethod, err = server.authenticate(req, ipAddr)
|
_, isCached, _, loginMethod, err = server.authenticate(req, ipAddr)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1143,7 +1143,7 @@ func TestCachedUserWithFolders(t *testing.T) {
|
||||||
assert.False(t, cachedUser.IsExpired())
|
assert.False(t, cachedUser.IsExpired())
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dataprovider.DeleteFolder(folderName)
|
err = dataprovider.DeleteFolder(folderName, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
// removing a used folder should invalidate the cache
|
// removing a used folder should invalidate the cache
|
||||||
_, isCached, _, loginMethod, err = server.authenticate(req, ipAddr)
|
_, isCached, _, loginMethod, err = server.authenticate(req, ipAddr)
|
||||||
|
@ -1155,7 +1155,7 @@ func TestCachedUserWithFolders(t *testing.T) {
|
||||||
assert.False(t, cachedUser.IsExpired())
|
assert.False(t, cachedUser.IsExpired())
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(user.Username)
|
err = dataprovider.DeleteUser(user.Username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
||||||
assert.False(t, ok)
|
assert.False(t, ok)
|
||||||
|
@ -1181,25 +1181,25 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
|
||||||
u.Password = password + "1"
|
u.Password = password + "1"
|
||||||
u.Permissions = make(map[string][]string)
|
u.Permissions = make(map[string][]string)
|
||||||
u.Permissions["/"] = []string{dataprovider.PermAny}
|
u.Permissions["/"] = []string{dataprovider.PermAny}
|
||||||
err := dataprovider.AddUser(&u)
|
err := dataprovider.AddUser(&u, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user1, err := dataprovider.UserExists(u.Username)
|
user1, err := dataprovider.UserExists(u.Username)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
u.Username = username + "2"
|
u.Username = username + "2"
|
||||||
u.Password = password + "2"
|
u.Password = password + "2"
|
||||||
err = dataprovider.AddUser(&u)
|
err = dataprovider.AddUser(&u, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user2, err := dataprovider.UserExists(u.Username)
|
user2, err := dataprovider.UserExists(u.Username)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
u.Username = username + "3"
|
u.Username = username + "3"
|
||||||
u.Password = password + "3"
|
u.Password = password + "3"
|
||||||
err = dataprovider.AddUser(&u)
|
err = dataprovider.AddUser(&u, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user3, err := dataprovider.UserExists(u.Username)
|
user3, err := dataprovider.UserExists(u.Username)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
u.Username = username + "4"
|
u.Username = username + "4"
|
||||||
u.Password = password + "4"
|
u.Password = password + "4"
|
||||||
err = dataprovider.AddUser(&u)
|
err = dataprovider.AddUser(&u, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user4, err := dataprovider.UserExists(u.Username)
|
user4, err := dataprovider.UserExists(u.Username)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1332,7 +1332,7 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
|
||||||
|
|
||||||
// now remove user1 after an update
|
// now remove user1 after an update
|
||||||
user1.HomeDir += "_mod"
|
user1.HomeDir += "_mod"
|
||||||
err = dataprovider.UpdateUser(&user1)
|
err = dataprovider.UpdateUser(&user1, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
_, ok = dataprovider.GetCachedWebDAVUser(user1.Username)
|
_, ok = dataprovider.GetCachedWebDAVUser(user1.Username)
|
||||||
assert.False(t, ok)
|
assert.False(t, ok)
|
||||||
|
@ -1363,13 +1363,13 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
|
||||||
_, ok = dataprovider.GetCachedWebDAVUser(user4.Username)
|
_, ok = dataprovider.GetCachedWebDAVUser(user4.Username)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(user1.Username)
|
err = dataprovider.DeleteUser(user1.Username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = dataprovider.DeleteUser(user2.Username)
|
err = dataprovider.DeleteUser(user2.Username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = dataprovider.DeleteUser(user3.Username)
|
err = dataprovider.DeleteUser(user3.Username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = dataprovider.DeleteUser(user4.Username)
|
err = dataprovider.DeleteUser(user4.Username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = os.RemoveAll(u.GetHomeDir())
|
err = os.RemoveAll(u.GetHomeDir())
|
||||||
|
@ -1391,7 +1391,7 @@ func TestUserCacheIsolation(t *testing.T) {
|
||||||
}
|
}
|
||||||
u.Permissions = make(map[string][]string)
|
u.Permissions = make(map[string][]string)
|
||||||
u.Permissions["/"] = []string{dataprovider.PermAny}
|
u.Permissions["/"] = []string{dataprovider.PermAny}
|
||||||
err := dataprovider.AddUser(&u)
|
err := dataprovider.AddUser(&u, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user, err := dataprovider.UserExists(u.Username)
|
user, err := dataprovider.UserExists(u.Username)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1427,7 +1427,7 @@ func TestUserCacheIsolation(t *testing.T) {
|
||||||
assert.False(t, cachedUser.User.FsConfig.S3Config.AccessSecret.IsEncrypted())
|
assert.False(t, cachedUser.User.FsConfig.S3Config.AccessSecret.IsEncrypted())
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(username)
|
err = dataprovider.DeleteUser(username, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
||||||
assert.False(t, ok)
|
assert.False(t, ok)
|
||||||
|
|
|
@ -672,6 +672,7 @@ func TestLockAfterDelete(t *testing.T) {
|
||||||
req, err := http.NewRequest("LOCK", fmt.Sprintf("http://%v/%v", webDavServerAddr, testFileName), bytes.NewReader([]byte(lockBody)))
|
req, err := http.NewRequest("LOCK", fmt.Sprintf("http://%v/%v", webDavServerAddr, testFileName), bytes.NewReader([]byte(lockBody)))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
req.SetBasicAuth(u.Username, u.Password)
|
req.SetBasicAuth(u.Username, u.Password)
|
||||||
|
req.Header.Set("Timeout", "Second-3600")
|
||||||
httpClient := httpclient.GetHTTPClient()
|
httpClient := httpclient.GetHTTPClient()
|
||||||
resp, err := httpClient.Do(req)
|
resp, err := httpClient.Do(req)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
Loading…
Reference in a new issue