mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-11-29 22:08:10 +02:00
Add client IP address to external auth, pre-login and keyboard interactive hooks
This commit is contained in:
@@ -94,7 +94,7 @@ var (
|
|||||||
QuotaScans ActiveScans
|
QuotaScans ActiveScans
|
||||||
idleTimeoutTicker *time.Ticker
|
idleTimeoutTicker *time.Ticker
|
||||||
idleTimeoutTickerDone chan bool
|
idleTimeoutTickerDone chan bool
|
||||||
supportedProcols = []string{ProtocolSFTP, ProtocolSCP, ProtocolSSH, ProtocolFTP}
|
supportedProtocols = []string{ProtocolSFTP, ProtocolSCP, ProtocolSSH, ProtocolFTP}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Initialize sets the common configuration
|
// Initialize sets the common configuration
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ type BaseConnection struct {
|
|||||||
// NewBaseConnection returns a new BaseConnection
|
// NewBaseConnection returns a new BaseConnection
|
||||||
func NewBaseConnection(ID, protocol string, user dataprovider.User, fs vfs.Fs) *BaseConnection {
|
func NewBaseConnection(ID, protocol string, user dataprovider.User, fs vfs.Fs) *BaseConnection {
|
||||||
connID := ID
|
connID := ID
|
||||||
if utils.IsStringInSlice(protocol, supportedProcols) {
|
if utils.IsStringInSlice(protocol, supportedProtocols) {
|
||||||
connID = fmt.Sprintf("%v_%v", protocol, ID)
|
connID = fmt.Sprintf("%v_%v", protocol, ID)
|
||||||
}
|
}
|
||||||
return &BaseConnection{
|
return &BaseConnection{
|
||||||
@@ -79,7 +79,7 @@ func (c *BaseConnection) GetProtocol() string {
|
|||||||
// 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
|
||||||
if utils.IsStringInSlice(c.protocol, supportedProcols) {
|
if utils.IsStringInSlice(c.protocol, supportedProtocols) {
|
||||||
c.ID = fmt.Sprintf("%v_%v", c.protocol, c.ID)
|
c.ID = fmt.Sprintf("%v_%v", c.protocol, c.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1030,7 +1030,7 @@ func TestUpdateQuotaMoveVFolders(t *testing.T) {
|
|||||||
func TestErrorsMapping(t *testing.T) {
|
func TestErrorsMapping(t *testing.T) {
|
||||||
fs := vfs.NewOsFs("", os.TempDir(), nil)
|
fs := vfs.NewOsFs("", os.TempDir(), nil)
|
||||||
conn := NewBaseConnection("", ProtocolSFTP, dataprovider.User{}, fs)
|
conn := NewBaseConnection("", ProtocolSFTP, dataprovider.User{}, fs)
|
||||||
for _, protocol := range supportedProcols {
|
for _, protocol := range supportedProtocols {
|
||||||
conn.SetProtocol(protocol)
|
conn.SetProtocol(protocol)
|
||||||
err := conn.GetFsError(os.ErrNotExist)
|
err := conn.GetFsError(os.ErrNotExist)
|
||||||
if protocol == ProtocolSFTP {
|
if protocol == ProtocolSFTP {
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ type Config struct {
|
|||||||
// to authenticate:
|
// to authenticate:
|
||||||
//
|
//
|
||||||
// - SFTPGO_AUTHD_USERNAME
|
// - SFTPGO_AUTHD_USERNAME
|
||||||
|
// - SFTPGO_AUTHD_IP
|
||||||
// - 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
|
||||||
@@ -190,6 +191,7 @@ type Config struct {
|
|||||||
// The request body will contain a JSON serialized struct with the following fields:
|
// The request body will contain a JSON serialized struct with the following fields:
|
||||||
//
|
//
|
||||||
// - username
|
// - username
|
||||||
|
// - ip
|
||||||
// - 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
|
||||||
@@ -227,15 +229,18 @@ type Config struct {
|
|||||||
//
|
//
|
||||||
// - SFTPGO_LOGIND_USER, it contains the user trying to login serialized as JSON
|
// - 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_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
|
// 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.
|
// or a valid SFTPGo user serialized as JSON.
|
||||||
//
|
//
|
||||||
// If the hook is an HTTP URL then it will be invoked as HTTP POST.
|
// If the hook is an HTTP URL then it will be invoked as HTTP POST.
|
||||||
// The login method is added to the query string, for example "<http_url>?login_method=password".
|
// 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.
|
// 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
|
// If no modification is needed the HTTP response code must be 204, otherwise
|
||||||
// must be 200 and the response body a valid SFTPGo user serialized as JSON.
|
// 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,
|
// 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:
|
// for example if you want to disable the user you can return a response like this:
|
||||||
@@ -262,6 +267,7 @@ type BackupData struct {
|
|||||||
type keyboardAuthHookRequest struct {
|
type keyboardAuthHookRequest struct {
|
||||||
RequestID string `json:"request_id"`
|
RequestID string `json:"request_id"`
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
|
IP string `json:"ip,omitempty"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
Answers []string `json:"answers,omitempty"`
|
Answers []string `json:"answers,omitempty"`
|
||||||
Questions []string `json:"questions,omitempty"`
|
Questions []string `json:"questions,omitempty"`
|
||||||
@@ -432,16 +438,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 string, password string) (User, error) {
|
func CheckUserAndPass(username, password, ip 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, "")
|
user, err := doExternalAuth(username, password, nil, "", ip)
|
||||||
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)
|
user, err := executePreLoginHook(username, SSHLoginMethodPassword, ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
@@ -451,16 +457,16 @@ func CheckUserAndPass(username string, password 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) (User, string, error) {
|
func CheckUserAndPubKey(username string, pubKey []byte, ip 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, "")
|
user, err := doExternalAuth(username, "", pubKey, "", ip)
|
||||||
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)
|
user, err := executePreLoginHook(username, SSHLoginMethodPublicKey, ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, "", err
|
return user, "", err
|
||||||
}
|
}
|
||||||
@@ -471,20 +477,20 @@ func CheckUserAndPubKey(username string, pubKey []byte) (User, string, error) {
|
|||||||
|
|
||||||
// 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) (User, error) {
|
func CheckKeyboardInteractiveAuth(username, authHook string, client ssh.KeyboardInteractiveChallenge, ip 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")
|
user, err = doExternalAuth(username, "", nil, "1", ip)
|
||||||
} else if len(config.PreLoginHook) > 0 {
|
} else if len(config.PreLoginHook) > 0 {
|
||||||
user, err = executePreLoginHook(username, SSHLoginMethodKeyboardInteractive)
|
user, err = executePreLoginHook(username, SSHLoginMethodKeyboardInteractive, ip)
|
||||||
} else {
|
} else {
|
||||||
user, err = provider.userExists(username)
|
user, err = provider.userExists(username)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
return doKeyboardInteractiveAuth(user, authHook, client)
|
return doKeyboardInteractiveAuth(user, authHook, client, ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateLastLogin updates the last login fields for the given SFTP user
|
// UpdateLastLogin updates the last login fields for the given SFTP user
|
||||||
@@ -1303,7 +1309,7 @@ func sendKeyboardAuthHTTPReq(url *url.URL, request keyboardAuthHookRequest) (key
|
|||||||
return response, err
|
return response, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeKeyboardInteractiveHTTPHook(user User, authHook string, client ssh.KeyboardInteractiveChallenge) (int, error) {
|
func executeKeyboardInteractiveHTTPHook(user User, authHook string, client ssh.KeyboardInteractiveChallenge, ip string) (int, error) {
|
||||||
authResult := 0
|
authResult := 0
|
||||||
var url *url.URL
|
var url *url.URL
|
||||||
url, err := url.Parse(authHook)
|
url, err := url.Parse(authHook)
|
||||||
@@ -1314,6 +1320,7 @@ func executeKeyboardInteractiveHTTPHook(user User, authHook string, client ssh.K
|
|||||||
requestID := xid.New().String()
|
requestID := xid.New().String()
|
||||||
req := keyboardAuthHookRequest{
|
req := keyboardAuthHookRequest{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
|
IP: ip,
|
||||||
Password: user.Password,
|
Password: user.Password,
|
||||||
RequestID: requestID,
|
RequestID: requestID,
|
||||||
}
|
}
|
||||||
@@ -1388,13 +1395,14 @@ func handleProgramInteractiveQuestions(client ssh.KeyboardInteractiveChallenge,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeKeyboardInteractiveProgram(user User, authHook string, client ssh.KeyboardInteractiveChallenge) (int, error) {
|
func executeKeyboardInteractiveProgram(user User, authHook string, client ssh.KeyboardInteractiveChallenge, ip string) (int, error) {
|
||||||
authResult := 0
|
authResult := 0
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
cmd := exec.CommandContext(ctx, authHook)
|
cmd := exec.CommandContext(ctx, authHook)
|
||||||
cmd.Env = append(os.Environ(),
|
cmd.Env = append(os.Environ(),
|
||||||
fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%v", user.Username),
|
fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%v", user.Username),
|
||||||
|
fmt.Sprintf("SFTPGO_AUTHD_IP=%v", ip),
|
||||||
fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%v", user.Password))
|
fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%v", user.Password))
|
||||||
stdout, err := cmd.StdoutPipe()
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1445,13 +1453,13 @@ func executeKeyboardInteractiveProgram(user User, authHook string, client ssh.Ke
|
|||||||
return authResult, err
|
return authResult, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func doKeyboardInteractiveAuth(user User, authHook string, client ssh.KeyboardInteractiveChallenge) (User, error) {
|
func doKeyboardInteractiveAuth(user User, authHook string, client ssh.KeyboardInteractiveChallenge, ip string) (User, error) {
|
||||||
var authResult int
|
var authResult int
|
||||||
var err error
|
var err error
|
||||||
if strings.HasPrefix(authHook, "http") {
|
if strings.HasPrefix(authHook, "http") {
|
||||||
authResult, err = executeKeyboardInteractiveHTTPHook(user, authHook, client)
|
authResult, err = executeKeyboardInteractiveHTTPHook(user, authHook, client, ip)
|
||||||
} else {
|
} else {
|
||||||
authResult, err = executeKeyboardInteractiveProgram(user, authHook, client)
|
authResult, err = executeKeyboardInteractiveProgram(user, authHook, client, ip)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
@@ -1466,7 +1474,7 @@ func doKeyboardInteractiveAuth(user User, authHook string, client ssh.KeyboardIn
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPreLoginHookResponse(loginMethod string, userAsJSON []byte) ([]byte, error) {
|
func getPreLoginHookResponse(loginMethod, ip 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
|
||||||
@@ -1477,6 +1485,7 @@ func getPreLoginHookResponse(loginMethod string, userAsJSON []byte) ([]byte, err
|
|||||||
}
|
}
|
||||||
q := url.Query()
|
q := url.Query()
|
||||||
q.Add("login_method", loginMethod)
|
q.Add("login_method", loginMethod)
|
||||||
|
q.Add("ip", ip)
|
||||||
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))
|
||||||
@@ -1499,11 +1508,12 @@ func getPreLoginHookResponse(loginMethod string, userAsJSON []byte) ([]byte, err
|
|||||||
cmd.Env = append(os.Environ(),
|
cmd.Env = append(os.Environ(),
|
||||||
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),
|
||||||
)
|
)
|
||||||
return cmd.Output()
|
return cmd.Output()
|
||||||
}
|
}
|
||||||
|
|
||||||
func executePreLoginHook(username, loginMethod string) (User, error) {
|
func executePreLoginHook(username, loginMethod, ip 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 {
|
||||||
@@ -1518,7 +1528,7 @@ func executePreLoginHook(username, loginMethod string) (User, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return u, err
|
return u, err
|
||||||
}
|
}
|
||||||
out, err := getPreLoginHookResponse(loginMethod, userAsJSON)
|
out, err := getPreLoginHookResponse(loginMethod, ip, 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)
|
||||||
}
|
}
|
||||||
@@ -1557,7 +1567,7 @@ func executePreLoginHook(username, loginMethod string) (User, error) {
|
|||||||
return provider.userExists(username)
|
return provider.userExists(username)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getExternalAuthResponse(username, password, pkey, keyboardInteractive string) ([]byte, error) {
|
func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip 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
|
||||||
@@ -1569,6 +1579,7 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive strin
|
|||||||
httpClient := httpclient.GetHTTPClient()
|
httpClient := httpclient.GetHTTPClient()
|
||||||
authRequest := make(map[string]string)
|
authRequest := make(map[string]string)
|
||||||
authRequest["username"] = username
|
authRequest["username"] = username
|
||||||
|
authRequest["ip"] = ip
|
||||||
authRequest["password"] = password
|
authRequest["password"] = password
|
||||||
authRequest["public_key"] = pkey
|
authRequest["public_key"] = pkey
|
||||||
authRequest["keyboard_interactive"] = keyboardInteractive
|
authRequest["keyboard_interactive"] = keyboardInteractive
|
||||||
@@ -1593,13 +1604,14 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive strin
|
|||||||
cmd := exec.CommandContext(ctx, config.ExternalAuthHook)
|
cmd := exec.CommandContext(ctx, config.ExternalAuthHook)
|
||||||
cmd.Env = append(os.Environ(),
|
cmd.Env = append(os.Environ(),
|
||||||
fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%v", username),
|
fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%v", username),
|
||||||
|
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_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 string) (User, error) {
|
func doExternalAuth(username, password string, pubKey []byte, keyboardInteractive, ip string) (User, error) {
|
||||||
var user User
|
var user User
|
||||||
pkey := ""
|
pkey := ""
|
||||||
if len(pubKey) > 0 {
|
if len(pubKey) > 0 {
|
||||||
@@ -1609,7 +1621,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)
|
out, err := getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, fmt.Errorf("External auth error: %v", err)
|
return user, fmt.Errorf("External auth error: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ RUN go build $(if [ -n "${FEATURES}" ]; then echo "-tags ${FEATURES}"; fi) -ldfl
|
|||||||
# now define the run environment
|
# now define the run environment
|
||||||
FROM debian:latest
|
FROM debian:latest
|
||||||
|
|
||||||
# ca-certificates is needed for Cloud Storage Support and to expose the REST API over HTTPS.
|
# ca-certificates is needed for Cloud Storage Support and for HTTPS/FTPS.
|
||||||
RUN apt-get update && apt-get install -y ca-certificates
|
RUN apt-get update && apt-get install -y ca-certificates && apt-get clean
|
||||||
|
|
||||||
# git and rsync are optional, uncomment the next line to add support for them if needed.
|
# git and rsync are optional, uncomment the next line to add support for them if needed.
|
||||||
#RUN apt-get update && apt-get install -y git rsync
|
#RUN apt-get update && apt-get install -y git rsync && apt-get clean
|
||||||
|
|
||||||
ARG BASE_DIR=/app
|
ARG BASE_DIR=/app
|
||||||
ARG DATA_REL_DIR=data
|
ARG DATA_REL_DIR=data
|
||||||
@@ -82,6 +82,5 @@ ENV SFTPGO_HTTPD__BACKUPS_PATH=${BACKUPS_DIR}
|
|||||||
#ENV SFTPGO_FTPD__CERTIFICATE_FILE=${CONFIG_DIR}/mycert.crt
|
#ENV SFTPGO_FTPD__CERTIFICATE_FILE=${CONFIG_DIR}/mycert.crt
|
||||||
#ENV SFTPGO_FTPD__CERTIFICATE_KEY_FILE=${CONFIG_DIR}/mycert.key
|
#ENV SFTPGO_FTPD__CERTIFICATE_KEY_FILE=${CONFIG_DIR}/mycert.key
|
||||||
|
|
||||||
|
|
||||||
ENTRYPOINT ["sftpgo"]
|
ENTRYPOINT ["sftpgo"]
|
||||||
CMD ["serve"]
|
CMD ["serve"]
|
||||||
|
|||||||
@@ -7,13 +7,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
|
||||||
|
|
||||||
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 is added to the query string, for example `<http_url>?login_method=password`.
|
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 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.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ To enable external authentication, you must set the absolute path of your authen
|
|||||||
The external program can read the following environment variables to get info about the user trying to authenticate:
|
The external program can read the following environment variables to get info about the user trying to authenticate:
|
||||||
|
|
||||||
- `SFTPGO_AUTHD_USERNAME`
|
- `SFTPGO_AUTHD_USERNAME`
|
||||||
|
- `SFTPGO_AUTHD_IP`
|
||||||
- `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
|
||||||
@@ -15,6 +16,7 @@ The program must write, on its standard output, a valid SFTPGo user serialized a
|
|||||||
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:
|
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`
|
- `username`
|
||||||
|
- `ip`
|
||||||
- `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
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ To enable keyboard interactive authentication, you must set the absolute path of
|
|||||||
The external program can read the following environment variables to get info about the user trying to authenticate:
|
The external program can read the following environment variables to get info about the user trying to authenticate:
|
||||||
|
|
||||||
- `SFTPGO_AUTHD_USERNAME`
|
- `SFTPGO_AUTHD_USERNAME`
|
||||||
|
- `SFTPGO_AUTHD_IP`
|
||||||
- `SFTPGO_AUTHD_PASSWORD`, this is the hashed password as stored inside the data provider
|
- `SFTPGO_AUTHD_PASSWORD`, this is the hashed password as stored inside the data provider
|
||||||
|
|
||||||
Previous global environment variables aren't cleared when the script is called. The content of these variables is _not_ quoted. They may contain special characters.
|
Previous global environment variables aren't cleared when the script is called. The content of these variables is _not_ quoted. They may contain special characters.
|
||||||
@@ -77,6 +78,7 @@ The request body will contain a JSON struct with the following fields:
|
|||||||
|
|
||||||
- `request_id`, string. Unique request identifier
|
- `request_id`, string. Unique request identifier
|
||||||
- `username`, string
|
- `username`, string
|
||||||
|
- `ip`, string
|
||||||
- `password`, string. This is the hashed password as stored inside the data provider
|
- `password`, string. This is the hashed password as stored inside the data provider
|
||||||
- `answers`, list of string. It will be null for the first request
|
- `answers`, list of string. It will be null for the first request
|
||||||
- `questions`, list of string. It will contains the previous asked questions. It will be null for the first request
|
- `questions`, list of string. It will contains the previous asked questions. It will be null for the first request
|
||||||
@@ -93,7 +95,7 @@ Content-Length: 189
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
Accept-Encoding: gzip
|
Accept-Encoding: gzip
|
||||||
|
|
||||||
{"request_id":"bq1r5r7cdrpd2qtn25ng","username":"a","password":"$pbkdf2-sha512$150000$ClOPkLNujMTL$XktKy0xuJsOfMYBz+f2bIyPTdbvDTSnJ1q+7+zp/HPq5Qojwp6kcpSIiVHiwvbi8P6HFXI/D3UJv9BLcnQFqPA=="}
|
{"request_id":"bq1r5r7cdrpd2qtn25ng","username":"a","ip":"127.0.0.1","password":"$pbkdf2-sha512$150000$ClOPkLNujMTL$XktKy0xuJsOfMYBz+f2bIyPTdbvDTSnJ1q+7+zp/HPq5Qojwp6kcpSIiVHiwvbi8P6HFXI/D3UJv9BLcnQFqPA=="}
|
||||||
```
|
```
|
||||||
|
|
||||||
as you can see in this first requests `answers` and `questions` are null.
|
as you can see in this first requests `answers` and `questions` are null.
|
||||||
@@ -121,7 +123,7 @@ Content-Length: 233
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
Accept-Encoding: gzip
|
Accept-Encoding: gzip
|
||||||
|
|
||||||
{"request_id":"bq1r5r7cdrpd2qtn25ng","username":"a","password":"$pbkdf2-sha512$150000$ClOPkLNujMTL$XktKy0xuJsOfMYBz+f2bIyPTdbvDTSnJ1q+7+zp/HPq5Qojwp6kcpSIiVHiwvbi8P6HFXI/D3UJv9BLcnQFqPA==","answers":["OK"],"questions":["Password: "]}
|
{"request_id":"bq1r5r7cdrpd2qtn25ng","username":"a","ip":"127.0.0.1","password":"$pbkdf2-sha512$150000$ClOPkLNujMTL$XktKy0xuJsOfMYBz+f2bIyPTdbvDTSnJ1q+7+zp/HPq5Qojwp6kcpSIiVHiwvbi8P6HFXI/D3UJv9BLcnQFqPA==","answers":["OK"],"questions":["Password: "]}
|
||||||
```
|
```
|
||||||
|
|
||||||
Here is the HTTP response that istructs SFTPGo to ask for a new question:
|
Here is the HTTP response that istructs SFTPGo to ask for a new question:
|
||||||
@@ -147,7 +149,7 @@ Content-Length: 239
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
Accept-Encoding: gzip
|
Accept-Encoding: gzip
|
||||||
|
|
||||||
{"request_id":"bq1r5r7cdrpd2qtn25ng","username":"a","password":"$pbkdf2-sha512$150000$ClOPkLNujMTL$XktKy0xuJsOfMYBz+f2bIyPTdbvDTSnJ1q+7+zp/HPq5Qojwp6kcpSIiVHiwvbi8P6HFXI/D3UJv9BLcnQFqPA==","answers":["answer2"],"questions":["Question2: "]}
|
{"request_id":"bq1r5r7cdrpd2qtn25ng","username":"a","ip":"127.0.0.1","password":"$pbkdf2-sha512$150000$ClOPkLNujMTL$XktKy0xuJsOfMYBz+f2bIyPTdbvDTSnJ1q+7+zp/HPq5Qojwp6kcpSIiVHiwvbi8P6HFXI/D3UJv9BLcnQFqPA==","answers":["answer2"],"questions":["Question2: "]}
|
||||||
```
|
```
|
||||||
|
|
||||||
Here is the final HTTP response that allows the user login:
|
Here is the final HTTP response that allows the user login:
|
||||||
|
|||||||
@@ -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)
|
user, err := dataprovider.CheckUserAndPass(username, password, utils.GetIPFromRemoteAddress(remoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
updateLoginMetrics(username, remoteAddr, dataprovider.FTPLoginMethodPassword, err)
|
updateLoginMetrics(username, remoteAddr, dataprovider.FTPLoginMethodPassword, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -595,7 +595,8 @@ func (c Configuration) validatePublicKeyCredentials(conn ssh.ConnMetadata, pubKe
|
|||||||
}
|
}
|
||||||
certPerm = &cert.Permissions
|
certPerm = &cert.Permissions
|
||||||
}
|
}
|
||||||
if user, keyID, err = dataprovider.CheckUserAndPubKey(conn.User(), pubKey.Marshal()); err == nil {
|
ipAddr := utils.GetIPFromRemoteAddress(conn.RemoteAddr().String())
|
||||||
|
if user, keyID, err = dataprovider.CheckUserAndPubKey(conn.User(), pubKey.Marshal(), ipAddr); 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
|
||||||
@@ -625,7 +626,8 @@ func (c Configuration) validatePasswordCredentials(conn ssh.ConnMetadata, pass [
|
|||||||
if len(conn.PartialSuccessMethods()) == 1 {
|
if len(conn.PartialSuccessMethods()) == 1 {
|
||||||
method = dataprovider.SSHLoginMethodKeyAndPassword
|
method = dataprovider.SSHLoginMethodKeyAndPassword
|
||||||
}
|
}
|
||||||
if user, err = dataprovider.CheckUserAndPass(conn.User(), string(pass)); err == nil {
|
ipAddr := utils.GetIPFromRemoteAddress(conn.RemoteAddr().String())
|
||||||
|
if user, err = dataprovider.CheckUserAndPass(conn.User(), string(pass), ipAddr); err == nil {
|
||||||
sshPerm, err = loginUser(user, method, "", conn)
|
sshPerm, err = loginUser(user, method, "", conn)
|
||||||
}
|
}
|
||||||
updateLoginMetrics(conn, method, err)
|
updateLoginMetrics(conn, method, err)
|
||||||
@@ -641,7 +643,8 @@ func (c Configuration) validateKeyboardInteractiveCredentials(conn ssh.ConnMetad
|
|||||||
if len(conn.PartialSuccessMethods()) == 1 {
|
if len(conn.PartialSuccessMethods()) == 1 {
|
||||||
method = dataprovider.SSHLoginMethodKeyAndKeyboardInt
|
method = dataprovider.SSHLoginMethodKeyAndKeyboardInt
|
||||||
}
|
}
|
||||||
if user, err = dataprovider.CheckKeyboardInteractiveAuth(conn.User(), c.KeyboardInteractiveHook, client); err == nil {
|
ipAddr := utils.GetIPFromRemoteAddress(conn.RemoteAddr().String())
|
||||||
|
if user, err = dataprovider.CheckKeyboardInteractiveAuth(conn.User(), c.KeyboardInteractiveHook, client, ipAddr); err == nil {
|
||||||
sshPerm, err = loginUser(user, method, "", conn)
|
sshPerm, err = loginUser(user, method, "", conn)
|
||||||
}
|
}
|
||||||
updateLoginMetrics(conn, method, err)
|
updateLoginMetrics(conn, method, err)
|
||||||
|
|||||||
Reference in New Issue
Block a user