mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-22 07:30:25 +00:00
add post-login hook
a login scope is supported too so you can get notifications for failed logins, successful logins or both
This commit is contained in:
parent
a9e21c282a
commit
aa0ed5dbd0
21 changed files with 220 additions and 129 deletions
|
@ -17,6 +17,7 @@ import (
|
||||||
|
|
||||||
"github.com/pires/go-proxyproto"
|
"github.com/pires/go-proxyproto"
|
||||||
|
|
||||||
|
"github.com/drakkan/sftpgo/dataprovider"
|
||||||
"github.com/drakkan/sftpgo/httpclient"
|
"github.com/drakkan/sftpgo/httpclient"
|
||||||
"github.com/drakkan/sftpgo/logger"
|
"github.com/drakkan/sftpgo/logger"
|
||||||
"github.com/drakkan/sftpgo/metrics"
|
"github.com/drakkan/sftpgo/metrics"
|
||||||
|
@ -446,9 +447,11 @@ func (conns *ActiveConnections) checkIdleConnections() {
|
||||||
logger.Debug(conn.GetProtocol(), conn.GetID(), "close idle connection, idle time: %v, username: %#v close err: %v",
|
logger.Debug(conn.GetProtocol(), conn.GetID(), "close idle connection, idle time: %v, username: %#v close err: %v",
|
||||||
idleTime, conn.GetUsername(), err)
|
idleTime, conn.GetUsername(), err)
|
||||||
if isFTPNoAuth {
|
if isFTPNoAuth {
|
||||||
logger.ConnectionFailedLog("", utils.GetIPFromRemoteAddress(c.GetRemoteAddress()),
|
ip := utils.GetIPFromRemoteAddress(c.GetRemoteAddress())
|
||||||
"no_auth_tryed", "client idle")
|
logger.ConnectionFailedLog("", ip, dataprovider.LoginMethodNoAuthTryed, c.GetProtocol(), "client idle")
|
||||||
metrics.AddNoAuthTryed()
|
metrics.AddNoAuthTryed()
|
||||||
|
dataprovider.ExecutePostLoginHook("", dataprovider.LoginMethodNoAuthTryed, ip, c.GetProtocol(),
|
||||||
|
dataprovider.ErrNoAuthTryed)
|
||||||
}
|
}
|
||||||
}(c, isUnauthenticatedFTPUser)
|
}(c, isUnauthenticatedFTPUser)
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,6 +115,8 @@ func init() {
|
||||||
ExternalAuthScope: 0,
|
ExternalAuthScope: 0,
|
||||||
CredentialsPath: "credentials",
|
CredentialsPath: "credentials",
|
||||||
PreLoginHook: "",
|
PreLoginHook: "",
|
||||||
|
PostLoginHook: "",
|
||||||
|
PostLoginScope: 0,
|
||||||
},
|
},
|
||||||
HTTPDConfig: httpd.Conf{
|
HTTPDConfig: httpd.Conf{
|
||||||
BindPort: 8080,
|
BindPort: 8080,
|
||||||
|
|
|
@ -88,14 +88,16 @@ var (
|
||||||
ValidPerms = []string{PermAny, PermListItems, PermDownload, PermUpload, PermOverwrite, PermRename, PermDelete,
|
ValidPerms = []string{PermAny, PermListItems, PermDownload, PermUpload, PermOverwrite, PermRename, PermDelete,
|
||||||
PermCreateDirs, PermCreateSymlinks, PermChmod, PermChown, PermChtimes}
|
PermCreateDirs, PermCreateSymlinks, PermChmod, PermChown, PermChtimes}
|
||||||
// ValidSSHLoginMethods defines all the valid SSH login methods
|
// ValidSSHLoginMethods defines all the valid SSH login methods
|
||||||
ValidSSHLoginMethods = []string{SSHLoginMethodPublicKey, SSHLoginMethodPassword, SSHLoginMethodKeyboardInteractive,
|
ValidSSHLoginMethods = []string{SSHLoginMethodPublicKey, LoginMethodPassword, SSHLoginMethodKeyboardInteractive,
|
||||||
SSHLoginMethodKeyAndPassword, SSHLoginMethodKeyAndKeyboardInt}
|
SSHLoginMethodKeyAndPassword, SSHLoginMethodKeyAndKeyboardInt}
|
||||||
// SSHMultiStepsLoginMethods defines the supported Multi-Step Authentications
|
// SSHMultiStepsLoginMethods defines the supported Multi-Step Authentications
|
||||||
SSHMultiStepsLoginMethods = []string{SSHLoginMethodKeyAndPassword, SSHLoginMethodKeyAndKeyboardInt}
|
SSHMultiStepsLoginMethods = []string{SSHLoginMethodKeyAndPassword, SSHLoginMethodKeyAndKeyboardInt}
|
||||||
config Config
|
// ErrNoAuthTryed defines the error for connection closed before authentication
|
||||||
provider Provider
|
ErrNoAuthTryed = errors.New("no auth tryed")
|
||||||
sqlPlaceholders []string
|
config Config
|
||||||
hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix,
|
provider Provider
|
||||||
|
sqlPlaceholders []string
|
||||||
|
hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix,
|
||||||
pbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix, md5cryptPwdPrefix, md5cryptApr1PwdPrefix, sha512cryptPwdPrefix}
|
pbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix, md5cryptPwdPrefix, md5cryptApr1PwdPrefix, sha512cryptPwdPrefix}
|
||||||
pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix}
|
pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix}
|
||||||
pbkdfPwdB64SaltPrefixes = []string{pbkdf2SHA256B64SaltPrefix}
|
pbkdfPwdB64SaltPrefixes = []string{pbkdf2SHA256B64SaltPrefix}
|
||||||
|
@ -173,39 +175,8 @@ type Config struct {
|
||||||
Actions UserActions `json:"actions" mapstructure:"actions"`
|
Actions UserActions `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.
|
||||||
// The external program can read the following environment variables to get info about the user trying
|
|
||||||
// to authenticate:
|
|
||||||
//
|
|
||||||
// - SFTPGO_AUTHD_USERNAME
|
|
||||||
// - SFTPGO_AUTHD_IP
|
|
||||||
// - SFTPGO_AUTHD_PASSWORD, not empty for password authentication
|
|
||||||
// - SFTPGO_AUTHD_PUBLIC_KEY, not empty for public key authentication
|
|
||||||
// - SFTPGO_AUTHD_KEYBOARD_INTERACTIVE, not empty for keyboard interactive authentication
|
|
||||||
//
|
|
||||||
// The content of these variables is _not_ quoted. They may contain special characters. They are under the
|
|
||||||
// control of a possibly malicious remote user.
|
|
||||||
//
|
|
||||||
// The program must respond on the standard output with a valid SFTPGo user serialized as JSON if the
|
|
||||||
// authentication succeed or a user with an empty username if the authentication fails.
|
|
||||||
// If the hook is an HTTP URL then it will be invoked as HTTP POST.
|
|
||||||
// The request body will contain a JSON serialized struct with the following fields:
|
|
||||||
//
|
|
||||||
// - username
|
|
||||||
// - ip
|
|
||||||
// - password, not empty for password authentication
|
|
||||||
// - public_key, not empty for public key authentication
|
|
||||||
// - keyboard_interactive, not empty for keyboard interactive authentication
|
|
||||||
//
|
|
||||||
// If authentication succeed the HTTP response code must be 200 and the response body a valid SFTPGo user
|
|
||||||
// serialized as JSON. If the authentication fails the HTTP response code must be != 200 or the response body
|
|
||||||
// must be empty.
|
|
||||||
//
|
|
||||||
// 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.
|
||||||
// Actions defined for user added/updated will not be executed in this case.
|
// Actions defined for user added/updated will not be executed in this case.
|
||||||
// The external hook should check authentication only, if there are login restrictions such as user
|
|
||||||
// disabled, expired, login allowed only from specific IP addresses it is enough to populate the matching user
|
|
||||||
// fields and these conditions will be checked in the same way as for builtin users.
|
|
||||||
// The external auth program must finish within 30 seconds.
|
|
||||||
// This method is slower than built-in authentication methods, but it's very flexible as anyone can
|
// This method is slower than built-in authentication methods, but it's very flexible as anyone can
|
||||||
// easily write his own authentication hooks.
|
// easily write his own authentication hooks.
|
||||||
ExternalAuthHook string `json:"external_auth_hook" mapstructure:"external_auth_hook"`
|
ExternalAuthHook string `json:"external_auth_hook" mapstructure:"external_auth_hook"`
|
||||||
|
@ -225,28 +196,6 @@ type Config struct {
|
||||||
// Absolute path to an external program or an HTTP URL to invoke just before the user login.
|
// Absolute path to an external program or an HTTP URL to invoke just before the user login.
|
||||||
// This program/URL allows to modify or create the user trying to login.
|
// This program/URL allows to modify or create the user trying to login.
|
||||||
// It is useful if you have users with dynamic fields to update just before the login.
|
// It is useful if you have users with dynamic fields to update just before the login.
|
||||||
// The external program can read the following environment variables:
|
|
||||||
//
|
|
||||||
// - SFTPGO_LOGIND_USER, it contains the user trying to login serialized as JSON
|
|
||||||
// - SFTPGO_LOGIND_METHOD, possible values are: "password", "publickey" and "keyboard-interactive"
|
|
||||||
// - SFTPGO_LOGIND_IP, ip address of the user trying to login
|
|
||||||
//
|
|
||||||
// The program must write on its standard output an empty string if no user update is needed
|
|
||||||
// or a valid SFTPGo user serialized as JSON.
|
|
||||||
//
|
|
||||||
// If the hook is an HTTP URL then it will be invoked as HTTP POST.
|
|
||||||
// The login method and the ip address of the user trying to login are added to
|
|
||||||
// the query string, for example "<http_url>?login_method=password&ip=1.2.3.4"
|
|
||||||
// The request body will contain the user trying to login serialized as JSON.
|
|
||||||
// If no modification is needed the HTTP response code must be 204, otherwise
|
|
||||||
// the response code must be 200 and the response body a valid SFTPGo user
|
|
||||||
// serialized as JSON.
|
|
||||||
//
|
|
||||||
// The JSON response can include only the fields to update instead of the full user,
|
|
||||||
// for example if you want to disable the user you can return a response like this:
|
|
||||||
//
|
|
||||||
// {"status":0}
|
|
||||||
//
|
|
||||||
// Please note that if you want to create a new user, the pre-login hook response must
|
// Please note that if you want to create a new user, the pre-login hook response must
|
||||||
// include all the mandatory user fields.
|
// include all the mandatory user fields.
|
||||||
//
|
//
|
||||||
|
@ -256,6 +205,15 @@ type Config struct {
|
||||||
// PreLoginHook and ExternalAuthHook are mutally exclusive.
|
// PreLoginHook and ExternalAuthHook are mutally exclusive.
|
||||||
// Leave empty to disable.
|
// Leave empty to disable.
|
||||||
PreLoginHook string `json:"pre_login_hook" mapstructure:"pre_login_hook"`
|
PreLoginHook string `json:"pre_login_hook" mapstructure:"pre_login_hook"`
|
||||||
|
// Absolute path to an external program or an HTTP URL to invoke after the user login.
|
||||||
|
// Based on the configured scope you can choose if notify failed or successful logins
|
||||||
|
// or both
|
||||||
|
PostLoginHook string `json:"post_login_hook" mapstructure:"post_login_hook"`
|
||||||
|
// PostLoginScope defines the scope for the post-login hook.
|
||||||
|
// - 0 means notify both failed and successful logins
|
||||||
|
// - 1 means notify failed logins
|
||||||
|
// - 2 means notify successful logins
|
||||||
|
PostLoginScope int `json:"post_login_scope" mapstructure:"post_login_scope"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// BackupData defines the structure for the backup/restore files
|
// BackupData defines the structure for the backup/restore files
|
||||||
|
@ -403,6 +361,16 @@ func validateHooks() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(config.PostLoginHook) > 0 && !strings.HasPrefix(config.PostLoginHook, "http") {
|
||||||
|
if !filepath.IsAbs(config.PostLoginHook) {
|
||||||
|
return fmt.Errorf("invalid post-login hook: %#v must be an absolute path", config.PostLoginHook)
|
||||||
|
}
|
||||||
|
_, err := os.Stat(config.PostLoginHook)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelWarn, "invalid post-login hook: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -438,16 +406,16 @@ func InitializeDatabase(cnf Config, basePath string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckUserAndPass retrieves the SFTP user with the given username and password if a match is found or an error
|
// CheckUserAndPass retrieves the SFTP user with the given username and password if a match is found or an error
|
||||||
func CheckUserAndPass(username, password, ip string) (User, error) {
|
func CheckUserAndPass(username, password, ip, protocol string) (User, error) {
|
||||||
if len(config.ExternalAuthHook) > 0 && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) {
|
if len(config.ExternalAuthHook) > 0 && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) {
|
||||||
user, err := doExternalAuth(username, password, nil, "", ip)
|
user, err := doExternalAuth(username, password, nil, "", ip, protocol)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
return checkUserAndPass(user, password)
|
return checkUserAndPass(user, password)
|
||||||
}
|
}
|
||||||
if len(config.PreLoginHook) > 0 {
|
if len(config.PreLoginHook) > 0 {
|
||||||
user, err := executePreLoginHook(username, SSHLoginMethodPassword, ip)
|
user, err := executePreLoginHook(username, LoginMethodPassword, ip, protocol)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
|
@ -457,16 +425,16 @@ func CheckUserAndPass(username, password, ip string) (User, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckUserAndPubKey retrieves the SFTP user with the given username and public key if a match is found or an error
|
// CheckUserAndPubKey retrieves the SFTP user with the given username and public key if a match is found or an error
|
||||||
func CheckUserAndPubKey(username string, pubKey []byte, ip string) (User, string, error) {
|
func CheckUserAndPubKey(username string, pubKey []byte, ip, protocol string) (User, string, error) {
|
||||||
if len(config.ExternalAuthHook) > 0 && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&2 != 0) {
|
if len(config.ExternalAuthHook) > 0 && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&2 != 0) {
|
||||||
user, err := doExternalAuth(username, "", pubKey, "", ip)
|
user, err := doExternalAuth(username, "", pubKey, "", ip, protocol)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, "", err
|
return user, "", err
|
||||||
}
|
}
|
||||||
return checkUserAndPubKey(user, pubKey)
|
return checkUserAndPubKey(user, pubKey)
|
||||||
}
|
}
|
||||||
if len(config.PreLoginHook) > 0 {
|
if len(config.PreLoginHook) > 0 {
|
||||||
user, err := executePreLoginHook(username, SSHLoginMethodPublicKey, ip)
|
user, err := executePreLoginHook(username, SSHLoginMethodPublicKey, ip, protocol)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, "", err
|
return user, "", err
|
||||||
}
|
}
|
||||||
|
@ -477,13 +445,13 @@ func CheckUserAndPubKey(username string, pubKey []byte, ip string) (User, string
|
||||||
|
|
||||||
// CheckKeyboardInteractiveAuth checks the keyboard interactive authentication and returns
|
// CheckKeyboardInteractiveAuth checks the keyboard interactive authentication and returns
|
||||||
// the authenticated user or an error
|
// the authenticated user or an error
|
||||||
func CheckKeyboardInteractiveAuth(username, authHook string, client ssh.KeyboardInteractiveChallenge, ip string) (User, error) {
|
func CheckKeyboardInteractiveAuth(username, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (User, error) {
|
||||||
var user User
|
var user User
|
||||||
var err error
|
var err error
|
||||||
if len(config.ExternalAuthHook) > 0 && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&4 != 0) {
|
if len(config.ExternalAuthHook) > 0 && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&4 != 0) {
|
||||||
user, err = doExternalAuth(username, "", nil, "1", ip)
|
user, err = doExternalAuth(username, "", nil, "1", ip, protocol)
|
||||||
} else if len(config.PreLoginHook) > 0 {
|
} else if len(config.PreLoginHook) > 0 {
|
||||||
user, err = executePreLoginHook(username, SSHLoginMethodKeyboardInteractive, ip)
|
user, err = executePreLoginHook(username, SSHLoginMethodKeyboardInteractive, ip, protocol)
|
||||||
} else {
|
} else {
|
||||||
user, err = provider.userExists(username)
|
user, err = provider.userExists(username)
|
||||||
}
|
}
|
||||||
|
@ -1474,7 +1442,7 @@ func doKeyboardInteractiveAuth(user User, authHook string, client ssh.KeyboardIn
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPreLoginHookResponse(loginMethod, ip string, userAsJSON []byte) ([]byte, error) {
|
func getPreLoginHookResponse(loginMethod, ip, protocol string, userAsJSON []byte) ([]byte, error) {
|
||||||
if strings.HasPrefix(config.PreLoginHook, "http") {
|
if strings.HasPrefix(config.PreLoginHook, "http") {
|
||||||
var url *url.URL
|
var url *url.URL
|
||||||
var result []byte
|
var result []byte
|
||||||
|
@ -1486,6 +1454,7 @@ func getPreLoginHookResponse(loginMethod, ip string, userAsJSON []byte) ([]byte,
|
||||||
q := url.Query()
|
q := url.Query()
|
||||||
q.Add("login_method", loginMethod)
|
q.Add("login_method", loginMethod)
|
||||||
q.Add("ip", ip)
|
q.Add("ip", ip)
|
||||||
|
q.Add("protocol", protocol)
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
httpClient := httpclient.GetHTTPClient()
|
httpClient := httpclient.GetHTTPClient()
|
||||||
resp, err := httpClient.Post(url.String(), "application/json", bytes.NewBuffer(userAsJSON))
|
resp, err := httpClient.Post(url.String(), "application/json", bytes.NewBuffer(userAsJSON))
|
||||||
|
@ -1509,11 +1478,12 @@ func getPreLoginHookResponse(loginMethod, ip string, userAsJSON []byte) ([]byte,
|
||||||
fmt.Sprintf("SFTPGO_LOGIND_USER=%v", string(userAsJSON)),
|
fmt.Sprintf("SFTPGO_LOGIND_USER=%v", string(userAsJSON)),
|
||||||
fmt.Sprintf("SFTPGO_LOGIND_METHOD=%v", loginMethod),
|
fmt.Sprintf("SFTPGO_LOGIND_METHOD=%v", loginMethod),
|
||||||
fmt.Sprintf("SFTPGO_LOGIND_IP=%v", ip),
|
fmt.Sprintf("SFTPGO_LOGIND_IP=%v", ip),
|
||||||
|
fmt.Sprintf("SFTPGO_LOGIND_PROTOCOL=%v", ip),
|
||||||
)
|
)
|
||||||
return cmd.Output()
|
return cmd.Output()
|
||||||
}
|
}
|
||||||
|
|
||||||
func executePreLoginHook(username, loginMethod, ip string) (User, error) {
|
func executePreLoginHook(username, loginMethod, ip, protocol string) (User, error) {
|
||||||
u, err := provider.userExists(username)
|
u, err := provider.userExists(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(*RecordNotFoundError); !ok {
|
if _, ok := err.(*RecordNotFoundError); !ok {
|
||||||
|
@ -1528,7 +1498,7 @@ func executePreLoginHook(username, loginMethod, ip string) (User, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return u, err
|
return u, err
|
||||||
}
|
}
|
||||||
out, err := getPreLoginHookResponse(loginMethod, ip, userAsJSON)
|
out, err := getPreLoginHookResponse(loginMethod, ip, protocol, userAsJSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return u, fmt.Errorf("Pre-login hook error: %v", err)
|
return u, fmt.Errorf("Pre-login hook error: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1567,7 +1537,70 @@ func executePreLoginHook(username, loginMethod, ip string) (User, error) {
|
||||||
return provider.userExists(username)
|
return provider.userExists(username)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip string) ([]byte, error) {
|
// ExecutePostLoginHook executes the post login hook if defined
|
||||||
|
func ExecutePostLoginHook(username, loginMethod, ip, protocol string, err error) {
|
||||||
|
if len(config.PostLoginHook) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if config.PostLoginScope == 1 && err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if config.PostLoginScope == 2 && err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(username, loginMethod, ip, protocol string, err error) {
|
||||||
|
status := 0
|
||||||
|
if err == nil {
|
||||||
|
status = 1
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(config.PostLoginHook, "http") {
|
||||||
|
var url *url.URL
|
||||||
|
url, err := url.Parse(config.PostLoginHook)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelDebug, "Invalid post-login hook %#v", config.PostLoginHook)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
postReq := make(map[string]interface{})
|
||||||
|
postReq["username"] = username
|
||||||
|
postReq["login_method"] = loginMethod
|
||||||
|
postReq["ip"] = ip
|
||||||
|
postReq["protocol"] = protocol
|
||||||
|
postReq["status"] = status
|
||||||
|
|
||||||
|
postAsJSON, err := json.Marshal(postReq)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelWarn, "error serializing post login request: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startTime := time.Now()
|
||||||
|
respCode := 0
|
||||||
|
httpClient := httpclient.GetHTTPClient()
|
||||||
|
resp, err := httpClient.Post(url.String(), "application/json", bytes.NewBuffer(postAsJSON))
|
||||||
|
if err == nil {
|
||||||
|
respCode = resp.StatusCode
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
providerLog(logger.LevelDebug, "post login hook executed, response code: %v, elapsed: %v err: %v",
|
||||||
|
respCode, time.Since(startTime), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
cmd := exec.CommandContext(ctx, config.PostLoginHook)
|
||||||
|
cmd.Env = append(os.Environ(),
|
||||||
|
fmt.Sprintf("SFTPGO_LOGIND_USER=%v", username),
|
||||||
|
fmt.Sprintf("SFTPGO_LOGIND_IP=%v", ip),
|
||||||
|
fmt.Sprintf("SFTPGO_LOGIND_METHOD=%v", loginMethod),
|
||||||
|
fmt.Sprintf("SFTPGO_LOGIND_STATUS=%v", status),
|
||||||
|
fmt.Sprintf("SFTPGO_LOGIND_PROTOCOL=%v", protocol))
|
||||||
|
startTime := time.Now()
|
||||||
|
err = cmd.Run()
|
||||||
|
providerLog(logger.LevelDebug, "post login hook executed, elapsed %v err: %v", time.Since(startTime), err)
|
||||||
|
}(username, loginMethod, ip, protocol, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, protocol string) ([]byte, error) {
|
||||||
if strings.HasPrefix(config.ExternalAuthHook, "http") {
|
if strings.HasPrefix(config.ExternalAuthHook, "http") {
|
||||||
var url *url.URL
|
var url *url.URL
|
||||||
var result []byte
|
var result []byte
|
||||||
|
@ -1582,6 +1615,7 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip s
|
||||||
authRequest["ip"] = ip
|
authRequest["ip"] = ip
|
||||||
authRequest["password"] = password
|
authRequest["password"] = password
|
||||||
authRequest["public_key"] = pkey
|
authRequest["public_key"] = pkey
|
||||||
|
authRequest["protocol"] = protocol
|
||||||
authRequest["keyboard_interactive"] = keyboardInteractive
|
authRequest["keyboard_interactive"] = keyboardInteractive
|
||||||
authRequestAsJSON, err := json.Marshal(authRequest)
|
authRequestAsJSON, err := json.Marshal(authRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1607,11 +1641,12 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip s
|
||||||
fmt.Sprintf("SFTPGO_AUTHD_IP=%v", ip),
|
fmt.Sprintf("SFTPGO_AUTHD_IP=%v", ip),
|
||||||
fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%v", password),
|
fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%v", password),
|
||||||
fmt.Sprintf("SFTPGO_AUTHD_PUBLIC_KEY=%v", pkey),
|
fmt.Sprintf("SFTPGO_AUTHD_PUBLIC_KEY=%v", pkey),
|
||||||
|
fmt.Sprintf("SFTPGO_AUTHD_PROTOCOL=%v", protocol),
|
||||||
fmt.Sprintf("SFTPGO_AUTHD_KEYBOARD_INTERACTIVE=%v", keyboardInteractive))
|
fmt.Sprintf("SFTPGO_AUTHD_KEYBOARD_INTERACTIVE=%v", keyboardInteractive))
|
||||||
return cmd.Output()
|
return cmd.Output()
|
||||||
}
|
}
|
||||||
|
|
||||||
func doExternalAuth(username, password string, pubKey []byte, keyboardInteractive, ip string) (User, error) {
|
func doExternalAuth(username, password string, pubKey []byte, keyboardInteractive, ip, protocol string) (User, error) {
|
||||||
var user User
|
var user User
|
||||||
pkey := ""
|
pkey := ""
|
||||||
if len(pubKey) > 0 {
|
if len(pubKey) > 0 {
|
||||||
|
@ -1621,7 +1656,7 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
|
||||||
}
|
}
|
||||||
pkey = string(ssh.MarshalAuthorizedKey(k))
|
pkey = string(ssh.MarshalAuthorizedKey(k))
|
||||||
}
|
}
|
||||||
out, err := getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip)
|
out, err := getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, protocol)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, fmt.Errorf("External auth error: %v", err)
|
return user, fmt.Errorf("External auth error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,13 +48,12 @@ const (
|
||||||
|
|
||||||
// Available login methods
|
// Available login methods
|
||||||
const (
|
const (
|
||||||
|
LoginMethodNoAuthTryed = "no_auth_tryed"
|
||||||
|
LoginMethodPassword = "password"
|
||||||
SSHLoginMethodPublicKey = "publickey"
|
SSHLoginMethodPublicKey = "publickey"
|
||||||
SSHLoginMethodPassword = "password"
|
|
||||||
SSHLoginMethodKeyboardInteractive = "keyboard-interactive"
|
SSHLoginMethodKeyboardInteractive = "keyboard-interactive"
|
||||||
SSHLoginMethodKeyAndPassword = "publickey+password"
|
SSHLoginMethodKeyAndPassword = "publickey+password"
|
||||||
SSHLoginMethodKeyAndKeyboardInt = "publickey+keyboard-interactive"
|
SSHLoginMethodKeyAndKeyboardInt = "publickey+keyboard-interactive"
|
||||||
FTPLoginMethodPassword = "ftp-password"
|
|
||||||
WebDavLoginMethodPassword = "dav-password"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -371,7 +370,7 @@ func (u *User) GetNextAuthMethods(partialSuccessMethods []string) []string {
|
||||||
}
|
}
|
||||||
for _, method := range u.GetAllowedLoginMethods() {
|
for _, method := range u.GetAllowedLoginMethods() {
|
||||||
if method == SSHLoginMethodKeyAndPassword {
|
if method == SSHLoginMethodKeyAndPassword {
|
||||||
methods = append(methods, SSHLoginMethodPassword)
|
methods = append(methods, LoginMethodPassword)
|
||||||
}
|
}
|
||||||
if method == SSHLoginMethodKeyAndKeyboardInt {
|
if method == SSHLoginMethodKeyAndKeyboardInt {
|
||||||
methods = append(methods, SSHLoginMethodKeyboardInteractive)
|
methods = append(methods, SSHLoginMethodKeyboardInteractive)
|
||||||
|
|
|
@ -27,6 +27,7 @@ The external program can also read the following environment variables:
|
||||||
- `SFTPGO_ACTION_BUCKET`, non-empty for S3 and GCS backends
|
- `SFTPGO_ACTION_BUCKET`, non-empty for S3 and GCS backends
|
||||||
- `SFTPGO_ACTION_ENDPOINT`, non-empty for S3 backend if configured
|
- `SFTPGO_ACTION_ENDPOINT`, non-empty for S3 backend if configured
|
||||||
- `SFTPGO_ACTION_STATUS`, integer. 0 means a generic error occurred. 1 means no error, 2 means quota exceeded error
|
- `SFTPGO_ACTION_STATUS`, integer. 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`
|
||||||
|
|
||||||
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.
|
||||||
|
@ -43,6 +44,7 @@ If the `hook` defines an HTTP URL then this URL will be invoked as HTTP POST. Th
|
||||||
- `bucket`, not null for S3 and GCS backends
|
- `bucket`, not null for S3 and GCS backends
|
||||||
- `endpoint`, not null for S3 backend if configured
|
- `endpoint`, not null for S3 backend if configured
|
||||||
- `status`, integer. 0 means a generic error occurred. 1 means no error, 2 means quota exceeded error
|
- `status`, integer. 0 means a generic error occurred. 1 means no error, 2 means quota exceeded error
|
||||||
|
- `protocol`, string. Possible values are `SSH`, `FTP`, `DAV`
|
||||||
|
|
||||||
The HTTP request will use the global configuration for HTTP clients.
|
The HTTP request will use the global configuration for HTTP clients.
|
||||||
|
|
||||||
|
|
|
@ -8,13 +8,14 @@ The external program can read the following environment variables to get info ab
|
||||||
- `SFTPGO_LOGIND_USER`, it contains the user trying to login serialized as JSON. A JSON serialized user id equal to zero means the user does not exists inside SFTPGo
|
- `SFTPGO_LOGIND_USER`, it contains the user trying to login serialized as JSON. A JSON serialized user id equal to zero means the user does not exists inside SFTPGo
|
||||||
- `SFTPGO_LOGIND_METHOD`, possible values are: `password`, `publickey` and `keyboard-interactive`
|
- `SFTPGO_LOGIND_METHOD`, possible values are: `password`, `publickey` and `keyboard-interactive`
|
||||||
- `SFTPGO_LOGIND_IP`, ip address of the user trying to login
|
- `SFTPGO_LOGIND_IP`, ip address of the user trying to login
|
||||||
|
- `SFTPGO_LOGIND_PROTOCOL`, possible values are `SSH`, `FTP`, `DAV`
|
||||||
|
|
||||||
The program must write, on its the standard output:
|
The program must write, on its the standard output:
|
||||||
|
|
||||||
- an empty string (or no response at all) if the user should not be created/updated
|
- an empty string (or no response at all) if the user should not be created/updated
|
||||||
- or the SFTPGo user, JSON serialized, if you want create or update the given user
|
- or the SFTPGo user, JSON serialized, if you want create or update the given user
|
||||||
|
|
||||||
If the hook is an HTTP URL then it will be invoked as HTTP POST. The login method and the ip address of the user trying to login are added to the query string, for example `<http_url>?login_method=password&ip=1.2.3.4`.
|
If the hook is an HTTP URL then it will be invoked as HTTP POST. The login method, the used protocol and the ip address of the user trying to login are added to the query string, for example `<http_url>?login_method=password&ip=1.2.3.4&protocol=SSH`.
|
||||||
The request body will contain the user trying to login serialized as JSON. If no modification is needed the HTTP response code must be 204, otherwise the response code must be 200 and the response body a valid SFTPGo user serialized as JSON.
|
The request body will contain the user trying to login serialized as JSON. If no modification is needed the HTTP response code must be 204, otherwise the response code must be 200 and the response body a valid SFTPGo user serialized as JSON.
|
||||||
|
|
||||||
Actions defined for user's updates will not be executed in this case.
|
Actions defined for user's updates will not be executed in this case.
|
||||||
|
|
|
@ -6,6 +6,7 @@ The external program can read the following environment variables to get info ab
|
||||||
|
|
||||||
- `SFTPGO_AUTHD_USERNAME`
|
- `SFTPGO_AUTHD_USERNAME`
|
||||||
- `SFTPGO_AUTHD_IP`
|
- `SFTPGO_AUTHD_IP`
|
||||||
|
- `SFTPGO_AUTHD_PROTOCOL`, possible values are `SSH`, `FTP`, `DAV`
|
||||||
- `SFTPGO_AUTHD_PASSWORD`, not empty for password authentication
|
- `SFTPGO_AUTHD_PASSWORD`, not empty for password authentication
|
||||||
- `SFTPGO_AUTHD_PUBLIC_KEY`, not empty for public key authentication
|
- `SFTPGO_AUTHD_PUBLIC_KEY`, not empty for public key authentication
|
||||||
- `SFTPGO_AUTHD_KEYBOARD_INTERACTIVE`, not empty for keyboard interactive authentication
|
- `SFTPGO_AUTHD_KEYBOARD_INTERACTIVE`, not empty for keyboard interactive authentication
|
||||||
|
@ -17,6 +18,7 @@ If the hook is an HTTP URL then it will be invoked as HTTP POST. The request bod
|
||||||
|
|
||||||
- `username`
|
- `username`
|
||||||
- `ip`
|
- `ip`
|
||||||
|
- `protocol`, possible values are `SSH`, `FTP`, `DAV`
|
||||||
- `password`, not empty for password authentication
|
- `password`, not empty for password authentication
|
||||||
- `public_key`, not empty for public key authentication
|
- `public_key`, not empty for public key authentication
|
||||||
- `keyboard_interactive`, not empty for keyboard interactive authentication
|
- `keyboard_interactive`, not empty for keyboard interactive authentication
|
||||||
|
|
|
@ -120,6 +120,8 @@ The configuration file contains the following sections:
|
||||||
- `credentials_path`, string. It defines the directory for storing user provided credential files such as Google Cloud Storage credentials. This can be an absolute path or a path relative to the config dir
|
- `credentials_path`, string. It defines the directory for storing user provided credential files such as Google Cloud Storage credentials. This can be an absolute path or a path relative to the config dir
|
||||||
- `pre_login_program`, string. Deprecated, please use `pre_login_hook`.
|
- `pre_login_program`, string. Deprecated, please use `pre_login_hook`.
|
||||||
- `pre_login_hook`, string. Absolute path to an external program or an HTTP URL to invoke to modify user details just before the login. See [Dynamic user modification](./dynamic-user-mod.md) for more details. Leave empty to disable.
|
- `pre_login_hook`, string. Absolute path to an external program or an HTTP URL to invoke to modify user details just before the login. See [Dynamic user modification](./dynamic-user-mod.md) for more details. Leave empty to disable.
|
||||||
|
- `post_login_hook`, string. Absolute path to an external program or an HTTP URL to invoke to notify a successul or failed login. See [Post-login hook](./post-login-hook.md) for more details. Leave empty to disable.
|
||||||
|
- `post_login_scope`, defines the scope for the post-login hook. 0 means notify both failed and successful logins. 1 means notify failed logins. 2 means notify successful logins.
|
||||||
- **"httpd"**, the configuration for the HTTP server used to serve REST API and to expose the built-in web interface
|
- **"httpd"**, the configuration for the HTTP server used to serve REST API and to expose the built-in web interface
|
||||||
- `bind_port`, integer. The port used for serving HTTP requests. Set to 0 to disable HTTP server. Default: 8080
|
- `bind_port`, integer. The port used for serving HTTP requests. Set to 0 to disable HTTP server. Default: 8080
|
||||||
- `bind_address`, string. Leave blank to listen on all available network interfaces. Default: "127.0.0.1"
|
- `bind_address`, string. Leave blank to listen on all available network interfaces. Default: "127.0.0.1"
|
||||||
|
|
|
@ -50,5 +50,6 @@ The logs can be divided into the following categories:
|
||||||
- `level` string
|
- `level` string
|
||||||
- `username`, string. Can be empty if the connection is closed before an authentication attempt
|
- `username`, string. Can be empty if the connection is closed before an authentication attempt
|
||||||
- `client_ip` string.
|
- `client_ip` string.
|
||||||
|
- `protocol` string. Possible values are `SSH`, `FTP`, `DAV`
|
||||||
- `login_type` string. Can be `publickey`, `password`, `keyboard-interactive`, `publickey+password`, `publickey+keyboard-interactive` or `no_auth_tryed`
|
- `login_type` string. Can be `publickey`, `password`, `keyboard-interactive`, `publickey+password`, `publickey+keyboard-interactive` or `no_auth_tryed`
|
||||||
- `error` string. Optional error description
|
- `error` string. Optional error description
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
# Post connect hook
|
# Post-connect hook
|
||||||
|
|
||||||
This hook is executed as soon as a new connection is estabilished. It notifies the connection's IP address and protocol. Based on the received response, the connection is accepted or rejected. This way you can implement your own blacklist/whitelist of IP addresses.
|
This hook is executed as soon as a new connection is estabilished. It notifies the connection's IP address and protocol. Based on the received response, the connection is accepted or rejected. Combining this hook with the [Post-login hook](./post-login-hook.md) you can implement your own (even for Protocol) blacklist/whitelist of IP addresses.
|
||||||
Please keep in mind that you can easily configure specialized program such as [Fail2ban](http://www.fail2ban.org/) for brute force protection.
|
|
||||||
|
|
||||||
The `post_connect_hook` can be defined as the absolute path of your program or an HTTP URL.
|
Please keep in mind that you can easily configure specialized program such as [Fail2ban](http://www.fail2ban.org/) for brute force protection. Executing a hook for each connection can be heavy.
|
||||||
|
|
||||||
|
The `post-connect-hook` can be defined as the absolute path of your program or an HTTP URL.
|
||||||
|
|
||||||
If the hook defines an external program it can reads the following environment variables:
|
If the hook defines an external program it can reads the following environment variables:
|
||||||
|
|
||||||
|
|
36
docs/post-login-hook.md
Normal file
36
docs/post-login-hook.md
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# Post-login hook
|
||||||
|
|
||||||
|
This hook is executed after a login or after closing a connection for authentication timeout. Defining an appropriate `post_login_scope` you can get notifications for failed logins, successful logins or both.
|
||||||
|
|
||||||
|
Combining this hook with the [Post-connect hook](./post-connect-hook.md) you can implement your own (even for Protocol) blacklist/whitelist of IP addresses.
|
||||||
|
|
||||||
|
Please keep in mind that you can easily configure specialized program such as [Fail2ban](http://www.fail2ban.org/) for brute force protection. Executing a hook after each login can be heavy.
|
||||||
|
|
||||||
|
The `post-login-hook` can be defined as the absolute path of your program or an HTTP URL.
|
||||||
|
|
||||||
|
If the hook defines an external program it can reads the following environment variables:
|
||||||
|
|
||||||
|
- `SFTPGO_LOGIND_USER`, username, can be empty if the connection is closed for authentication timeout
|
||||||
|
- `SFTPGO_LOGIND_IP`
|
||||||
|
- `SFTPGO_LOGIND_METHOD`, possible values are `publickey`, `password`, `keyboard-interactive`, `publickey+password`, `publickey+keyboard-interactive` or `no_auth_tryed`
|
||||||
|
- `SFTPGO_LOGIND_STATUS`, 1 means login OK, 0 login KO
|
||||||
|
- `SFTPGO_LOGIND_PROTOCOL`, possible values are `SSH`, `FTP`, `DAV`
|
||||||
|
|
||||||
|
Previous global environment variables aren't cleared when the script is called.
|
||||||
|
The program must finish within 20 seconds.
|
||||||
|
|
||||||
|
If the hook is an HTTP URL then it will be invoked as HTTP POST. The request body will contain a JSON serialized struct with the following fields:
|
||||||
|
|
||||||
|
- `username`
|
||||||
|
- `login_method`
|
||||||
|
- `ip`
|
||||||
|
- `protocol`
|
||||||
|
- `status`
|
||||||
|
|
||||||
|
The HTTP request will use the global configuration for HTTP clients.
|
||||||
|
|
||||||
|
The `post_login_scope` supports the following configuration values:
|
||||||
|
|
||||||
|
- `0` means notify both failed and successful logins
|
||||||
|
- `1` means notify failed logins. Connections closed for authentication timeout are notified as failed connections. You will get an empty username in this case
|
||||||
|
- `2` means notify successful logins
|
|
@ -118,7 +118,7 @@ func (s *Server) ClientDisconnected(cc ftpserver.ClientContext) {
|
||||||
// AuthUser authenticates the user and selects an handling driver
|
// AuthUser authenticates the user and selects an handling driver
|
||||||
func (s *Server) AuthUser(cc ftpserver.ClientContext, username, password string) (ftpserver.ClientDriver, error) {
|
func (s *Server) AuthUser(cc ftpserver.ClientContext, username, password string) (ftpserver.ClientDriver, error) {
|
||||||
remoteAddr := cc.RemoteAddr().String()
|
remoteAddr := cc.RemoteAddr().String()
|
||||||
user, err := dataprovider.CheckUserAndPass(username, password, utils.GetIPFromRemoteAddress(remoteAddr))
|
user, err := dataprovider.CheckUserAndPass(username, password, utils.GetIPFromRemoteAddress(remoteAddr), common.ProtocolFTP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
updateLoginMetrics(username, remoteAddr, err)
|
updateLoginMetrics(username, remoteAddr, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -189,10 +189,12 @@ func (s *Server) validateUser(user dataprovider.User, cc ftpserver.ClientContext
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLoginMetrics(username, remoteAddress string, err error) {
|
func updateLoginMetrics(username, remoteAddress string, err error) {
|
||||||
metrics.AddLoginAttempt(dataprovider.FTPLoginMethodPassword)
|
metrics.AddLoginAttempt(dataprovider.LoginMethodPassword)
|
||||||
|
ip := utils.GetIPFromRemoteAddress(remoteAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ConnectionFailedLog(username, utils.GetIPFromRemoteAddress(remoteAddress),
|
logger.ConnectionFailedLog(username, ip, dataprovider.LoginMethodPassword,
|
||||||
dataprovider.FTPLoginMethodPassword, err.Error())
|
common.ProtocolFTP, err.Error())
|
||||||
}
|
}
|
||||||
metrics.AddLoginResult(dataprovider.FTPLoginMethodPassword, err)
|
metrics.AddLoginResult(dataprovider.LoginMethodPassword, err)
|
||||||
|
dataprovider.ExecutePostLoginHook(username, dataprovider.LoginMethodPassword, ip, common.ProtocolFTP, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -622,7 +622,7 @@ func TestUpdateUser(t *testing.T) {
|
||||||
user.Permissions["/subdir"] = []string{dataprovider.PermListItems, dataprovider.PermUpload}
|
user.Permissions["/subdir"] = []string{dataprovider.PermListItems, dataprovider.PermUpload}
|
||||||
user.Filters.AllowedIP = []string{"192.168.1.0/24", "192.168.2.0/24"}
|
user.Filters.AllowedIP = []string{"192.168.1.0/24", "192.168.2.0/24"}
|
||||||
user.Filters.DeniedIP = []string{"192.168.3.0/24", "192.168.4.0/24"}
|
user.Filters.DeniedIP = []string{"192.168.3.0/24", "192.168.4.0/24"}
|
||||||
user.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPassword}
|
user.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword}
|
||||||
user.Filters.FileExtensions = append(user.Filters.FileExtensions, dataprovider.ExtensionsFilter{
|
user.Filters.FileExtensions = append(user.Filters.FileExtensions, dataprovider.ExtensionsFilter{
|
||||||
Path: "/subdir",
|
Path: "/subdir",
|
||||||
AllowedExtensions: []string{".zip", ".rar"},
|
AllowedExtensions: []string{".zip", ".rar"},
|
||||||
|
|
|
@ -165,7 +165,7 @@ func TestCompareUserFilters(t *testing.T) {
|
||||||
actual.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey}
|
actual.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey}
|
||||||
err = checkUser(expected, actual)
|
err = checkUser(expected, actual)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
expected.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPassword}
|
expected.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword}
|
||||||
err = checkUser(expected, actual)
|
err = checkUser(expected, actual)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
expected.Filters.DeniedLoginMethods = []string{}
|
expected.Filters.DeniedLoginMethods = []string{}
|
||||||
|
|
|
@ -183,13 +183,14 @@ func CommandLog(command, path, target, user, fileMode, connectionID, protocol st
|
||||||
// A connection can fail for an authentication error or other errors such as
|
// A connection can fail for an authentication error or other errors such as
|
||||||
// a client abort or a time out if the login does not happen in two minutes.
|
// a client abort or a time out if the login does not happen in two minutes.
|
||||||
// These logs are useful for better integration with Fail2ban and similar tools.
|
// These logs are useful for better integration with Fail2ban and similar tools.
|
||||||
func ConnectionFailedLog(user, ip, loginType, errorString string) {
|
func ConnectionFailedLog(user, ip, loginType, protocol, errorString string) {
|
||||||
logger.Debug().
|
logger.Debug().
|
||||||
Timestamp().
|
Timestamp().
|
||||||
Str("sender", "connection_failed").
|
Str("sender", "connection_failed").
|
||||||
Str("client_ip", ip).
|
Str("client_ip", ip).
|
||||||
Str("username", user).
|
Str("username", user).
|
||||||
Str("login_type", loginType).
|
Str("login_type", loginType).
|
||||||
|
Str("protocol", protocol).
|
||||||
Str("error", errorString).
|
Str("error", errorString).
|
||||||
Msg("")
|
Msg("")
|
||||||
}
|
}
|
||||||
|
|
|
@ -350,7 +350,7 @@ func TestUploadFiles(t *testing.T) {
|
||||||
func TestWithInvalidHome(t *testing.T) {
|
func TestWithInvalidHome(t *testing.T) {
|
||||||
u := dataprovider.User{}
|
u := dataprovider.User{}
|
||||||
u.HomeDir = "home_rel_path" //nolint:goconst
|
u.HomeDir = "home_rel_path" //nolint:goconst
|
||||||
_, err := loginUser(u, dataprovider.SSHLoginMethodPassword, "", nil)
|
_, err := loginUser(u, dataprovider.LoginMethodPassword, "", nil)
|
||||||
assert.Error(t, err, "login a user with an invalid home_dir must fail")
|
assert.Error(t, err, "login a user with an invalid home_dir must fail")
|
||||||
|
|
||||||
u.HomeDir = os.TempDir()
|
u.HomeDir = os.TempDir()
|
||||||
|
|
|
@ -272,8 +272,10 @@ func (c Configuration) AcceptInboundConnection(conn net.Conn, config *ssh.Server
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Debug(logSender, "", "failed to accept an incoming connection: %v", err)
|
logger.Debug(logSender, "", "failed to accept an incoming connection: %v", err)
|
||||||
if _, ok := err.(*ssh.ServerAuthError); !ok {
|
if _, ok := err.(*ssh.ServerAuthError); !ok {
|
||||||
logger.ConnectionFailedLog("", utils.GetIPFromRemoteAddress(remoteAddr.String()), "no_auth_tryed", err.Error())
|
ip := utils.GetIPFromRemoteAddress(remoteAddr.String())
|
||||||
|
logger.ConnectionFailedLog("", ip, dataprovider.LoginMethodNoAuthTryed, common.ProtocolSSH, err.Error())
|
||||||
metrics.AddNoAuthTryed()
|
metrics.AddNoAuthTryed()
|
||||||
|
dataprovider.ExecutePostLoginHook("", dataprovider.LoginMethodNoAuthTryed, ip, common.ProtocolSSH, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -596,7 +598,7 @@ func (c Configuration) validatePublicKeyCredentials(conn ssh.ConnMetadata, pubKe
|
||||||
certPerm = &cert.Permissions
|
certPerm = &cert.Permissions
|
||||||
}
|
}
|
||||||
ipAddr := utils.GetIPFromRemoteAddress(conn.RemoteAddr().String())
|
ipAddr := utils.GetIPFromRemoteAddress(conn.RemoteAddr().String())
|
||||||
if user, keyID, err = dataprovider.CheckUserAndPubKey(conn.User(), pubKey.Marshal(), ipAddr); err == nil {
|
if user, keyID, err = dataprovider.CheckUserAndPubKey(conn.User(), pubKey.Marshal(), ipAddr, common.ProtocolSSH); err == nil {
|
||||||
if user.IsPartialAuth(method) {
|
if user.IsPartialAuth(method) {
|
||||||
logger.Debug(logSender, connectionID, "user %#v authenticated with partial success", conn.User())
|
logger.Debug(logSender, connectionID, "user %#v authenticated with partial success", conn.User())
|
||||||
return certPerm, ssh.ErrPartialSuccess
|
return certPerm, ssh.ErrPartialSuccess
|
||||||
|
@ -622,12 +624,12 @@ func (c Configuration) validatePasswordCredentials(conn ssh.ConnMetadata, pass [
|
||||||
var user dataprovider.User
|
var user dataprovider.User
|
||||||
var sshPerm *ssh.Permissions
|
var sshPerm *ssh.Permissions
|
||||||
|
|
||||||
method := dataprovider.SSHLoginMethodPassword
|
method := dataprovider.LoginMethodPassword
|
||||||
if len(conn.PartialSuccessMethods()) == 1 {
|
if len(conn.PartialSuccessMethods()) == 1 {
|
||||||
method = dataprovider.SSHLoginMethodKeyAndPassword
|
method = dataprovider.SSHLoginMethodKeyAndPassword
|
||||||
}
|
}
|
||||||
ipAddr := utils.GetIPFromRemoteAddress(conn.RemoteAddr().String())
|
ipAddr := utils.GetIPFromRemoteAddress(conn.RemoteAddr().String())
|
||||||
if user, err = dataprovider.CheckUserAndPass(conn.User(), string(pass), ipAddr); err == nil {
|
if user, err = dataprovider.CheckUserAndPass(conn.User(), string(pass), ipAddr, common.ProtocolSSH); err == nil {
|
||||||
sshPerm, err = loginUser(user, method, "", conn)
|
sshPerm, err = loginUser(user, method, "", conn)
|
||||||
}
|
}
|
||||||
updateLoginMetrics(conn, method, err)
|
updateLoginMetrics(conn, method, err)
|
||||||
|
@ -644,7 +646,8 @@ func (c Configuration) validateKeyboardInteractiveCredentials(conn ssh.ConnMetad
|
||||||
method = dataprovider.SSHLoginMethodKeyAndKeyboardInt
|
method = dataprovider.SSHLoginMethodKeyAndKeyboardInt
|
||||||
}
|
}
|
||||||
ipAddr := utils.GetIPFromRemoteAddress(conn.RemoteAddr().String())
|
ipAddr := utils.GetIPFromRemoteAddress(conn.RemoteAddr().String())
|
||||||
if user, err = dataprovider.CheckKeyboardInteractiveAuth(conn.User(), c.KeyboardInteractiveHook, client, ipAddr); err == nil {
|
if user, err = dataprovider.CheckKeyboardInteractiveAuth(conn.User(), c.KeyboardInteractiveHook, client,
|
||||||
|
ipAddr, common.ProtocolSSH); err == nil {
|
||||||
sshPerm, err = loginUser(user, method, "", conn)
|
sshPerm, err = loginUser(user, method, "", conn)
|
||||||
}
|
}
|
||||||
updateLoginMetrics(conn, method, err)
|
updateLoginMetrics(conn, method, err)
|
||||||
|
@ -653,8 +656,10 @@ func (c Configuration) validateKeyboardInteractiveCredentials(conn ssh.ConnMetad
|
||||||
|
|
||||||
func updateLoginMetrics(conn ssh.ConnMetadata, method string, err error) {
|
func updateLoginMetrics(conn ssh.ConnMetadata, method string, err error) {
|
||||||
metrics.AddLoginAttempt(method)
|
metrics.AddLoginAttempt(method)
|
||||||
|
ip := utils.GetIPFromRemoteAddress(conn.RemoteAddr().String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ConnectionFailedLog(conn.User(), utils.GetIPFromRemoteAddress(conn.RemoteAddr().String()), method, err.Error())
|
logger.ConnectionFailedLog(conn.User(), ip, method, common.ProtocolSSH, err.Error())
|
||||||
}
|
}
|
||||||
metrics.AddLoginResult(method, err)
|
metrics.AddLoginResult(method, err)
|
||||||
|
dataprovider.ExecutePostLoginHook(conn.User(), method, ip, common.ProtocolSSH, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -940,7 +940,7 @@ func TestMultiStepLoginKeyAndPwd(t *testing.T) {
|
||||||
u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, []string{
|
u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, []string{
|
||||||
dataprovider.SSHLoginMethodKeyAndKeyboardInt,
|
dataprovider.SSHLoginMethodKeyAndKeyboardInt,
|
||||||
dataprovider.SSHLoginMethodPublicKey,
|
dataprovider.SSHLoginMethodPublicKey,
|
||||||
dataprovider.SSHLoginMethodPassword,
|
dataprovider.LoginMethodPassword,
|
||||||
dataprovider.SSHLoginMethodKeyboardInteractive,
|
dataprovider.SSHLoginMethodKeyboardInteractive,
|
||||||
}...)
|
}...)
|
||||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
|
@ -984,7 +984,7 @@ func TestMultiStepLoginKeyAndKeyInt(t *testing.T) {
|
||||||
u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, []string{
|
u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, []string{
|
||||||
dataprovider.SSHLoginMethodKeyAndPassword,
|
dataprovider.SSHLoginMethodKeyAndPassword,
|
||||||
dataprovider.SSHLoginMethodPublicKey,
|
dataprovider.SSHLoginMethodPublicKey,
|
||||||
dataprovider.SSHLoginMethodPassword,
|
dataprovider.LoginMethodPassword,
|
||||||
dataprovider.SSHLoginMethodKeyboardInteractive,
|
dataprovider.SSHLoginMethodKeyboardInteractive,
|
||||||
}...)
|
}...)
|
||||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
|
@ -1037,7 +1037,7 @@ func TestMultiStepLoginCertAndPwd(t *testing.T) {
|
||||||
u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, []string{
|
u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, []string{
|
||||||
dataprovider.SSHLoginMethodKeyAndKeyboardInt,
|
dataprovider.SSHLoginMethodKeyAndKeyboardInt,
|
||||||
dataprovider.SSHLoginMethodPublicKey,
|
dataprovider.SSHLoginMethodPublicKey,
|
||||||
dataprovider.SSHLoginMethodPassword,
|
dataprovider.LoginMethodPassword,
|
||||||
dataprovider.SSHLoginMethodKeyboardInteractive,
|
dataprovider.SSHLoginMethodKeyboardInteractive,
|
||||||
}...)
|
}...)
|
||||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
|
@ -1159,14 +1159,14 @@ func TestLoginInvalidFs(t *testing.T) {
|
||||||
|
|
||||||
func TestDeniedLoginMethods(t *testing.T) {
|
func TestDeniedLoginMethods(t *testing.T) {
|
||||||
u := getTestUser(true)
|
u := getTestUser(true)
|
||||||
u.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodPassword}
|
u.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.LoginMethodPassword}
|
||||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
client, err := getSftpClient(user, true)
|
client, err := getSftpClient(user, true)
|
||||||
if !assert.Error(t, err, "public key login is disabled, authentication must fail") {
|
if !assert.Error(t, err, "public key login is disabled, authentication must fail") {
|
||||||
client.Close()
|
client.Close()
|
||||||
}
|
}
|
||||||
user.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.SSHLoginMethodPassword}
|
user.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.LoginMethodPassword}
|
||||||
user, _, err = httpd.UpdateUser(user, http.StatusOK)
|
user, _, err = httpd.UpdateUser(user, http.StatusOK)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
client, err = getSftpClient(user, true)
|
client, err = getSftpClient(user, true)
|
||||||
|
@ -5177,7 +5177,7 @@ func TestUserAllowedLoginMethods(t *testing.T) {
|
||||||
assert.Equal(t, 0, len(allowedMethods))
|
assert.Equal(t, 0, len(allowedMethods))
|
||||||
|
|
||||||
user.Filters.DeniedLoginMethods = []string{
|
user.Filters.DeniedLoginMethods = []string{
|
||||||
dataprovider.SSHLoginMethodPassword,
|
dataprovider.LoginMethodPassword,
|
||||||
dataprovider.SSHLoginMethodPublicKey,
|
dataprovider.SSHLoginMethodPublicKey,
|
||||||
dataprovider.SSHLoginMethodKeyboardInteractive,
|
dataprovider.SSHLoginMethodKeyboardInteractive,
|
||||||
}
|
}
|
||||||
|
@ -5191,22 +5191,22 @@ func TestUserAllowedLoginMethods(t *testing.T) {
|
||||||
func TestUserPartialAuth(t *testing.T) {
|
func TestUserPartialAuth(t *testing.T) {
|
||||||
user := getTestUser(true)
|
user := getTestUser(true)
|
||||||
user.Filters.DeniedLoginMethods = []string{
|
user.Filters.DeniedLoginMethods = []string{
|
||||||
dataprovider.SSHLoginMethodPassword,
|
dataprovider.LoginMethodPassword,
|
||||||
dataprovider.SSHLoginMethodPublicKey,
|
dataprovider.SSHLoginMethodPublicKey,
|
||||||
dataprovider.SSHLoginMethodKeyboardInteractive,
|
dataprovider.SSHLoginMethodKeyboardInteractive,
|
||||||
}
|
}
|
||||||
assert.False(t, user.IsPartialAuth(dataprovider.SSHLoginMethodPassword))
|
assert.False(t, user.IsPartialAuth(dataprovider.LoginMethodPassword))
|
||||||
assert.False(t, user.IsPartialAuth(dataprovider.SSHLoginMethodKeyboardInteractive))
|
assert.False(t, user.IsPartialAuth(dataprovider.SSHLoginMethodKeyboardInteractive))
|
||||||
assert.True(t, user.IsPartialAuth(dataprovider.SSHLoginMethodPublicKey))
|
assert.True(t, user.IsPartialAuth(dataprovider.SSHLoginMethodPublicKey))
|
||||||
|
|
||||||
user.Filters.DeniedLoginMethods = []string{
|
user.Filters.DeniedLoginMethods = []string{
|
||||||
dataprovider.SSHLoginMethodPassword,
|
dataprovider.LoginMethodPassword,
|
||||||
dataprovider.SSHLoginMethodKeyboardInteractive,
|
dataprovider.SSHLoginMethodKeyboardInteractive,
|
||||||
}
|
}
|
||||||
assert.False(t, user.IsPartialAuth(dataprovider.SSHLoginMethodPublicKey))
|
assert.False(t, user.IsPartialAuth(dataprovider.SSHLoginMethodPublicKey))
|
||||||
|
|
||||||
user.Filters.DeniedLoginMethods = []string{
|
user.Filters.DeniedLoginMethods = []string{
|
||||||
dataprovider.SSHLoginMethodPassword,
|
dataprovider.LoginMethodPassword,
|
||||||
dataprovider.SSHLoginMethodPublicKey,
|
dataprovider.SSHLoginMethodPublicKey,
|
||||||
}
|
}
|
||||||
assert.False(t, user.IsPartialAuth(dataprovider.SSHLoginMethodPublicKey))
|
assert.False(t, user.IsPartialAuth(dataprovider.SSHLoginMethodPublicKey))
|
||||||
|
@ -5215,14 +5215,14 @@ func TestUserPartialAuth(t *testing.T) {
|
||||||
func TestUserGetNextAuthMethods(t *testing.T) {
|
func TestUserGetNextAuthMethods(t *testing.T) {
|
||||||
user := getTestUser(true)
|
user := getTestUser(true)
|
||||||
user.Filters.DeniedLoginMethods = []string{
|
user.Filters.DeniedLoginMethods = []string{
|
||||||
dataprovider.SSHLoginMethodPassword,
|
dataprovider.LoginMethodPassword,
|
||||||
dataprovider.SSHLoginMethodPublicKey,
|
dataprovider.SSHLoginMethodPublicKey,
|
||||||
dataprovider.SSHLoginMethodKeyboardInteractive,
|
dataprovider.SSHLoginMethodKeyboardInteractive,
|
||||||
}
|
}
|
||||||
methods := user.GetNextAuthMethods(nil)
|
methods := user.GetNextAuthMethods(nil)
|
||||||
assert.Equal(t, 0, len(methods))
|
assert.Equal(t, 0, len(methods))
|
||||||
|
|
||||||
methods = user.GetNextAuthMethods([]string{dataprovider.SSHLoginMethodPassword})
|
methods = user.GetNextAuthMethods([]string{dataprovider.LoginMethodPassword})
|
||||||
assert.Equal(t, 0, len(methods))
|
assert.Equal(t, 0, len(methods))
|
||||||
|
|
||||||
methods = user.GetNextAuthMethods([]string{dataprovider.SSHLoginMethodKeyboardInteractive})
|
methods = user.GetNextAuthMethods([]string{dataprovider.SSHLoginMethodKeyboardInteractive})
|
||||||
|
@ -5236,21 +5236,21 @@ func TestUserGetNextAuthMethods(t *testing.T) {
|
||||||
|
|
||||||
methods = user.GetNextAuthMethods([]string{dataprovider.SSHLoginMethodPublicKey})
|
methods = user.GetNextAuthMethods([]string{dataprovider.SSHLoginMethodPublicKey})
|
||||||
assert.Equal(t, 2, len(methods))
|
assert.Equal(t, 2, len(methods))
|
||||||
assert.True(t, utils.IsStringInSlice(dataprovider.SSHLoginMethodPassword, methods))
|
assert.True(t, utils.IsStringInSlice(dataprovider.LoginMethodPassword, methods))
|
||||||
assert.True(t, utils.IsStringInSlice(dataprovider.SSHLoginMethodKeyboardInteractive, methods))
|
assert.True(t, utils.IsStringInSlice(dataprovider.SSHLoginMethodKeyboardInteractive, methods))
|
||||||
|
|
||||||
user.Filters.DeniedLoginMethods = []string{
|
user.Filters.DeniedLoginMethods = []string{
|
||||||
dataprovider.SSHLoginMethodPassword,
|
dataprovider.LoginMethodPassword,
|
||||||
dataprovider.SSHLoginMethodPublicKey,
|
dataprovider.SSHLoginMethodPublicKey,
|
||||||
dataprovider.SSHLoginMethodKeyboardInteractive,
|
dataprovider.SSHLoginMethodKeyboardInteractive,
|
||||||
dataprovider.SSHLoginMethodKeyAndKeyboardInt,
|
dataprovider.SSHLoginMethodKeyAndKeyboardInt,
|
||||||
}
|
}
|
||||||
methods = user.GetNextAuthMethods([]string{dataprovider.SSHLoginMethodPublicKey})
|
methods = user.GetNextAuthMethods([]string{dataprovider.SSHLoginMethodPublicKey})
|
||||||
assert.Equal(t, 1, len(methods))
|
assert.Equal(t, 1, len(methods))
|
||||||
assert.True(t, utils.IsStringInSlice(dataprovider.SSHLoginMethodPassword, methods))
|
assert.True(t, utils.IsStringInSlice(dataprovider.LoginMethodPassword, methods))
|
||||||
|
|
||||||
user.Filters.DeniedLoginMethods = []string{
|
user.Filters.DeniedLoginMethods = []string{
|
||||||
dataprovider.SSHLoginMethodPassword,
|
dataprovider.LoginMethodPassword,
|
||||||
dataprovider.SSHLoginMethodPublicKey,
|
dataprovider.SSHLoginMethodPublicKey,
|
||||||
dataprovider.SSHLoginMethodKeyboardInteractive,
|
dataprovider.SSHLoginMethodKeyboardInteractive,
|
||||||
dataprovider.SSHLoginMethodKeyAndPassword,
|
dataprovider.SSHLoginMethodKeyAndPassword,
|
||||||
|
@ -5263,19 +5263,19 @@ func TestUserGetNextAuthMethods(t *testing.T) {
|
||||||
func TestUserIsLoginMethodAllowed(t *testing.T) {
|
func TestUserIsLoginMethodAllowed(t *testing.T) {
|
||||||
user := getTestUser(true)
|
user := getTestUser(true)
|
||||||
user.Filters.DeniedLoginMethods = []string{
|
user.Filters.DeniedLoginMethods = []string{
|
||||||
dataprovider.SSHLoginMethodPassword,
|
dataprovider.LoginMethodPassword,
|
||||||
dataprovider.SSHLoginMethodPublicKey,
|
dataprovider.SSHLoginMethodPublicKey,
|
||||||
dataprovider.SSHLoginMethodKeyboardInteractive,
|
dataprovider.SSHLoginMethodKeyboardInteractive,
|
||||||
}
|
}
|
||||||
assert.False(t, user.IsLoginMethodAllowed(dataprovider.SSHLoginMethodPassword, nil))
|
assert.False(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, nil))
|
||||||
assert.True(t, user.IsLoginMethodAllowed(dataprovider.SSHLoginMethodPassword, []string{dataprovider.SSHLoginMethodPublicKey}))
|
assert.True(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, []string{dataprovider.SSHLoginMethodPublicKey}))
|
||||||
assert.True(t, user.IsLoginMethodAllowed(dataprovider.SSHLoginMethodKeyboardInteractive, []string{dataprovider.SSHLoginMethodPublicKey}))
|
assert.True(t, user.IsLoginMethodAllowed(dataprovider.SSHLoginMethodKeyboardInteractive, []string{dataprovider.SSHLoginMethodPublicKey}))
|
||||||
|
|
||||||
user.Filters.DeniedLoginMethods = []string{
|
user.Filters.DeniedLoginMethods = []string{
|
||||||
dataprovider.SSHLoginMethodPublicKey,
|
dataprovider.SSHLoginMethodPublicKey,
|
||||||
dataprovider.SSHLoginMethodKeyboardInteractive,
|
dataprovider.SSHLoginMethodKeyboardInteractive,
|
||||||
}
|
}
|
||||||
assert.True(t, user.IsLoginMethodAllowed(dataprovider.SSHLoginMethodPassword, nil))
|
assert.True(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUserEmptySubDirPerms(t *testing.T) {
|
func TestUserEmptySubDirPerms(t *testing.T) {
|
||||||
|
|
|
@ -72,7 +72,9 @@
|
||||||
"external_auth_hook": "",
|
"external_auth_hook": "",
|
||||||
"external_auth_scope": 0,
|
"external_auth_scope": 0,
|
||||||
"credentials_path": "credentials",
|
"credentials_path": "credentials",
|
||||||
"pre_login_hook": ""
|
"pre_login_hook": "",
|
||||||
|
"post_login_hook": "",
|
||||||
|
"post_login_scope": 0
|
||||||
},
|
},
|
||||||
"httpd": {
|
"httpd": {
|
||||||
"bind_port": 8080,
|
"bind_port": 8080,
|
||||||
|
|
|
@ -141,7 +141,7 @@ func (s *webDavServer) authenticate(r *http.Request) (dataprovider.User, error)
|
||||||
if !ok {
|
if !ok {
|
||||||
return user, err401
|
return user, err401
|
||||||
}
|
}
|
||||||
user, err = dataprovider.CheckUserAndPass(username, password, utils.GetIPFromRemoteAddress(r.RemoteAddr))
|
user, err = dataprovider.CheckUserAndPass(username, password, utils.GetIPFromRemoteAddress(r.RemoteAddr), common.ProtocolWebDAV)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
updateLoginMetrics(username, r.RemoteAddr, err)
|
updateLoginMetrics(username, r.RemoteAddr, err)
|
||||||
return user, err
|
return user, err
|
||||||
|
@ -232,10 +232,11 @@ func checkRemoteAddress(r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLoginMetrics(username, remoteAddress string, err error) {
|
func updateLoginMetrics(username, remoteAddress string, err error) {
|
||||||
metrics.AddLoginAttempt(dataprovider.WebDavLoginMethodPassword)
|
metrics.AddLoginAttempt(dataprovider.LoginMethodPassword)
|
||||||
|
ip := utils.GetIPFromRemoteAddress(remoteAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ConnectionFailedLog(username, utils.GetIPFromRemoteAddress(remoteAddress),
|
logger.ConnectionFailedLog(username, ip, dataprovider.LoginMethodPassword, common.ProtocolWebDAV, err.Error())
|
||||||
dataprovider.WebDavLoginMethodPassword, err.Error())
|
|
||||||
}
|
}
|
||||||
metrics.AddLoginResult(dataprovider.WebDavLoginMethodPassword, err)
|
metrics.AddLoginResult(dataprovider.LoginMethodPassword, err)
|
||||||
|
dataprovider.ExecutePostLoginHook(username, dataprovider.LoginMethodPassword, ip, common.ProtocolWebDAV, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -652,8 +652,6 @@ func TestClientClose(t *testing.T) {
|
||||||
}, 1*time.Second, 50*time.Millisecond)
|
}, 1*time.Second, 50*time.Millisecond)
|
||||||
|
|
||||||
for _, stat := range common.Connections.GetStats() {
|
for _, stat := range common.Connections.GetStats() {
|
||||||
logger.DebugToConsole("close upload connection id %#v, active transfers: %v", stat.ConnectionID,
|
|
||||||
stat.GetTransfersAsString())
|
|
||||||
common.Connections.Close(stat.ConnectionID)
|
common.Connections.Close(stat.ConnectionID)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
@ -684,8 +682,6 @@ func TestClientClose(t *testing.T) {
|
||||||
}, 1*time.Second, 50*time.Millisecond)
|
}, 1*time.Second, 50*time.Millisecond)
|
||||||
|
|
||||||
for _, stat := range common.Connections.GetStats() {
|
for _, stat := range common.Connections.GetStats() {
|
||||||
logger.DebugToConsole("close download connection id %#v, active transfers: %v", stat.ConnectionID,
|
|
||||||
stat.GetTransfersAsString())
|
|
||||||
common.Connections.Close(stat.ConnectionID)
|
common.Connections.Close(stat.ConnectionID)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
Loading…
Reference in a new issue