add support for auth plugins
This commit is contained in:
parent
ced2e16f41
commit
a20373b613
17 changed files with 1885 additions and 91 deletions
|
@ -16,7 +16,7 @@ import (
|
||||||
"github.com/drakkan/sftpgo/v2/util"
|
"github.com/drakkan/sftpgo/v2/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HostEvent is the enumerable for the support host event
|
// HostEvent is the enumerable for the support host events
|
||||||
type HostEvent int
|
type HostEvent int
|
||||||
|
|
||||||
// Supported host events
|
// Supported host events
|
||||||
|
|
|
@ -356,23 +356,6 @@ func (d *BackupData) HasFolder(name string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
type keyboardAuthHookRequest struct {
|
|
||||||
RequestID string `json:"request_id"`
|
|
||||||
Username string `json:"username,omitempty"`
|
|
||||||
IP string `json:"ip,omitempty"`
|
|
||||||
Password string `json:"password,omitempty"`
|
|
||||||
Answers []string `json:"answers,omitempty"`
|
|
||||||
Questions []string `json:"questions,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type keyboardAuthHookResponse struct {
|
|
||||||
Instruction string `json:"instruction"`
|
|
||||||
Questions []string `json:"questions"`
|
|
||||||
Echos []bool `json:"echos"`
|
|
||||||
AuthResult int `json:"auth_result"`
|
|
||||||
CheckPwd int `json:"check_password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type checkPasswordRequest struct {
|
type checkPasswordRequest struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
|
@ -680,17 +663,15 @@ func CheckCompositeCredentials(username, password, ip, loginMethod, protocol str
|
||||||
return user, loginMethod, fmt.Errorf("certificate login method is not allowed for user %#v", user.Username)
|
return user, loginMethod, fmt.Errorf("certificate login method is not allowed for user %#v", user.Username)
|
||||||
}
|
}
|
||||||
if loginMethod == LoginMethodTLSCertificateAndPwd {
|
if loginMethod == LoginMethodTLSCertificateAndPwd {
|
||||||
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) {
|
if plugin.Handler.HasAuthScope(plugin.AuthScopePassword) {
|
||||||
|
user, err = doPluginAuth(username, password, nil, ip, protocol, nil, plugin.AuthScopePassword)
|
||||||
|
} else if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) {
|
||||||
user, err = doExternalAuth(username, password, nil, "", ip, protocol, nil)
|
user, err = doExternalAuth(username, password, nil, "", ip, protocol, nil)
|
||||||
if err != nil {
|
} else if config.PreLoginHook != "" {
|
||||||
return user, loginMethod, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if config.PreLoginHook != "" {
|
|
||||||
user, err = executePreLoginHook(username, LoginMethodPassword, ip, protocol)
|
user, err = executePreLoginHook(username, LoginMethodPassword, ip, protocol)
|
||||||
if err != nil {
|
}
|
||||||
return user, loginMethod, err
|
if err != nil {
|
||||||
}
|
return user, loginMethod, err
|
||||||
}
|
}
|
||||||
user, err = checkUserAndPass(&user, password, ip, protocol)
|
user, err = checkUserAndPass(&user, password, ip, protocol)
|
||||||
}
|
}
|
||||||
|
@ -699,6 +680,9 @@ func CheckCompositeCredentials(username, password, ip, loginMethod, protocol str
|
||||||
|
|
||||||
// CheckUserBeforeTLSAuth checks if a user exits before trying mutual TLS
|
// CheckUserBeforeTLSAuth checks if a user exits before trying mutual TLS
|
||||||
func CheckUserBeforeTLSAuth(username, ip, protocol string, tlsCert *x509.Certificate) (User, error) {
|
func CheckUserBeforeTLSAuth(username, ip, protocol string, tlsCert *x509.Certificate) (User, error) {
|
||||||
|
if plugin.Handler.HasAuthScope(plugin.AuthScopeTLSCertificate) {
|
||||||
|
return doPluginAuth(username, "", nil, ip, protocol, tlsCert, plugin.AuthScopeTLSCertificate)
|
||||||
|
}
|
||||||
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&8 != 0) {
|
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&8 != 0) {
|
||||||
return doExternalAuth(username, "", nil, "", ip, protocol, tlsCert)
|
return doExternalAuth(username, "", nil, "", ip, protocol, tlsCert)
|
||||||
}
|
}
|
||||||
|
@ -711,6 +695,13 @@ func CheckUserBeforeTLSAuth(username, ip, protocol string, tlsCert *x509.Certifi
|
||||||
// CheckUserAndTLSCert returns the SFTPGo user with the given username and check if the
|
// CheckUserAndTLSCert returns the SFTPGo user with the given username and check if the
|
||||||
// given TLS certificate allow authentication without password
|
// given TLS certificate allow authentication without password
|
||||||
func CheckUserAndTLSCert(username, ip, protocol string, tlsCert *x509.Certificate) (User, error) {
|
func CheckUserAndTLSCert(username, ip, protocol string, tlsCert *x509.Certificate) (User, error) {
|
||||||
|
if plugin.Handler.HasAuthScope(plugin.AuthScopeTLSCertificate) {
|
||||||
|
user, err := doPluginAuth(username, "", nil, ip, protocol, tlsCert, plugin.AuthScopeTLSCertificate)
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
return checkUserAndTLSCertificate(&user, protocol, tlsCert)
|
||||||
|
}
|
||||||
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&8 != 0) {
|
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&8 != 0) {
|
||||||
user, err := doExternalAuth(username, "", nil, "", ip, protocol, tlsCert)
|
user, err := doExternalAuth(username, "", nil, "", ip, protocol, tlsCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -730,6 +721,13 @@ func CheckUserAndTLSCert(username, ip, protocol string, tlsCert *x509.Certificat
|
||||||
|
|
||||||
// CheckUserAndPass retrieves the SFTPGo user with the given username and password if a match is found or an error
|
// CheckUserAndPass retrieves the SFTPGo user with the given username and password if a match is found or an error
|
||||||
func CheckUserAndPass(username, password, ip, protocol string) (User, error) {
|
func CheckUserAndPass(username, password, ip, protocol string) (User, error) {
|
||||||
|
if plugin.Handler.HasAuthScope(plugin.AuthScopePassword) {
|
||||||
|
user, err := doPluginAuth(username, password, nil, ip, protocol, nil, plugin.AuthScopePassword)
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
return checkUserAndPass(&user, password, ip, protocol)
|
||||||
|
}
|
||||||
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) {
|
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) {
|
||||||
user, err := doExternalAuth(username, password, nil, "", ip, protocol, nil)
|
user, err := doExternalAuth(username, password, nil, "", ip, protocol, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -749,6 +747,13 @@ func CheckUserAndPass(username, password, ip, protocol 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, protocol string) (User, string, error) {
|
func CheckUserAndPubKey(username string, pubKey []byte, ip, protocol string) (User, string, error) {
|
||||||
|
if plugin.Handler.HasAuthScope(plugin.AuthScopePublicKey) {
|
||||||
|
user, err := doPluginAuth(username, "", pubKey, ip, protocol, nil, plugin.AuthScopePublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return user, "", err
|
||||||
|
}
|
||||||
|
return checkUserAndPubKey(&user, pubKey)
|
||||||
|
}
|
||||||
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&2 != 0) {
|
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&2 != 0) {
|
||||||
user, err := doExternalAuth(username, "", pubKey, "", ip, protocol, nil)
|
user, err := doExternalAuth(username, "", pubKey, "", ip, protocol, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -771,7 +776,9 @@ func CheckUserAndPubKey(username string, pubKey []byte, ip, protocol string) (Us
|
||||||
func CheckKeyboardInteractiveAuth(username, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol 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 config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&4 != 0) {
|
if plugin.Handler.HasAuthScope(plugin.AuthScopeKeyboardInteractive) {
|
||||||
|
user, err = doPluginAuth(username, "", nil, ip, protocol, nil, plugin.AuthScopeKeyboardInteractive)
|
||||||
|
} else if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&4 != 0) {
|
||||||
user, err = doExternalAuth(username, "", nil, "1", ip, protocol, nil)
|
user, err = doExternalAuth(username, "", nil, "1", ip, protocol, nil)
|
||||||
} else if config.PreLoginHook != "" {
|
} else if config.PreLoginHook != "" {
|
||||||
user, err = executePreLoginHook(username, SSHLoginMethodKeyboardInteractive, ip, protocol)
|
user, err = executePreLoginHook(username, SSHLoginMethodKeyboardInteractive, ip, protocol)
|
||||||
|
@ -1824,51 +1831,79 @@ func terminateInteractiveAuthProgram(cmd *exec.Cmd, isFinished bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateKeyboardAuthResponse(response keyboardAuthHookResponse) error {
|
func sendKeyboardAuthHTTPReq(url string, request *plugin.KeyboardAuthRequest) (*plugin.KeyboardAuthResponse, error) {
|
||||||
if len(response.Questions) == 0 {
|
|
||||||
err := errors.New("interactive auth error: hook response does not contain questions")
|
|
||||||
providerLog(logger.LevelInfo, "%v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(response.Questions) != len(response.Echos) {
|
|
||||||
err := fmt.Errorf("interactive auth error, http hook response questions don't match echos: %v %v",
|
|
||||||
len(response.Questions), len(response.Echos))
|
|
||||||
providerLog(logger.LevelInfo, "%v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendKeyboardAuthHTTPReq(url string, request keyboardAuthHookRequest) (keyboardAuthHookResponse, error) {
|
|
||||||
var response keyboardAuthHookResponse
|
|
||||||
reqAsJSON, err := json.Marshal(request)
|
reqAsJSON, err := json.Marshal(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelWarn, "error serializing keyboard interactive auth request: %v", err)
|
providerLog(logger.LevelWarn, "error serializing keyboard interactive auth request: %v", err)
|
||||||
return response, err
|
return nil, err
|
||||||
}
|
}
|
||||||
resp, err := httpclient.Post(url, "application/json", bytes.NewBuffer(reqAsJSON))
|
resp, err := httpclient.Post(url, "application/json", bytes.NewBuffer(reqAsJSON))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelWarn, "error getting keyboard interactive auth hook HTTP response: %v", err)
|
providerLog(logger.LevelWarn, "error getting keyboard interactive auth hook HTTP response: %v", err)
|
||||||
return response, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return response, fmt.Errorf("wrong keyboard interactive auth http status code: %v, expected 200", resp.StatusCode)
|
return nil, fmt.Errorf("wrong keyboard interactive auth http status code: %v, expected 200", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
var response plugin.KeyboardAuthResponse
|
||||||
err = render.DecodeJSON(resp.Body, &response)
|
err = render.DecodeJSON(resp.Body, &response)
|
||||||
return response, err
|
return &response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeKeyboardInteractivePlugin(user *User, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (int, error) {
|
||||||
|
authResult := 0
|
||||||
|
requestID := xid.New().String()
|
||||||
|
authStep := 1
|
||||||
|
req := &plugin.KeyboardAuthRequest{
|
||||||
|
Username: user.Username,
|
||||||
|
IP: ip,
|
||||||
|
Password: user.Password,
|
||||||
|
RequestID: requestID,
|
||||||
|
Step: authStep,
|
||||||
|
}
|
||||||
|
var response *plugin.KeyboardAuthResponse
|
||||||
|
var err error
|
||||||
|
for {
|
||||||
|
response, err = plugin.Handler.ExecuteKeyboardInteractiveStep(req)
|
||||||
|
if err != nil {
|
||||||
|
return authResult, err
|
||||||
|
}
|
||||||
|
if response.AuthResult != 0 {
|
||||||
|
return response.AuthResult, err
|
||||||
|
}
|
||||||
|
if err = response.Validate(); err != nil {
|
||||||
|
providerLog(logger.LevelInfo, "invalid response from keyboard interactive plugin: %v", err)
|
||||||
|
return authResult, err
|
||||||
|
}
|
||||||
|
answers, err := getKeyboardInteractiveAnswers(client, response, user, ip, protocol)
|
||||||
|
if err != nil {
|
||||||
|
return authResult, err
|
||||||
|
}
|
||||||
|
authStep++
|
||||||
|
req = &plugin.KeyboardAuthRequest{
|
||||||
|
RequestID: requestID,
|
||||||
|
Step: authStep,
|
||||||
|
Username: user.Username,
|
||||||
|
Password: user.Password,
|
||||||
|
Answers: answers,
|
||||||
|
Questions: response.Questions,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeKeyboardInteractiveHTTPHook(user *User, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (int, error) {
|
func executeKeyboardInteractiveHTTPHook(user *User, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (int, error) {
|
||||||
authResult := 0
|
authResult := 0
|
||||||
requestID := xid.New().String()
|
requestID := xid.New().String()
|
||||||
req := keyboardAuthHookRequest{
|
authStep := 1
|
||||||
|
req := &plugin.KeyboardAuthRequest{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
IP: ip,
|
IP: ip,
|
||||||
Password: user.Password,
|
Password: user.Password,
|
||||||
RequestID: requestID,
|
RequestID: requestID,
|
||||||
|
Step: authStep,
|
||||||
}
|
}
|
||||||
var response keyboardAuthHookResponse
|
var response *plugin.KeyboardAuthResponse
|
||||||
var err error
|
var err error
|
||||||
for {
|
for {
|
||||||
response, err = sendKeyboardAuthHTTPReq(authHook, req)
|
response, err = sendKeyboardAuthHTTPReq(authHook, req)
|
||||||
|
@ -1878,15 +1913,18 @@ func executeKeyboardInteractiveHTTPHook(user *User, authHook string, client ssh.
|
||||||
if response.AuthResult != 0 {
|
if response.AuthResult != 0 {
|
||||||
return response.AuthResult, err
|
return response.AuthResult, err
|
||||||
}
|
}
|
||||||
if err = validateKeyboardAuthResponse(response); err != nil {
|
if err = response.Validate(); err != nil {
|
||||||
|
providerLog(logger.LevelInfo, "invalid response from keyboard interactive http hook: %v", err)
|
||||||
return authResult, err
|
return authResult, err
|
||||||
}
|
}
|
||||||
answers, err := getKeyboardInteractiveAnswers(client, response, user, ip, protocol)
|
answers, err := getKeyboardInteractiveAnswers(client, response, user, ip, protocol)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return authResult, err
|
return authResult, err
|
||||||
}
|
}
|
||||||
req = keyboardAuthHookRequest{
|
authStep++
|
||||||
|
req = &plugin.KeyboardAuthRequest{
|
||||||
RequestID: requestID,
|
RequestID: requestID,
|
||||||
|
Step: authStep,
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Password: user.Password,
|
Password: user.Password,
|
||||||
Answers: answers,
|
Answers: answers,
|
||||||
|
@ -1895,7 +1933,7 @@ func executeKeyboardInteractiveHTTPHook(user *User, authHook string, client ssh.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getKeyboardInteractiveAnswers(client ssh.KeyboardInteractiveChallenge, response keyboardAuthHookResponse,
|
func getKeyboardInteractiveAnswers(client ssh.KeyboardInteractiveChallenge, response *plugin.KeyboardAuthResponse,
|
||||||
user *User, ip, protocol string) ([]string, error) {
|
user *User, ip, protocol string) ([]string, error) {
|
||||||
questions := response.Questions
|
questions := response.Questions
|
||||||
answers, err := client(user.Username, response.Instruction, questions, response.Echos)
|
answers, err := client(user.Username, response.Instruction, questions, response.Echos)
|
||||||
|
@ -1920,7 +1958,7 @@ func getKeyboardInteractiveAnswers(client ssh.KeyboardInteractiveChallenge, resp
|
||||||
return answers, err
|
return answers, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleProgramInteractiveQuestions(client ssh.KeyboardInteractiveChallenge, response keyboardAuthHookResponse,
|
func handleProgramInteractiveQuestions(client ssh.KeyboardInteractiveChallenge, response *plugin.KeyboardAuthResponse,
|
||||||
user *User, stdin io.WriteCloser, ip, protocol string) error {
|
user *User, stdin io.WriteCloser, ip, protocol string) error {
|
||||||
answers, err := getKeyboardInteractiveAnswers(client, response, user, ip, protocol)
|
answers, err := getKeyboardInteractiveAnswers(client, response, user, ip, protocol)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1964,7 +2002,7 @@ func executeKeyboardInteractiveProgram(user *User, authHook string, client ssh.K
|
||||||
var once sync.Once
|
var once sync.Once
|
||||||
scanner := bufio.NewScanner(stdout)
|
scanner := bufio.NewScanner(stdout)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
var response keyboardAuthHookResponse
|
var response plugin.KeyboardAuthResponse
|
||||||
err = json.Unmarshal(scanner.Bytes(), &response)
|
err = json.Unmarshal(scanner.Bytes(), &response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelInfo, "interactive auth error parsing response: %v", err)
|
providerLog(logger.LevelInfo, "interactive auth error parsing response: %v", err)
|
||||||
|
@ -1975,12 +2013,13 @@ func executeKeyboardInteractiveProgram(user *User, authHook string, client ssh.K
|
||||||
authResult = response.AuthResult
|
authResult = response.AuthResult
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err = validateKeyboardAuthResponse(response); err != nil {
|
if err = response.Validate(); err != nil {
|
||||||
|
providerLog(logger.LevelInfo, "invalid response from keyboard interactive program: %v", err)
|
||||||
once.Do(func() { terminateInteractiveAuthProgram(cmd, false) })
|
once.Do(func() { terminateInteractiveAuthProgram(cmd, false) })
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
err := handleProgramInteractiveQuestions(client, response, user, stdin, ip, protocol)
|
err := handleProgramInteractiveQuestions(client, &response, user, stdin, ip, protocol)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
once.Do(func() { terminateInteractiveAuthProgram(cmd, false) })
|
once.Do(func() { terminateInteractiveAuthProgram(cmd, false) })
|
||||||
}
|
}
|
||||||
|
@ -2001,7 +2040,9 @@ func executeKeyboardInteractiveProgram(user *User, authHook string, client ssh.K
|
||||||
func doKeyboardInteractiveAuth(user *User, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (User, error) {
|
func doKeyboardInteractiveAuth(user *User, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (User, error) {
|
||||||
var authResult int
|
var authResult int
|
||||||
var err error
|
var err error
|
||||||
if strings.HasPrefix(authHook, "http") {
|
if plugin.Handler.HasAuthScope(plugin.AuthScopeKeyboardInteractive) {
|
||||||
|
authResult, err = executeKeyboardInteractivePlugin(user, client, ip, protocol)
|
||||||
|
} else if strings.HasPrefix(authHook, "http") {
|
||||||
authResult, err = executeKeyboardInteractiveHTTPHook(user, authHook, client, ip, protocol)
|
authResult, err = executeKeyboardInteractiveHTTPHook(user, authHook, client, ip, protocol)
|
||||||
} else {
|
} else {
|
||||||
authResult, err = executeKeyboardInteractiveProgram(user, authHook, client, ip, protocol)
|
authResult, err = executeKeyboardInteractiveProgram(user, authHook, client, ip, protocol)
|
||||||
|
@ -2389,6 +2430,67 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
|
||||||
return provider.userExists(user.Username)
|
return provider.userExists(user.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func doPluginAuth(username, password string, pubKey []byte, ip, protocol string,
|
||||||
|
tlsCert *x509.Certificate, authScope int,
|
||||||
|
) (User, error) {
|
||||||
|
var user User
|
||||||
|
|
||||||
|
u, userAsJSON, err := getUserAndJSONForHook(username)
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Filters.Hooks.ExternalAuthDisabled {
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pkey, err := util.GetSSHPublicKeyAsString(pubKey)
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
out, err := plugin.Handler.Authenticate(username, password, ip, protocol, pkey, tlsCert, authScope, userAsJSON)
|
||||||
|
if err != nil {
|
||||||
|
return user, fmt.Errorf("plugin auth error for user %#v: %v, elapsed: %v, auth scope: %v",
|
||||||
|
username, err, time.Since(startTime), authScope)
|
||||||
|
}
|
||||||
|
providerLog(logger.LevelDebug, "plugin auth completed for user %#v, elapsed: %v,auth scope: %v",
|
||||||
|
username, time.Since(startTime), authScope)
|
||||||
|
if util.IsByteArrayEmpty(out) {
|
||||||
|
providerLog(logger.LevelDebug, "empty response from plugin auth, no modification requested for user %#v id: %v",
|
||||||
|
username, u.ID)
|
||||||
|
if u.ID == 0 {
|
||||||
|
return u, util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username))
|
||||||
|
}
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(out, &user)
|
||||||
|
if err != nil {
|
||||||
|
return user, fmt.Errorf("invalid plugin auth response: %v", err)
|
||||||
|
}
|
||||||
|
updateUserFromExtAuthResponse(&user, password, pkey)
|
||||||
|
if u.ID > 0 {
|
||||||
|
user.ID = u.ID
|
||||||
|
user.UsedQuotaSize = u.UsedQuotaSize
|
||||||
|
user.UsedQuotaFiles = u.UsedQuotaFiles
|
||||||
|
user.LastQuotaUpdate = u.LastQuotaUpdate
|
||||||
|
user.LastLogin = u.LastLogin
|
||||||
|
err = provider.updateUser(&user)
|
||||||
|
if err == nil {
|
||||||
|
webDAVUsersCache.swap(&user)
|
||||||
|
cachedPasswords.Add(user.Username, password)
|
||||||
|
}
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
err = provider.addUser(&user)
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
return provider.userExists(user.Username)
|
||||||
|
}
|
||||||
|
|
||||||
func getUserAndJSONForHook(username string) (User, []byte, error) {
|
func getUserAndJSONForHook(username string) (User, []byte, error) {
|
||||||
var userAsJSON []byte
|
var userAsJSON []byte
|
||||||
u, err := provider.userExists(username)
|
u, err := provider.userExists(username)
|
||||||
|
|
|
@ -251,7 +251,7 @@ The configuration file contains the following sections:
|
||||||
- `master_key`, string. Defines the master encryption key as string. If not empty, it takes precedence over `master_key_path`. Default empty.
|
- `master_key`, string. Defines the master encryption key as string. If not empty, it takes precedence over `master_key_path`. Default empty.
|
||||||
- `master_key_path, string. Defines the absolute path to a file containing the master encryption key. Default empty.
|
- `master_key_path, string. Defines the absolute path to a file containing the master encryption key. Default empty.
|
||||||
- **plugins**, list of external plugins. Each plugin is configured using a struct with the following fields:
|
- **plugins**, list of external plugins. Each plugin is configured using a struct with the following fields:
|
||||||
- `type`, string. Defines the plugin type. Supported types: `notifier`, `kms`.
|
- `type`, string. Defines the plugin type. Supported types: `notifier`, `kms`, `auth`.
|
||||||
- `notifier_options`, struct. Defines the options for notifier plugins.
|
- `notifier_options`, struct. Defines the options for notifier plugins.
|
||||||
- `fs_events`, list of strings. Defines the filesystem events that will be notified to this plugin.
|
- `fs_events`, list of strings. Defines the filesystem events that will be notified to this plugin.
|
||||||
- `user_events`, list of strings. Defines the user events that will be notified to this plugin.
|
- `user_events`, list of strings. Defines the user events that will be notified to this plugin.
|
||||||
|
@ -260,6 +260,8 @@ The configuration file contains the following sections:
|
||||||
- `kms_options`, struct. Defines the options for kms plugins.
|
- `kms_options`, struct. Defines the options for kms plugins.
|
||||||
- `scheme`, string. KMS scheme. Supported schemes are: `awskms`, `gcpkms`, `hashivault`, `azurekeyvault`.
|
- `scheme`, string. KMS scheme. Supported schemes are: `awskms`, `gcpkms`, `hashivault`, `azurekeyvault`.
|
||||||
- `encrypted_status`, string. Encrypted status for a KMS secret. Supported statuses are: `AWS`, `GCP`, `VaultTransit`, `AzureKeyVault`.
|
- `encrypted_status`, string. Encrypted status for a KMS secret. Supported statuses are: `AWS`, `GCP`, `VaultTransit`, `AzureKeyVault`.
|
||||||
|
- `auth_options`, struct. Defines the options for auth plugins.
|
||||||
|
- `scope`, integer. 1 means passwords only. 2 means public keys only. 4 means key keyboard interactive only. 8 means TLS certificate. The flags can be combined, for example 6 means public keys and keyboard interactive. The scope must be explicit, `0` is not a valid option.
|
||||||
- `cmd`, string. Path to the plugin executable.
|
- `cmd`, string. Path to the plugin executable.
|
||||||
- `args`, list of strings. Optional arguments to pass to the plugin executable.
|
- `args`, list of strings. Optional arguments to pass to the plugin executable.
|
||||||
- `sha256sum`, string. SHA256 checksum for the plugin executable. If not empty it will be used to verify the integrity of the executable.
|
- `sha256sum`, string. SHA256 checksum for the plugin executable. If not empty it will be used to verify the integrity of the executable.
|
||||||
|
|
|
@ -77,6 +77,7 @@ If the hook is an HTTP URL then it will be invoked as HTTP POST multiple times f
|
||||||
The request body will contain a JSON struct with the following fields:
|
The request body will contain a JSON struct with the following fields:
|
||||||
|
|
||||||
- `request_id`, string. Unique request identifier
|
- `request_id`, string. Unique request identifier
|
||||||
|
- `step`, integer. Counter starting from 1
|
||||||
- `username`, string
|
- `username`, string
|
||||||
- `ip`, 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
|
||||||
|
@ -95,7 +96,7 @@ Content-Length: 189
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
Accept-Encoding: gzip
|
Accept-Encoding: gzip
|
||||||
|
|
||||||
{"request_id":"bq1r5r7cdrpd2qtn25ng","username":"a","ip":"127.0.0.1","password":"$pbkdf2-sha512$150000$ClOPkLNujMTL$XktKy0xuJsOfMYBz+f2bIyPTdbvDTSnJ1q+7+zp/HPq5Qojwp6kcpSIiVHiwvbi8P6HFXI/D3UJv9BLcnQFqPA=="}
|
{"request_id":"bq1r5r7cdrpd2qtn25ng","username":"a","ip":"127.0.0.1","step":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.
|
||||||
|
@ -123,7 +124,7 @@ Content-Length: 233
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
Accept-Encoding: gzip
|
Accept-Encoding: gzip
|
||||||
|
|
||||||
{"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: "]}
|
{"request_id":"bq1r5r7cdrpd2qtn25ng","step":2,"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 instructs SFTPGo to ask for a new question:
|
Here is the HTTP response that instructs SFTPGo to ask for a new question:
|
||||||
|
@ -149,7 +150,7 @@ Content-Length: 239
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
Accept-Encoding: gzip
|
Accept-Encoding: gzip
|
||||||
|
|
||||||
{"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: "]}
|
{"request_id":"bq1r5r7cdrpd2qtn25ng","step":3,"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:
|
||||||
|
|
|
@ -8,6 +8,12 @@ The plugins are configured via the `plugins` section in the main SFTPGo configur
|
||||||
|
|
||||||
For added security you can enable the automatic TLS. In this way, the client and the server automatically negotiate mutual TLS for transport authentication. This ensures that only the original client will be allowed to connect to the server, and all other connections will be rejected. The client will also refuse to connect to any server that isn't the original instance started by the client.
|
For added security you can enable the automatic TLS. In this way, the client and the server automatically negotiate mutual TLS for transport authentication. This ensures that only the original client will be allowed to connect to the server, and all other connections will be rejected. The client will also refuse to connect to any server that isn't the original instance started by the client.
|
||||||
|
|
||||||
|
The following plugin types are supported:
|
||||||
|
|
||||||
|
- `auth`, allows to authenticate users
|
||||||
|
- `notifier`, allows to receive notifications for supported filesystem events such as file uploads, downloads etc. and user events such as add, update, delete.
|
||||||
|
- `kms`, allows to support additional KMS providers.
|
||||||
|
|
||||||
Full configuration details can be found [here](./full-configuration.md)
|
Full configuration details can be found [here](./full-configuration.md)
|
||||||
|
|
||||||
## Available plugins
|
## Available plugins
|
||||||
|
|
180
sdk/plugin/auth.go
Normal file
180
sdk/plugin/auth.go
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-hclog"
|
||||||
|
"github.com/hashicorp/go-plugin"
|
||||||
|
|
||||||
|
"github.com/drakkan/sftpgo/v2/logger"
|
||||||
|
"github.com/drakkan/sftpgo/v2/sdk/plugin/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Supported auth scopes
|
||||||
|
const (
|
||||||
|
AuthScopePassword = 1
|
||||||
|
AuthScopePublicKey = 2
|
||||||
|
AuthScopeKeyboardInteractive = 4
|
||||||
|
AuthScopeTLSCertificate = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeyboardAuthRequest defines the request for a keyboard interactive authentication step
|
||||||
|
type KeyboardAuthRequest struct {
|
||||||
|
RequestID string `json:"request_id"`
|
||||||
|
Step int `json:"step"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
IP string `json:"ip,omitempty"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
Answers []string `json:"answers,omitempty"`
|
||||||
|
Questions []string `json:"questions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyboardAuthResponse defines the response for a keyboard interactive authentication step
|
||||||
|
type KeyboardAuthResponse struct {
|
||||||
|
Instruction string `json:"instruction"`
|
||||||
|
Questions []string `json:"questions"`
|
||||||
|
Echos []bool `json:"echos"`
|
||||||
|
AuthResult int `json:"auth_result"`
|
||||||
|
CheckPwd int `json:"check_password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate returns an error if the KeyboardAuthResponse is invalid
|
||||||
|
func (r *KeyboardAuthResponse) Validate() error {
|
||||||
|
if len(r.Questions) == 0 {
|
||||||
|
err := errors.New("interactive auth error: response does not contain questions")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(r.Questions) != len(r.Echos) {
|
||||||
|
err := fmt.Errorf("interactive auth error: response questions don't match echos: %v %v",
|
||||||
|
len(r.Questions), len(r.Echos))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthConfig defines configuration parameters for auth plugins
|
||||||
|
type AuthConfig struct {
|
||||||
|
// Scope defines the scope for the authentication plugin.
|
||||||
|
// - 1 means passwords only
|
||||||
|
// - 2 means public keys only
|
||||||
|
// - 4 means keyboard interactive only
|
||||||
|
// - 8 means TLS certificates only
|
||||||
|
// you can combine the scopes, for example 3 means password and public key, 5 password and keyboard
|
||||||
|
// interactive and so on
|
||||||
|
Scope int `json:"scope" mapstructure:"scope"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AuthConfig) validate() error {
|
||||||
|
authScopeMax := AuthScopePassword + AuthScopePublicKey + AuthScopeKeyboardInteractive + AuthScopeTLSCertificate
|
||||||
|
if c.Scope == 0 || c.Scope > authScopeMax {
|
||||||
|
return fmt.Errorf("invalid auth scope: %v", c.Scope)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type authPlugin struct {
|
||||||
|
config Config
|
||||||
|
service auth.Authenticator
|
||||||
|
client *plugin.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAuthPlugin(config Config) (*authPlugin, error) {
|
||||||
|
p := &authPlugin{
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
if err := p.initialize(); err != nil {
|
||||||
|
logger.Warn(logSender, "", "unable to create auth plugin: %v, config %+v", err, config)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *authPlugin) initialize() error {
|
||||||
|
killProcess(p.config.Cmd)
|
||||||
|
logger.Debug(logSender, "", "create new auth plugin %#v", p.config.Cmd)
|
||||||
|
if err := p.config.AuthOptions.validate(); err != nil {
|
||||||
|
return fmt.Errorf("invalid options for auth plugin %#v: %v", p.config.Cmd, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var secureConfig *plugin.SecureConfig
|
||||||
|
if p.config.SHA256Sum != "" {
|
||||||
|
secureConfig.Checksum = []byte(p.config.SHA256Sum)
|
||||||
|
secureConfig.Hash = sha256.New()
|
||||||
|
}
|
||||||
|
client := plugin.NewClient(&plugin.ClientConfig{
|
||||||
|
HandshakeConfig: auth.Handshake,
|
||||||
|
Plugins: auth.PluginMap,
|
||||||
|
Cmd: exec.Command(p.config.Cmd, p.config.Args...),
|
||||||
|
AllowedProtocols: []plugin.Protocol{
|
||||||
|
plugin.ProtocolGRPC,
|
||||||
|
},
|
||||||
|
AutoMTLS: p.config.AutoMTLS,
|
||||||
|
SecureConfig: secureConfig,
|
||||||
|
Managed: false,
|
||||||
|
Logger: &logger.HCLogAdapter{
|
||||||
|
Logger: hclog.New(&hclog.LoggerOptions{
|
||||||
|
Name: fmt.Sprintf("%v.%v", logSender, auth.PluginName),
|
||||||
|
Level: pluginsLogLevel,
|
||||||
|
DisableTime: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
rpcClient, err := client.Client()
|
||||||
|
if err != nil {
|
||||||
|
logger.Debug(logSender, "", "unable to get rpc client for kms plugin %#v: %v", p.config.Cmd, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
raw, err := rpcClient.Dispense(auth.PluginName)
|
||||||
|
if err != nil {
|
||||||
|
logger.Debug(logSender, "", "unable to get plugin %v from rpc client for command %#v: %v",
|
||||||
|
auth.PluginName, p.config.Cmd, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.service = raw.(auth.Authenticator)
|
||||||
|
p.client = client
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *authPlugin) exited() bool {
|
||||||
|
return p.client.Exited()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *authPlugin) cleanup() {
|
||||||
|
p.client.Kill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *authPlugin) checkUserAndPass(username, password, ip, protocol string, userAsJSON []byte) ([]byte, error) {
|
||||||
|
return p.service.CheckUserAndPass(username, password, ip, protocol, userAsJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *authPlugin) checkUserAndTLSCertificate(username, tlsCert, ip, protocol string, userAsJSON []byte) ([]byte, error) {
|
||||||
|
return p.service.CheckUserAndTLSCert(username, tlsCert, ip, protocol, userAsJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *authPlugin) checkUserAndPublicKey(username, pubKey, ip, protocol string, userAsJSON []byte) ([]byte, error) {
|
||||||
|
return p.service.CheckUserAndPublicKey(username, pubKey, ip, protocol, userAsJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *authPlugin) checkUserAndKeyboardInteractive(username, ip, protocol string, userAsJSON []byte) ([]byte, error) {
|
||||||
|
return p.service.CheckUserAndKeyboardInteractive(username, ip, protocol, userAsJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *authPlugin) sendKeyboardIteractiveRequest(req *KeyboardAuthRequest) (*KeyboardAuthResponse, error) {
|
||||||
|
instructions, questions, echos, authResult, checkPassword, err := p.service.SendKeyboardAuthRequest(
|
||||||
|
req.RequestID, req.Username, req.Password, req.IP, req.Answers, req.Questions, int32(req.Step))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &KeyboardAuthResponse{
|
||||||
|
Instruction: instructions,
|
||||||
|
Questions: questions,
|
||||||
|
Echos: echos,
|
||||||
|
AuthResult: authResult,
|
||||||
|
CheckPwd: checkPassword,
|
||||||
|
}, nil
|
||||||
|
}
|
59
sdk/plugin/auth/auth.go
Normal file
59
sdk/plugin/auth/auth.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// Package auth defines the implementation for authentication plugins.
|
||||||
|
// Authentication plugins allow to authenticate external users
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-plugin"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
"github.com/drakkan/sftpgo/v2/sdk/plugin/auth/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PluginName defines the name for a notifier plugin
|
||||||
|
PluginName = "auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handshake is a common handshake that is shared by plugin and host.
|
||||||
|
var Handshake = plugin.HandshakeConfig{
|
||||||
|
ProtocolVersion: 1,
|
||||||
|
MagicCookieKey: "SFTPGO_AUTH_PLUGIN",
|
||||||
|
MagicCookieValue: "d1ed507d-d2be-4a38-a460-6fe0b2cc7efc",
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginMap is the map of plugins we can dispense.
|
||||||
|
var PluginMap = map[string]plugin.Plugin{
|
||||||
|
PluginName: &Plugin{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticator defines the interface for authentication plugins
|
||||||
|
type Authenticator interface {
|
||||||
|
CheckUserAndPass(username, password, ip, protocol string, userAsJSON []byte) ([]byte, error)
|
||||||
|
CheckUserAndTLSCert(username, tlsCert, ip, protocol string, userAsJSON []byte) ([]byte, error)
|
||||||
|
CheckUserAndPublicKey(username, pubKey, ip, protocol string, userAsJSON []byte) ([]byte, error)
|
||||||
|
CheckUserAndKeyboardInteractive(username, ip, protocol string, userAsJSON []byte) ([]byte, error)
|
||||||
|
SendKeyboardAuthRequest(requestID, username, password, ip string, answers, questions []string, step int32) (string, []string, []bool, int, int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin defines the implementation to serve/connect to an authe plugin
|
||||||
|
type Plugin struct {
|
||||||
|
plugin.Plugin
|
||||||
|
Impl Authenticator
|
||||||
|
}
|
||||||
|
|
||||||
|
// GRPCServer defines the GRPC server implementation for this plugin
|
||||||
|
func (p *Plugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
|
||||||
|
proto.RegisterAuthServer(s, &GRPCServer{
|
||||||
|
Impl: p.Impl,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GRPCClient defines the GRPC client implementation for this plugin
|
||||||
|
func (p *Plugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
|
||||||
|
return &GRPCClient{
|
||||||
|
client: proto.NewAuthClient(c),
|
||||||
|
}, nil
|
||||||
|
}
|
165
sdk/plugin/auth/grpc.go
Normal file
165
sdk/plugin/auth/grpc.go
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/drakkan/sftpgo/v2/sdk/plugin/auth/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
rpcTimeout = 20 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// GRPCClient is an implementation of Authenticator interface that talks over RPC.
|
||||||
|
type GRPCClient struct {
|
||||||
|
client proto.AuthClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckUserAndPass implements the Authenticator interface
|
||||||
|
func (c *GRPCClient) CheckUserAndPass(username, password, ip, protocol string, userAsJSON []byte) ([]byte, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resp, err := c.client.CheckUserAndPass(ctx, &proto.CheckUserAndPassRequest{
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
Ip: ip,
|
||||||
|
Protocol: protocol,
|
||||||
|
User: userAsJSON,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.User, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckUserAndTLSCert implements the Authenticator interface
|
||||||
|
func (c *GRPCClient) CheckUserAndTLSCert(username, tlsCert, ip, protocol string, userAsJSON []byte) ([]byte, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resp, err := c.client.CheckUserAndTLSCert(ctx, &proto.CheckUserAndTLSCertRequest{
|
||||||
|
Username: username,
|
||||||
|
TlsCert: tlsCert,
|
||||||
|
Ip: ip,
|
||||||
|
Protocol: protocol,
|
||||||
|
User: userAsJSON,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.User, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckUserAndPublicKey implements the Authenticator interface
|
||||||
|
func (c *GRPCClient) CheckUserAndPublicKey(username, pubKey, ip, protocol string, userAsJSON []byte) ([]byte, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resp, err := c.client.CheckUserAndPublicKey(ctx, &proto.CheckUserAndPublicKeyRequest{
|
||||||
|
Username: username,
|
||||||
|
PubKey: pubKey,
|
||||||
|
Ip: ip,
|
||||||
|
Protocol: protocol,
|
||||||
|
User: userAsJSON,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.User, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckUserAndKeyboardInteractive implements the Authenticator interface
|
||||||
|
func (c *GRPCClient) CheckUserAndKeyboardInteractive(username, ip, protocol string, userAsJSON []byte) ([]byte, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resp, err := c.client.CheckUserAndKeyboardInteractive(ctx, &proto.CheckUserAndKeyboardInteractiveRequest{
|
||||||
|
Username: username,
|
||||||
|
Ip: ip,
|
||||||
|
Protocol: protocol,
|
||||||
|
User: userAsJSON,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.User, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendKeyboardAuthRequest implements the Authenticator interface
|
||||||
|
func (c *GRPCClient) SendKeyboardAuthRequest(requestID, username, password, ip string, answers, questions []string, step int32) (string, []string, []bool, int, int, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resp, err := c.client.SendKeyboardAuthRequest(ctx, &proto.KeyboardAuthRequest{
|
||||||
|
RequestID: requestID,
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
Ip: ip,
|
||||||
|
Answers: answers,
|
||||||
|
Questions: questions,
|
||||||
|
Step: step,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
return resp.Instructions, resp.Questions, resp.Echos, int(resp.AuthResult), int(resp.CheckPassword), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GRPCServer defines the gRPC server that GRPCClient talks to.
|
||||||
|
type GRPCServer struct {
|
||||||
|
Impl Authenticator
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckUserAndPass implements the server side check user and password method
|
||||||
|
func (s *GRPCServer) CheckUserAndPass(ctx context.Context, req *proto.CheckUserAndPassRequest) (*proto.AuthResponse, error) {
|
||||||
|
user, err := s.Impl.CheckUserAndPass(req.Username, req.Password, req.Ip, req.Protocol, req.User)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &proto.AuthResponse{User: user}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckUserAndTLSCert implements the server side check user and tls certificate method
|
||||||
|
func (s *GRPCServer) CheckUserAndTLSCert(ctx context.Context, req *proto.CheckUserAndTLSCertRequest) (*proto.AuthResponse, error) {
|
||||||
|
user, err := s.Impl.CheckUserAndTLSCert(req.Username, req.TlsCert, req.Ip, req.Protocol, req.User)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &proto.AuthResponse{User: user}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckUserAndPublicKey implements the server side check user and public key method
|
||||||
|
func (s *GRPCServer) CheckUserAndPublicKey(ctx context.Context, req *proto.CheckUserAndPublicKeyRequest) (*proto.AuthResponse, error) {
|
||||||
|
user, err := s.Impl.CheckUserAndPublicKey(req.Username, req.PubKey, req.Ip, req.Protocol, req.User)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &proto.AuthResponse{User: user}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckUserAndKeyboardInteractive implements the server side check user and keyboard interactive method
|
||||||
|
func (s *GRPCServer) CheckUserAndKeyboardInteractive(ctx context.Context, req *proto.CheckUserAndKeyboardInteractiveRequest) (*proto.AuthResponse, error) {
|
||||||
|
user, err := s.Impl.CheckUserAndKeyboardInteractive(req.Username, req.Ip, req.Protocol, req.User)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &proto.AuthResponse{User: user}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendKeyboardAuthRequest implements the server side method to send a keyboard interactive authentication request
|
||||||
|
func (s *GRPCServer) SendKeyboardAuthRequest(ctx context.Context, req *proto.KeyboardAuthRequest) (*proto.KeyboardAuthResponse, error) {
|
||||||
|
instructions, questions, echos, authResult, checkPwd, err := s.Impl.SendKeyboardAuthRequest(req.RequestID, req.Username,
|
||||||
|
req.Password, req.Ip, req.Answers, req.Questions, req.Step)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &proto.KeyboardAuthResponse{
|
||||||
|
Instructions: instructions,
|
||||||
|
Questions: questions,
|
||||||
|
Echos: echos,
|
||||||
|
AuthResult: int32(authResult),
|
||||||
|
CheckPassword: int32(checkPwd),
|
||||||
|
}, nil
|
||||||
|
}
|
1028
sdk/plugin/auth/proto/auth.pb.go
Normal file
1028
sdk/plugin/auth/proto/auth.pb.go
Normal file
File diff suppressed because it is too large
Load diff
65
sdk/plugin/auth/proto/auth.proto
Normal file
65
sdk/plugin/auth/proto/auth.proto
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
package proto;
|
||||||
|
|
||||||
|
option go_package = "sdk/plugin/auth/proto";
|
||||||
|
|
||||||
|
message CheckUserAndPassRequest {
|
||||||
|
string username = 1;
|
||||||
|
string password = 2;
|
||||||
|
string ip = 3;
|
||||||
|
string protocol = 4;
|
||||||
|
bytes user = 5; // SFTPGo user JSON serialized
|
||||||
|
}
|
||||||
|
|
||||||
|
message CheckUserAndTLSCertRequest {
|
||||||
|
string username = 1;
|
||||||
|
string tlsCert = 2; // tls certificate pem encoded
|
||||||
|
string ip = 3;
|
||||||
|
string protocol = 4;
|
||||||
|
bytes user = 5; // SFTPGo user JSON serialized
|
||||||
|
}
|
||||||
|
|
||||||
|
message CheckUserAndPublicKeyRequest {
|
||||||
|
string username = 1;
|
||||||
|
string pubKey = 2;
|
||||||
|
string ip = 3;
|
||||||
|
string protocol = 4;
|
||||||
|
bytes user = 5; // SFTPGo user JSON serialized
|
||||||
|
}
|
||||||
|
|
||||||
|
message CheckUserAndKeyboardInteractiveRequest {
|
||||||
|
string username = 1;
|
||||||
|
string ip = 2;
|
||||||
|
string protocol = 3;
|
||||||
|
bytes user = 4; // SFTPGo user JSON serialized
|
||||||
|
}
|
||||||
|
|
||||||
|
message KeyboardAuthRequest {
|
||||||
|
string requestID = 1;
|
||||||
|
string username = 2;
|
||||||
|
string password = 3;
|
||||||
|
string ip = 4;
|
||||||
|
repeated string answers = 5;
|
||||||
|
repeated string questions = 6;
|
||||||
|
int32 step = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message KeyboardAuthResponse {
|
||||||
|
string instructions = 1;
|
||||||
|
repeated string questions = 2;
|
||||||
|
repeated bool echos = 3;
|
||||||
|
int32 auth_result = 4;
|
||||||
|
int32 check_password = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AuthResponse {
|
||||||
|
bytes user = 1; // SFTPGo user JSON serialized
|
||||||
|
}
|
||||||
|
|
||||||
|
service Auth {
|
||||||
|
rpc CheckUserAndPass(CheckUserAndPassRequest) returns (AuthResponse);
|
||||||
|
rpc CheckUserAndTLSCert(CheckUserAndTLSCertRequest) returns (AuthResponse);
|
||||||
|
rpc CheckUserAndPublicKey(CheckUserAndPublicKeyRequest) returns (AuthResponse);
|
||||||
|
rpc CheckUserAndKeyboardInteractive(CheckUserAndKeyboardInteractiveRequest) returns (AuthResponse);
|
||||||
|
rpc SendKeyboardAuthRequest(KeyboardAuthRequest) returns (KeyboardAuthResponse);
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ type KMSConfig struct {
|
||||||
EncryptedStatus string `json:"encrypted_status" mapstructure:"encrypted_status"`
|
EncryptedStatus string `json:"encrypted_status" mapstructure:"encrypted_status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *KMSConfig) isValid() error {
|
func (c *KMSConfig) validate() error {
|
||||||
if !util.IsStringInSlice(c.Scheme, validKMSSchemes) {
|
if !util.IsStringInSlice(c.Scheme, validKMSSchemes) {
|
||||||
return fmt.Errorf("invalid kms scheme: %v", c.Scheme)
|
return fmt.Errorf("invalid kms scheme: %v", c.Scheme)
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ func newKMSPlugin(config Config) (*kmsPlugin, error) {
|
||||||
func (p *kmsPlugin) initialize() error {
|
func (p *kmsPlugin) initialize() error {
|
||||||
killProcess(p.config.Cmd)
|
killProcess(p.config.Cmd)
|
||||||
logger.Debug(logSender, "", "create new kms plugin %#v", p.config.Cmd)
|
logger.Debug(logSender, "", "create new kms plugin %#v", p.config.Cmd)
|
||||||
if err := p.config.KMSOptions.isValid(); err != nil {
|
if err := p.config.KMSOptions.validate(); err != nil {
|
||||||
return fmt.Errorf("invalid options for kms plugin %#v: %v", p.config.Cmd, err)
|
return fmt.Errorf("invalid options for kms plugin %#v: %v", p.config.Cmd, err)
|
||||||
}
|
}
|
||||||
var secureConfig *plugin.SecureConfig
|
var secureConfig *plugin.SecureConfig
|
||||||
|
|
|
@ -2,5 +2,6 @@
|
||||||
|
|
||||||
protoc notifier/proto/notifier.proto --go_out=plugins=grpc:../.. --go_out=../../..
|
protoc notifier/proto/notifier.proto --go_out=plugins=grpc:../.. --go_out=../../..
|
||||||
protoc kms/proto/kms.proto --go_out=plugins=grpc:../.. --go_out=../../..
|
protoc kms/proto/kms.proto --go_out=plugins=grpc:../.. --go_out=../../..
|
||||||
|
protoc auth/proto/auth.proto --go_out=plugins=grpc:../.. --go_out=../../..
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Package notifier defines the implementation for event notifier plugins.
|
// Package notifier defines the implementation for event notifier plugins.
|
||||||
// Notifier plugins allow to receive filesystem events such as file uploads,
|
// Notifier plugins allow to receive notifications for supported filesystem
|
||||||
// downloads etc. and user events such as add, update, delete.
|
// events such as file uploads, downloads etc. and user events such as add,
|
||||||
|
// update, delete.
|
||||||
package notifier
|
package notifier
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -144,7 +144,7 @@ type UserEvent struct {
|
||||||
|
|
||||||
Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
|
Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
|
||||||
Action string `protobuf:"bytes,2,opt,name=action,proto3" json:"action,omitempty"`
|
Action string `protobuf:"bytes,2,opt,name=action,proto3" json:"action,omitempty"`
|
||||||
User []byte `protobuf:"bytes,3,opt,name=user,proto3" json:"user,omitempty"` // SFTPGo user json serialized
|
User []byte `protobuf:"bytes,3,opt,name=user,proto3" json:"user,omitempty"` // SFTPGo user JSON serialized
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *UserEvent) Reset() {
|
func (x *UserEvent) Reset() {
|
||||||
|
|
|
@ -21,7 +21,7 @@ message FsEvent {
|
||||||
message UserEvent {
|
message UserEvent {
|
||||||
google.protobuf.Timestamp timestamp = 1;
|
google.protobuf.Timestamp timestamp = 1;
|
||||||
string action = 2;
|
string action = 2;
|
||||||
bytes user = 3; // SFTPGo user json serialized
|
bytes user = 3; // SFTPGo user JSON serialized
|
||||||
}
|
}
|
||||||
|
|
||||||
service Notifier {
|
service Notifier {
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
package plugin
|
package plugin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
@ -11,8 +13,10 @@ import (
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/v2/kms"
|
"github.com/drakkan/sftpgo/v2/kms"
|
||||||
"github.com/drakkan/sftpgo/v2/logger"
|
"github.com/drakkan/sftpgo/v2/logger"
|
||||||
|
"github.com/drakkan/sftpgo/v2/sdk/plugin/auth"
|
||||||
kmsplugin "github.com/drakkan/sftpgo/v2/sdk/plugin/kms"
|
kmsplugin "github.com/drakkan/sftpgo/v2/sdk/plugin/kms"
|
||||||
"github.com/drakkan/sftpgo/v2/sdk/plugin/notifier"
|
"github.com/drakkan/sftpgo/v2/sdk/plugin/notifier"
|
||||||
|
"github.com/drakkan/sftpgo/v2/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -38,6 +42,8 @@ type Config struct {
|
||||||
NotifierOptions NotifierConfig `json:"notifier_options" mapstructure:"notifier_options"`
|
NotifierOptions NotifierConfig `json:"notifier_options" mapstructure:"notifier_options"`
|
||||||
// KMSOptions defines options for a KMS plugin
|
// KMSOptions defines options for a KMS plugin
|
||||||
KMSOptions KMSConfig `json:"kms_options" mapstructure:"kms_options"`
|
KMSOptions KMSConfig `json:"kms_options" mapstructure:"kms_options"`
|
||||||
|
// AuthOptions defines options for authentication plugins
|
||||||
|
AuthOptions AuthConfig `json:"auth_options" mapstructure:"auth_options"`
|
||||||
// Path to the plugin executable
|
// Path to the plugin executable
|
||||||
Cmd string `json:"cmd" mapstructure:"cmd"`
|
Cmd string `json:"cmd" mapstructure:"cmd"`
|
||||||
// Args to pass to the plugin executable
|
// Args to pass to the plugin executable
|
||||||
|
@ -69,19 +75,23 @@ type Manager struct {
|
||||||
closed int32
|
closed int32
|
||||||
done chan bool
|
done chan bool
|
||||||
// List of configured plugins
|
// List of configured plugins
|
||||||
Configs []Config `json:"plugins" mapstructure:"plugins"`
|
Configs []Config `json:"plugins" mapstructure:"plugins"`
|
||||||
notifLock sync.RWMutex
|
notifLock sync.RWMutex
|
||||||
notifiers []*notifierPlugin
|
notifiers []*notifierPlugin
|
||||||
kmsLock sync.RWMutex
|
kmsLock sync.RWMutex
|
||||||
kms []*kmsPlugin
|
kms []*kmsPlugin
|
||||||
|
authLock sync.RWMutex
|
||||||
|
auths []*authPlugin
|
||||||
|
authScopes int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize initializes the configured plugins
|
// Initialize initializes the configured plugins
|
||||||
func Initialize(configs []Config, logVerbose bool) error {
|
func Initialize(configs []Config, logVerbose bool) error {
|
||||||
Handler = Manager{
|
Handler = Manager{
|
||||||
Configs: configs,
|
Configs: configs,
|
||||||
done: make(chan bool),
|
done: make(chan bool),
|
||||||
closed: 0,
|
closed: 0,
|
||||||
|
authScopes: -1,
|
||||||
}
|
}
|
||||||
if len(configs) == 0 {
|
if len(configs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
@ -117,6 +127,17 @@ func Initialize(configs []Config, logVerbose bool) error {
|
||||||
Handler.Configs[idx].newKMSPluginSecretProvider)
|
Handler.Configs[idx].newKMSPluginSecretProvider)
|
||||||
logger.Debug(logSender, "", "registered secret provider for scheme: %v, encrypted status: %v",
|
logger.Debug(logSender, "", "registered secret provider for scheme: %v, encrypted status: %v",
|
||||||
config.KMSOptions.Scheme, config.KMSOptions.EncryptedStatus)
|
config.KMSOptions.Scheme, config.KMSOptions.EncryptedStatus)
|
||||||
|
case auth.PluginName:
|
||||||
|
plugin, err := newAuthPlugin(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
Handler.auths = append(Handler.auths, plugin)
|
||||||
|
if Handler.authScopes == -1 {
|
||||||
|
Handler.authScopes = config.AuthOptions.Scope
|
||||||
|
} else {
|
||||||
|
Handler.authScopes |= config.AuthOptions.Scope
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported plugin type: %v", config.Type)
|
return fmt.Errorf("unsupported plugin type: %v", config.Type)
|
||||||
}
|
}
|
||||||
|
@ -181,6 +202,133 @@ func (m *Manager) kmsDecrypt(secret kms.BaseSecret, url string, masterKey string
|
||||||
return plugin.Decrypt(secret, url, masterKey)
|
return plugin.Decrypt(secret, url, masterKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasAuthScope returns true if there is an auth plugin that support the specified scope
|
||||||
|
func (m *Manager) HasAuthScope(scope int) bool {
|
||||||
|
if m.authScopes == -1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return m.authScopes&scope != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate tries to authenticate the specified user using an external plugin
|
||||||
|
func (m *Manager) Authenticate(username, password, ip, protocol string, pkey string,
|
||||||
|
tlsCert *x509.Certificate, authScope int, userAsJSON []byte,
|
||||||
|
) ([]byte, error) {
|
||||||
|
switch authScope {
|
||||||
|
case AuthScopePassword:
|
||||||
|
return m.checkUserAndPass(username, password, ip, protocol, userAsJSON)
|
||||||
|
case AuthScopePublicKey:
|
||||||
|
return m.checkUserAndPublicKey(username, pkey, ip, protocol, userAsJSON)
|
||||||
|
case AuthScopeKeyboardInteractive:
|
||||||
|
return m.checkUserAndKeyboardInteractive(username, ip, protocol, userAsJSON)
|
||||||
|
case AuthScopeTLSCertificate:
|
||||||
|
cert, err := util.EncodeTLSCertToPem(tlsCert)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn(logSender, "", "unable to encode tls certificate to pem: %v", err)
|
||||||
|
return nil, fmt.Errorf("unable to encode tls cert to pem: %w", err)
|
||||||
|
}
|
||||||
|
return m.checkUserAndTLSCert(username, cert, ip, protocol, userAsJSON)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported auth scope: %v", authScope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteKeyboardInteractiveStep executes a keyboard interactive step
|
||||||
|
func (m *Manager) ExecuteKeyboardInteractiveStep(req *KeyboardAuthRequest) (*KeyboardAuthResponse, error) {
|
||||||
|
var plugin *authPlugin
|
||||||
|
|
||||||
|
m.authLock.Lock()
|
||||||
|
for _, p := range m.auths {
|
||||||
|
if p.config.AuthOptions.Scope&AuthScopePassword != 0 {
|
||||||
|
plugin = p
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.authLock.Unlock()
|
||||||
|
|
||||||
|
if plugin == nil {
|
||||||
|
return nil, errors.New("no auth plugin configured for keyaboard interactive authentication step")
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugin.sendKeyboardIteractiveRequest(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) checkUserAndPass(username, password, ip, protocol string, userAsJSON []byte) ([]byte, error) {
|
||||||
|
var plugin *authPlugin
|
||||||
|
|
||||||
|
m.authLock.Lock()
|
||||||
|
for _, p := range m.auths {
|
||||||
|
if p.config.AuthOptions.Scope&AuthScopePassword != 0 {
|
||||||
|
plugin = p
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.authLock.Unlock()
|
||||||
|
|
||||||
|
if plugin == nil {
|
||||||
|
return nil, errors.New("no auth plugin configured for password checking")
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugin.checkUserAndPass(username, password, ip, protocol, userAsJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) checkUserAndPublicKey(username, pubKey, ip, protocol string, userAsJSON []byte) ([]byte, error) {
|
||||||
|
var plugin *authPlugin
|
||||||
|
|
||||||
|
m.authLock.Lock()
|
||||||
|
for _, p := range m.auths {
|
||||||
|
if p.config.AuthOptions.Scope&AuthScopePublicKey != 0 {
|
||||||
|
plugin = p
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.authLock.Unlock()
|
||||||
|
|
||||||
|
if plugin == nil {
|
||||||
|
return nil, errors.New("no auth plugin configured for public key checking")
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugin.checkUserAndPublicKey(username, pubKey, ip, protocol, userAsJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) checkUserAndTLSCert(username, tlsCert, ip, protocol string, userAsJSON []byte) ([]byte, error) {
|
||||||
|
var plugin *authPlugin
|
||||||
|
|
||||||
|
m.authLock.Lock()
|
||||||
|
for _, p := range m.auths {
|
||||||
|
if p.config.AuthOptions.Scope&AuthScopeTLSCertificate != 0 {
|
||||||
|
plugin = p
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.authLock.Unlock()
|
||||||
|
|
||||||
|
if plugin == nil {
|
||||||
|
return nil, errors.New("no auth plugin configured for TLS certificate checking")
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugin.checkUserAndTLSCertificate(username, tlsCert, ip, protocol, userAsJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) checkUserAndKeyboardInteractive(username, ip, protocol string, userAsJSON []byte) ([]byte, error) {
|
||||||
|
var plugin *authPlugin
|
||||||
|
|
||||||
|
m.authLock.Lock()
|
||||||
|
for _, p := range m.auths {
|
||||||
|
if p.config.AuthOptions.Scope&AuthScopeKeyboardInteractive != 0 {
|
||||||
|
plugin = p
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.authLock.Unlock()
|
||||||
|
|
||||||
|
if plugin == nil {
|
||||||
|
return nil, errors.New("no auth plugin configured for keyboard interactive checking")
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugin.checkUserAndKeyboardInteractive(username, ip, protocol, userAsJSON)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) checkCrashedPlugins() {
|
func (m *Manager) checkCrashedPlugins() {
|
||||||
m.notifLock.RLock()
|
m.notifLock.RLock()
|
||||||
for idx, n := range m.notifiers {
|
for idx, n := range m.notifiers {
|
||||||
|
@ -203,6 +351,16 @@ func (m *Manager) checkCrashedPlugins() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.kmsLock.RUnlock()
|
m.kmsLock.RUnlock()
|
||||||
|
|
||||||
|
m.authLock.RLock()
|
||||||
|
for idx, a := range m.auths {
|
||||||
|
if a.exited() {
|
||||||
|
defer func(cfg Config, index int) {
|
||||||
|
Handler.restartAuthPlugin(cfg, index)
|
||||||
|
}(a.config, idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.authLock.RUnlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) restartNotifierPlugin(config Config, idx int) {
|
func (m *Manager) restartNotifierPlugin(config Config, idx int) {
|
||||||
|
@ -239,6 +397,22 @@ func (m *Manager) restartKMSPlugin(config Config, idx int) {
|
||||||
m.kmsLock.Unlock()
|
m.kmsLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) restartAuthPlugin(config Config, idx int) {
|
||||||
|
if atomic.LoadInt32(&m.closed) == 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info(logSender, "", "try to restart crashed auth plugin %#v, idx: %v", config.Cmd, idx)
|
||||||
|
plugin, err := newAuthPlugin(config)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn(logSender, "", "unable to restart auth plugin %#v, err: %v", config.Cmd, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.authLock.Lock()
|
||||||
|
m.auths[idx] = plugin
|
||||||
|
m.authLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup releases all the active plugins
|
// Cleanup releases all the active plugins
|
||||||
func (m *Manager) Cleanup() {
|
func (m *Manager) Cleanup() {
|
||||||
atomic.StoreInt32(&m.closed, 1)
|
atomic.StoreInt32(&m.closed, 1)
|
||||||
|
@ -256,6 +430,13 @@ func (m *Manager) Cleanup() {
|
||||||
k.cleanup()
|
k.cleanup()
|
||||||
}
|
}
|
||||||
m.kmsLock.Unlock()
|
m.kmsLock.Unlock()
|
||||||
|
|
||||||
|
m.authLock.Lock()
|
||||||
|
for _, a := range m.auths {
|
||||||
|
logger.Debug(logSender, "", "cleanup auth plugin %v", a.config.Cmd)
|
||||||
|
a.cleanup()
|
||||||
|
}
|
||||||
|
m.authLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func startCheckTicker() {
|
func startCheckTicker() {
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||||
"github.com/drakkan/sftpgo/v2/logger"
|
"github.com/drakkan/sftpgo/v2/logger"
|
||||||
"github.com/drakkan/sftpgo/v2/metric"
|
"github.com/drakkan/sftpgo/v2/metric"
|
||||||
|
"github.com/drakkan/sftpgo/v2/sdk/plugin"
|
||||||
"github.com/drakkan/sftpgo/v2/util"
|
"github.com/drakkan/sftpgo/v2/util"
|
||||||
"github.com/drakkan/sftpgo/v2/vfs"
|
"github.com/drakkan/sftpgo/v2/vfs"
|
||||||
)
|
)
|
||||||
|
@ -306,22 +307,24 @@ func (c *Configuration) configureLoginBanner(serverConfig *ssh.ServerConfig, con
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Configuration) configureKeyboardInteractiveAuth(serverConfig *ssh.ServerConfig) {
|
func (c *Configuration) configureKeyboardInteractiveAuth(serverConfig *ssh.ServerConfig) {
|
||||||
if c.KeyboardInteractiveHook == "" {
|
if c.KeyboardInteractiveHook == "" && !plugin.Handler.HasAuthScope(plugin.AuthScopeKeyboardInteractive) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(c.KeyboardInteractiveHook, "http") {
|
if c.KeyboardInteractiveHook != "" {
|
||||||
if !filepath.IsAbs(c.KeyboardInteractiveHook) {
|
if !strings.HasPrefix(c.KeyboardInteractiveHook, "http") {
|
||||||
logger.WarnToConsole("invalid keyboard interactive authentication program: %#v must be an absolute path",
|
if !filepath.IsAbs(c.KeyboardInteractiveHook) {
|
||||||
c.KeyboardInteractiveHook)
|
logger.WarnToConsole("invalid keyboard interactive authentication program: %#v must be an absolute path",
|
||||||
logger.Warn(logSender, "", "invalid keyboard interactive authentication program: %#v must be an absolute path",
|
c.KeyboardInteractiveHook)
|
||||||
c.KeyboardInteractiveHook)
|
logger.Warn(logSender, "", "invalid keyboard interactive authentication program: %#v must be an absolute path",
|
||||||
return
|
c.KeyboardInteractiveHook)
|
||||||
}
|
return
|
||||||
_, err := os.Stat(c.KeyboardInteractiveHook)
|
}
|
||||||
if err != nil {
|
_, err := os.Stat(c.KeyboardInteractiveHook)
|
||||||
logger.WarnToConsole("invalid keyboard interactive authentication program:: %v", err)
|
if err != nil {
|
||||||
logger.Warn(logSender, "", "invalid keyboard interactive authentication program:: %v", err)
|
logger.WarnToConsole("invalid keyboard interactive authentication program:: %v", err)
|
||||||
return
|
logger.Warn(logSender, "", "invalid keyboard interactive authentication program:: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
serverConfig.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {
|
serverConfig.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {
|
||||||
|
|
Loading…
Reference in a new issue