diff --git a/common/defender.go b/common/defender.go index de39aedc..77c1ce7c 100644 --- a/common/defender.go +++ b/common/defender.go @@ -16,7 +16,7 @@ import ( "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 // Supported host events diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index 733b79ef..8644268d 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -356,23 +356,6 @@ func (d *BackupData) HasFolder(name string) bool { 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 { Username string `json:"username"` 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) } 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) - if err != nil { - return user, loginMethod, err - } - } - if config.PreLoginHook != "" { + } else if config.PreLoginHook != "" { 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) } @@ -699,6 +680,9 @@ func CheckCompositeCredentials(username, password, ip, loginMethod, protocol str // CheckUserBeforeTLSAuth checks if a user exits before trying mutual TLS 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) { 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 // given TLS certificate allow authentication without password 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) { user, err := doExternalAuth(username, "", nil, "", ip, protocol, tlsCert) 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 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) { user, err := doExternalAuth(username, password, nil, "", ip, protocol, 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 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) { user, err := doExternalAuth(username, "", pubKey, "", ip, protocol, 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) { var user User 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) } else if config.PreLoginHook != "" { user, err = executePreLoginHook(username, SSHLoginMethodKeyboardInteractive, ip, protocol) @@ -1824,51 +1831,79 @@ func terminateInteractiveAuthProgram(cmd *exec.Cmd, isFinished bool) { } } -func validateKeyboardAuthResponse(response keyboardAuthHookResponse) 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 +func sendKeyboardAuthHTTPReq(url string, request *plugin.KeyboardAuthRequest) (*plugin.KeyboardAuthResponse, error) { reqAsJSON, err := json.Marshal(request) if err != nil { 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)) if err != nil { providerLog(logger.LevelWarn, "error getting keyboard interactive auth hook HTTP response: %v", err) - return response, err + return nil, err } defer resp.Body.Close() 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) - 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) { authResult := 0 requestID := xid.New().String() - req := keyboardAuthHookRequest{ + authStep := 1 + req := &plugin.KeyboardAuthRequest{ Username: user.Username, IP: ip, Password: user.Password, RequestID: requestID, + Step: authStep, } - var response keyboardAuthHookResponse + var response *plugin.KeyboardAuthResponse var err error for { response, err = sendKeyboardAuthHTTPReq(authHook, req) @@ -1878,15 +1913,18 @@ func executeKeyboardInteractiveHTTPHook(user *User, authHook string, client ssh. if response.AuthResult != 0 { 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 } answers, err := getKeyboardInteractiveAnswers(client, response, user, ip, protocol) if err != nil { return authResult, err } - req = keyboardAuthHookRequest{ + authStep++ + req = &plugin.KeyboardAuthRequest{ RequestID: requestID, + Step: authStep, Username: user.Username, Password: user.Password, 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) { questions := response.Questions answers, err := client(user.Username, response.Instruction, questions, response.Echos) @@ -1920,7 +1958,7 @@ func getKeyboardInteractiveAnswers(client ssh.KeyboardInteractiveChallenge, resp 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 { answers, err := getKeyboardInteractiveAnswers(client, response, user, ip, protocol) if err != nil { @@ -1964,7 +2002,7 @@ func executeKeyboardInteractiveProgram(user *User, authHook string, client ssh.K var once sync.Once scanner := bufio.NewScanner(stdout) for scanner.Scan() { - var response keyboardAuthHookResponse + var response plugin.KeyboardAuthResponse err = json.Unmarshal(scanner.Bytes(), &response) if err != nil { 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 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) }) break } go func() { - err := handleProgramInteractiveQuestions(client, response, user, stdin, ip, protocol) + err := handleProgramInteractiveQuestions(client, &response, user, stdin, ip, protocol) if err != nil { 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) { var authResult int 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) } else { 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) } +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) { var userAsJSON []byte u, err := provider.userExists(username) diff --git a/docs/full-configuration.md b/docs/full-configuration.md index bd89e2a3..b515bc7a 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -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_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: - - `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. - `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. @@ -260,6 +260,8 @@ The configuration file contains the following sections: - `kms_options`, struct. Defines the options for kms plugins. - `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`. + - `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. - `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. diff --git a/docs/keyboard-interactive.md b/docs/keyboard-interactive.md index 842974c7..1f30d8cc 100644 --- a/docs/keyboard-interactive.md +++ b/docs/keyboard-interactive.md @@ -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: - `request_id`, string. Unique request identifier +- `step`, integer. Counter starting from 1 - `username`, string - `ip`, string - `password`, string. This is the hashed password as stored inside the data provider @@ -95,7 +96,7 @@ Content-Length: 189 Content-Type: application/json 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. @@ -123,7 +124,7 @@ Content-Length: 233 Content-Type: application/json 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: @@ -149,7 +150,7 @@ Content-Length: 239 Content-Type: application/json 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: diff --git a/docs/plugins.md b/docs/plugins.md index b0504264..cf2b656f 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -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. +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) ## Available plugins diff --git a/sdk/plugin/auth.go b/sdk/plugin/auth.go new file mode 100644 index 00000000..4cf408ff --- /dev/null +++ b/sdk/plugin/auth.go @@ -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 +} diff --git a/sdk/plugin/auth/auth.go b/sdk/plugin/auth/auth.go new file mode 100644 index 00000000..efed0ce6 --- /dev/null +++ b/sdk/plugin/auth/auth.go @@ -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 +} diff --git a/sdk/plugin/auth/grpc.go b/sdk/plugin/auth/grpc.go new file mode 100644 index 00000000..7319307c --- /dev/null +++ b/sdk/plugin/auth/grpc.go @@ -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 +} diff --git a/sdk/plugin/auth/proto/auth.pb.go b/sdk/plugin/auth/proto/auth.pb.go new file mode 100644 index 00000000..78fde998 --- /dev/null +++ b/sdk/plugin/auth/proto/auth.pb.go @@ -0,0 +1,1028 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v3.17.3 +// source: auth/proto/auth.proto + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type CheckUserAndPassRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` + Ip string `protobuf:"bytes,3,opt,name=ip,proto3" json:"ip,omitempty"` + Protocol string `protobuf:"bytes,4,opt,name=protocol,proto3" json:"protocol,omitempty"` + User []byte `protobuf:"bytes,5,opt,name=user,proto3" json:"user,omitempty"` // SFTPGo user JSON serialized +} + +func (x *CheckUserAndPassRequest) Reset() { + *x = CheckUserAndPassRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_auth_proto_auth_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CheckUserAndPassRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckUserAndPassRequest) ProtoMessage() {} + +func (x *CheckUserAndPassRequest) ProtoReflect() protoreflect.Message { + mi := &file_auth_proto_auth_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckUserAndPassRequest.ProtoReflect.Descriptor instead. +func (*CheckUserAndPassRequest) Descriptor() ([]byte, []int) { + return file_auth_proto_auth_proto_rawDescGZIP(), []int{0} +} + +func (x *CheckUserAndPassRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *CheckUserAndPassRequest) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *CheckUserAndPassRequest) GetIp() string { + if x != nil { + return x.Ip + } + return "" +} + +func (x *CheckUserAndPassRequest) GetProtocol() string { + if x != nil { + return x.Protocol + } + return "" +} + +func (x *CheckUserAndPassRequest) GetUser() []byte { + if x != nil { + return x.User + } + return nil +} + +type CheckUserAndTLSCertRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + TlsCert string `protobuf:"bytes,2,opt,name=tlsCert,proto3" json:"tlsCert,omitempty"` // tls certificate pem encoded + Ip string `protobuf:"bytes,3,opt,name=ip,proto3" json:"ip,omitempty"` + Protocol string `protobuf:"bytes,4,opt,name=protocol,proto3" json:"protocol,omitempty"` + User []byte `protobuf:"bytes,5,opt,name=user,proto3" json:"user,omitempty"` // SFTPGo user JSON serialized +} + +func (x *CheckUserAndTLSCertRequest) Reset() { + *x = CheckUserAndTLSCertRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_auth_proto_auth_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CheckUserAndTLSCertRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckUserAndTLSCertRequest) ProtoMessage() {} + +func (x *CheckUserAndTLSCertRequest) ProtoReflect() protoreflect.Message { + mi := &file_auth_proto_auth_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckUserAndTLSCertRequest.ProtoReflect.Descriptor instead. +func (*CheckUserAndTLSCertRequest) Descriptor() ([]byte, []int) { + return file_auth_proto_auth_proto_rawDescGZIP(), []int{1} +} + +func (x *CheckUserAndTLSCertRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *CheckUserAndTLSCertRequest) GetTlsCert() string { + if x != nil { + return x.TlsCert + } + return "" +} + +func (x *CheckUserAndTLSCertRequest) GetIp() string { + if x != nil { + return x.Ip + } + return "" +} + +func (x *CheckUserAndTLSCertRequest) GetProtocol() string { + if x != nil { + return x.Protocol + } + return "" +} + +func (x *CheckUserAndTLSCertRequest) GetUser() []byte { + if x != nil { + return x.User + } + return nil +} + +type CheckUserAndPublicKeyRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + PubKey string `protobuf:"bytes,2,opt,name=pubKey,proto3" json:"pubKey,omitempty"` + Ip string `protobuf:"bytes,3,opt,name=ip,proto3" json:"ip,omitempty"` + Protocol string `protobuf:"bytes,4,opt,name=protocol,proto3" json:"protocol,omitempty"` + User []byte `protobuf:"bytes,5,opt,name=user,proto3" json:"user,omitempty"` // SFTPGo user JSON serialized +} + +func (x *CheckUserAndPublicKeyRequest) Reset() { + *x = CheckUserAndPublicKeyRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_auth_proto_auth_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CheckUserAndPublicKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckUserAndPublicKeyRequest) ProtoMessage() {} + +func (x *CheckUserAndPublicKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_auth_proto_auth_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckUserAndPublicKeyRequest.ProtoReflect.Descriptor instead. +func (*CheckUserAndPublicKeyRequest) Descriptor() ([]byte, []int) { + return file_auth_proto_auth_proto_rawDescGZIP(), []int{2} +} + +func (x *CheckUserAndPublicKeyRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *CheckUserAndPublicKeyRequest) GetPubKey() string { + if x != nil { + return x.PubKey + } + return "" +} + +func (x *CheckUserAndPublicKeyRequest) GetIp() string { + if x != nil { + return x.Ip + } + return "" +} + +func (x *CheckUserAndPublicKeyRequest) GetProtocol() string { + if x != nil { + return x.Protocol + } + return "" +} + +func (x *CheckUserAndPublicKeyRequest) GetUser() []byte { + if x != nil { + return x.User + } + return nil +} + +type CheckUserAndKeyboardInteractiveRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + Ip string `protobuf:"bytes,2,opt,name=ip,proto3" json:"ip,omitempty"` + Protocol string `protobuf:"bytes,3,opt,name=protocol,proto3" json:"protocol,omitempty"` + User []byte `protobuf:"bytes,4,opt,name=user,proto3" json:"user,omitempty"` // SFTPGo user JSON serialized +} + +func (x *CheckUserAndKeyboardInteractiveRequest) Reset() { + *x = CheckUserAndKeyboardInteractiveRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_auth_proto_auth_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CheckUserAndKeyboardInteractiveRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckUserAndKeyboardInteractiveRequest) ProtoMessage() {} + +func (x *CheckUserAndKeyboardInteractiveRequest) ProtoReflect() protoreflect.Message { + mi := &file_auth_proto_auth_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckUserAndKeyboardInteractiveRequest.ProtoReflect.Descriptor instead. +func (*CheckUserAndKeyboardInteractiveRequest) Descriptor() ([]byte, []int) { + return file_auth_proto_auth_proto_rawDescGZIP(), []int{3} +} + +func (x *CheckUserAndKeyboardInteractiveRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *CheckUserAndKeyboardInteractiveRequest) GetIp() string { + if x != nil { + return x.Ip + } + return "" +} + +func (x *CheckUserAndKeyboardInteractiveRequest) GetProtocol() string { + if x != nil { + return x.Protocol + } + return "" +} + +func (x *CheckUserAndKeyboardInteractiveRequest) GetUser() []byte { + if x != nil { + return x.User + } + return nil +} + +type KeyboardAuthRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RequestID string `protobuf:"bytes,1,opt,name=requestID,proto3" json:"requestID,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` + Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"` + Ip string `protobuf:"bytes,4,opt,name=ip,proto3" json:"ip,omitempty"` + Answers []string `protobuf:"bytes,5,rep,name=answers,proto3" json:"answers,omitempty"` + Questions []string `protobuf:"bytes,6,rep,name=questions,proto3" json:"questions,omitempty"` + Step int32 `protobuf:"varint,7,opt,name=step,proto3" json:"step,omitempty"` +} + +func (x *KeyboardAuthRequest) Reset() { + *x = KeyboardAuthRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_auth_proto_auth_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *KeyboardAuthRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KeyboardAuthRequest) ProtoMessage() {} + +func (x *KeyboardAuthRequest) ProtoReflect() protoreflect.Message { + mi := &file_auth_proto_auth_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use KeyboardAuthRequest.ProtoReflect.Descriptor instead. +func (*KeyboardAuthRequest) Descriptor() ([]byte, []int) { + return file_auth_proto_auth_proto_rawDescGZIP(), []int{4} +} + +func (x *KeyboardAuthRequest) GetRequestID() string { + if x != nil { + return x.RequestID + } + return "" +} + +func (x *KeyboardAuthRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *KeyboardAuthRequest) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *KeyboardAuthRequest) GetIp() string { + if x != nil { + return x.Ip + } + return "" +} + +func (x *KeyboardAuthRequest) GetAnswers() []string { + if x != nil { + return x.Answers + } + return nil +} + +func (x *KeyboardAuthRequest) GetQuestions() []string { + if x != nil { + return x.Questions + } + return nil +} + +func (x *KeyboardAuthRequest) GetStep() int32 { + if x != nil { + return x.Step + } + return 0 +} + +type KeyboardAuthResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Instructions string `protobuf:"bytes,1,opt,name=instructions,proto3" json:"instructions,omitempty"` + Questions []string `protobuf:"bytes,2,rep,name=questions,proto3" json:"questions,omitempty"` + Echos []bool `protobuf:"varint,3,rep,packed,name=echos,proto3" json:"echos,omitempty"` + AuthResult int32 `protobuf:"varint,4,opt,name=auth_result,json=authResult,proto3" json:"auth_result,omitempty"` + CheckPassword int32 `protobuf:"varint,5,opt,name=check_password,json=checkPassword,proto3" json:"check_password,omitempty"` +} + +func (x *KeyboardAuthResponse) Reset() { + *x = KeyboardAuthResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_auth_proto_auth_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *KeyboardAuthResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KeyboardAuthResponse) ProtoMessage() {} + +func (x *KeyboardAuthResponse) ProtoReflect() protoreflect.Message { + mi := &file_auth_proto_auth_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use KeyboardAuthResponse.ProtoReflect.Descriptor instead. +func (*KeyboardAuthResponse) Descriptor() ([]byte, []int) { + return file_auth_proto_auth_proto_rawDescGZIP(), []int{5} +} + +func (x *KeyboardAuthResponse) GetInstructions() string { + if x != nil { + return x.Instructions + } + return "" +} + +func (x *KeyboardAuthResponse) GetQuestions() []string { + if x != nil { + return x.Questions + } + return nil +} + +func (x *KeyboardAuthResponse) GetEchos() []bool { + if x != nil { + return x.Echos + } + return nil +} + +func (x *KeyboardAuthResponse) GetAuthResult() int32 { + if x != nil { + return x.AuthResult + } + return 0 +} + +func (x *KeyboardAuthResponse) GetCheckPassword() int32 { + if x != nil { + return x.CheckPassword + } + return 0 +} + +type AuthResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + User []byte `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` // SFTPGo user JSON serialized +} + +func (x *AuthResponse) Reset() { + *x = AuthResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_auth_proto_auth_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AuthResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthResponse) ProtoMessage() {} + +func (x *AuthResponse) ProtoReflect() protoreflect.Message { + mi := &file_auth_proto_auth_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthResponse.ProtoReflect.Descriptor instead. +func (*AuthResponse) Descriptor() ([]byte, []int) { + return file_auth_proto_auth_proto_rawDescGZIP(), []int{6} +} + +func (x *AuthResponse) GetUser() []byte { + if x != nil { + return x.User + } + return nil +} + +var File_auth_proto_auth_proto protoreflect.FileDescriptor + +var file_auth_proto_auth_proto_rawDesc = []byte{ + 0x0a, 0x15, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x75, 0x74, + 0x68, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x91, + 0x01, 0x0a, 0x17, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x41, 0x6e, 0x64, 0x50, + 0x61, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, + 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, + 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, + 0x69, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, + 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x75, 0x73, + 0x65, 0x72, 0x22, 0x92, 0x01, 0x0a, 0x1a, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, + 0x41, 0x6e, 0x64, 0x54, 0x4c, 0x53, 0x43, 0x65, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, + 0x07, 0x74, 0x6c, 0x73, 0x43, 0x65, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x74, 0x6c, 0x73, 0x43, 0x65, 0x72, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x92, 0x01, 0x0a, 0x1c, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x55, 0x73, 0x65, 0x72, 0x41, 0x6e, 0x64, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, + 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12, 0x1a, 0x0a, 0x08, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x84, 0x01, 0x0a, + 0x26, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x41, 0x6e, 0x64, 0x4b, 0x65, 0x79, + 0x62, 0x6f, 0x61, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x69, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, + 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x75, + 0x73, 0x65, 0x72, 0x22, 0xc7, 0x01, 0x0a, 0x13, 0x4b, 0x65, 0x79, 0x62, 0x6f, 0x61, 0x72, 0x64, + 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x72, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x44, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, + 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, + 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, + 0x70, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x07, 0x61, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x74, 0x65, + 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x73, 0x74, 0x65, 0x70, 0x22, 0xb6, 0x01, + 0x0a, 0x14, 0x4b, 0x65, 0x79, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x69, 0x6e, 0x73, 0x74, 0x72, 0x75, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x69, 0x6e, + 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x63, 0x68, 0x6f, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x08, 0x52, 0x05, 0x65, 0x63, 0x68, 0x6f, 0x73, 0x12, 0x1f, + 0x0a, 0x0b, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, + 0x25, 0x0a, 0x0e, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x61, + 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x22, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x32, 0xac, 0x03, 0x0a, 0x04, 0x41, + 0x75, 0x74, 0x68, 0x12, 0x47, 0x0a, 0x10, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, + 0x41, 0x6e, 0x64, 0x50, 0x61, 0x73, 0x73, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x43, 0x68, 0x65, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x41, 0x6e, 0x64, 0x50, 0x61, 0x73, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x13, + 0x43, 0x68, 0x65, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x41, 0x6e, 0x64, 0x54, 0x4c, 0x53, 0x43, + 0x65, 0x72, 0x74, 0x12, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x55, 0x73, 0x65, 0x72, 0x41, 0x6e, 0x64, 0x54, 0x4c, 0x53, 0x43, 0x65, 0x72, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, + 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x15, 0x43, + 0x68, 0x65, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x41, 0x6e, 0x64, 0x50, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x4b, 0x65, 0x79, 0x12, 0x23, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x41, 0x6e, 0x64, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, + 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x65, + 0x0a, 0x1f, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x41, 0x6e, 0x64, 0x4b, 0x65, + 0x79, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x61, 0x63, 0x74, 0x69, 0x76, + 0x65, 0x12, 0x2d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x55, + 0x73, 0x65, 0x72, 0x41, 0x6e, 0x64, 0x4b, 0x65, 0x79, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x49, 0x6e, + 0x74, 0x65, 0x72, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, 0x17, 0x53, 0x65, 0x6e, 0x64, 0x4b, 0x65, 0x79, + 0x62, 0x6f, 0x61, 0x72, 0x64, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4b, 0x65, 0x79, 0x62, 0x6f, 0x61, 0x72, + 0x64, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4b, 0x65, 0x79, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x41, 0x75, 0x74, + 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x17, 0x5a, 0x15, 0x73, 0x64, 0x6b, + 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_auth_proto_auth_proto_rawDescOnce sync.Once + file_auth_proto_auth_proto_rawDescData = file_auth_proto_auth_proto_rawDesc +) + +func file_auth_proto_auth_proto_rawDescGZIP() []byte { + file_auth_proto_auth_proto_rawDescOnce.Do(func() { + file_auth_proto_auth_proto_rawDescData = protoimpl.X.CompressGZIP(file_auth_proto_auth_proto_rawDescData) + }) + return file_auth_proto_auth_proto_rawDescData +} + +var file_auth_proto_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_auth_proto_auth_proto_goTypes = []interface{}{ + (*CheckUserAndPassRequest)(nil), // 0: proto.CheckUserAndPassRequest + (*CheckUserAndTLSCertRequest)(nil), // 1: proto.CheckUserAndTLSCertRequest + (*CheckUserAndPublicKeyRequest)(nil), // 2: proto.CheckUserAndPublicKeyRequest + (*CheckUserAndKeyboardInteractiveRequest)(nil), // 3: proto.CheckUserAndKeyboardInteractiveRequest + (*KeyboardAuthRequest)(nil), // 4: proto.KeyboardAuthRequest + (*KeyboardAuthResponse)(nil), // 5: proto.KeyboardAuthResponse + (*AuthResponse)(nil), // 6: proto.AuthResponse +} +var file_auth_proto_auth_proto_depIdxs = []int32{ + 0, // 0: proto.Auth.CheckUserAndPass:input_type -> proto.CheckUserAndPassRequest + 1, // 1: proto.Auth.CheckUserAndTLSCert:input_type -> proto.CheckUserAndTLSCertRequest + 2, // 2: proto.Auth.CheckUserAndPublicKey:input_type -> proto.CheckUserAndPublicKeyRequest + 3, // 3: proto.Auth.CheckUserAndKeyboardInteractive:input_type -> proto.CheckUserAndKeyboardInteractiveRequest + 4, // 4: proto.Auth.SendKeyboardAuthRequest:input_type -> proto.KeyboardAuthRequest + 6, // 5: proto.Auth.CheckUserAndPass:output_type -> proto.AuthResponse + 6, // 6: proto.Auth.CheckUserAndTLSCert:output_type -> proto.AuthResponse + 6, // 7: proto.Auth.CheckUserAndPublicKey:output_type -> proto.AuthResponse + 6, // 8: proto.Auth.CheckUserAndKeyboardInteractive:output_type -> proto.AuthResponse + 5, // 9: proto.Auth.SendKeyboardAuthRequest:output_type -> proto.KeyboardAuthResponse + 5, // [5:10] is the sub-list for method output_type + 0, // [0:5] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_auth_proto_auth_proto_init() } +func file_auth_proto_auth_proto_init() { + if File_auth_proto_auth_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_auth_proto_auth_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CheckUserAndPassRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_auth_proto_auth_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CheckUserAndTLSCertRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_auth_proto_auth_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CheckUserAndPublicKeyRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_auth_proto_auth_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CheckUserAndKeyboardInteractiveRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_auth_proto_auth_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*KeyboardAuthRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_auth_proto_auth_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*KeyboardAuthResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_auth_proto_auth_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AuthResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_auth_proto_auth_proto_rawDesc, + NumEnums: 0, + NumMessages: 7, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_auth_proto_auth_proto_goTypes, + DependencyIndexes: file_auth_proto_auth_proto_depIdxs, + MessageInfos: file_auth_proto_auth_proto_msgTypes, + }.Build() + File_auth_proto_auth_proto = out.File + file_auth_proto_auth_proto_rawDesc = nil + file_auth_proto_auth_proto_goTypes = nil + file_auth_proto_auth_proto_depIdxs = nil +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConnInterface + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion6 + +// AuthClient is the client API for Auth service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type AuthClient interface { + CheckUserAndPass(ctx context.Context, in *CheckUserAndPassRequest, opts ...grpc.CallOption) (*AuthResponse, error) + CheckUserAndTLSCert(ctx context.Context, in *CheckUserAndTLSCertRequest, opts ...grpc.CallOption) (*AuthResponse, error) + CheckUserAndPublicKey(ctx context.Context, in *CheckUserAndPublicKeyRequest, opts ...grpc.CallOption) (*AuthResponse, error) + CheckUserAndKeyboardInteractive(ctx context.Context, in *CheckUserAndKeyboardInteractiveRequest, opts ...grpc.CallOption) (*AuthResponse, error) + SendKeyboardAuthRequest(ctx context.Context, in *KeyboardAuthRequest, opts ...grpc.CallOption) (*KeyboardAuthResponse, error) +} + +type authClient struct { + cc grpc.ClientConnInterface +} + +func NewAuthClient(cc grpc.ClientConnInterface) AuthClient { + return &authClient{cc} +} + +func (c *authClient) CheckUserAndPass(ctx context.Context, in *CheckUserAndPassRequest, opts ...grpc.CallOption) (*AuthResponse, error) { + out := new(AuthResponse) + err := c.cc.Invoke(ctx, "/proto.Auth/CheckUserAndPass", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authClient) CheckUserAndTLSCert(ctx context.Context, in *CheckUserAndTLSCertRequest, opts ...grpc.CallOption) (*AuthResponse, error) { + out := new(AuthResponse) + err := c.cc.Invoke(ctx, "/proto.Auth/CheckUserAndTLSCert", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authClient) CheckUserAndPublicKey(ctx context.Context, in *CheckUserAndPublicKeyRequest, opts ...grpc.CallOption) (*AuthResponse, error) { + out := new(AuthResponse) + err := c.cc.Invoke(ctx, "/proto.Auth/CheckUserAndPublicKey", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authClient) CheckUserAndKeyboardInteractive(ctx context.Context, in *CheckUserAndKeyboardInteractiveRequest, opts ...grpc.CallOption) (*AuthResponse, error) { + out := new(AuthResponse) + err := c.cc.Invoke(ctx, "/proto.Auth/CheckUserAndKeyboardInteractive", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authClient) SendKeyboardAuthRequest(ctx context.Context, in *KeyboardAuthRequest, opts ...grpc.CallOption) (*KeyboardAuthResponse, error) { + out := new(KeyboardAuthResponse) + err := c.cc.Invoke(ctx, "/proto.Auth/SendKeyboardAuthRequest", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AuthServer is the server API for Auth service. +type AuthServer interface { + CheckUserAndPass(context.Context, *CheckUserAndPassRequest) (*AuthResponse, error) + CheckUserAndTLSCert(context.Context, *CheckUserAndTLSCertRequest) (*AuthResponse, error) + CheckUserAndPublicKey(context.Context, *CheckUserAndPublicKeyRequest) (*AuthResponse, error) + CheckUserAndKeyboardInteractive(context.Context, *CheckUserAndKeyboardInteractiveRequest) (*AuthResponse, error) + SendKeyboardAuthRequest(context.Context, *KeyboardAuthRequest) (*KeyboardAuthResponse, error) +} + +// UnimplementedAuthServer can be embedded to have forward compatible implementations. +type UnimplementedAuthServer struct { +} + +func (*UnimplementedAuthServer) CheckUserAndPass(context.Context, *CheckUserAndPassRequest) (*AuthResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CheckUserAndPass not implemented") +} +func (*UnimplementedAuthServer) CheckUserAndTLSCert(context.Context, *CheckUserAndTLSCertRequest) (*AuthResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CheckUserAndTLSCert not implemented") +} +func (*UnimplementedAuthServer) CheckUserAndPublicKey(context.Context, *CheckUserAndPublicKeyRequest) (*AuthResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CheckUserAndPublicKey not implemented") +} +func (*UnimplementedAuthServer) CheckUserAndKeyboardInteractive(context.Context, *CheckUserAndKeyboardInteractiveRequest) (*AuthResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CheckUserAndKeyboardInteractive not implemented") +} +func (*UnimplementedAuthServer) SendKeyboardAuthRequest(context.Context, *KeyboardAuthRequest) (*KeyboardAuthResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SendKeyboardAuthRequest not implemented") +} + +func RegisterAuthServer(s *grpc.Server, srv AuthServer) { + s.RegisterService(&_Auth_serviceDesc, srv) +} + +func _Auth_CheckUserAndPass_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CheckUserAndPassRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServer).CheckUserAndPass(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.Auth/CheckUserAndPass", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServer).CheckUserAndPass(ctx, req.(*CheckUserAndPassRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Auth_CheckUserAndTLSCert_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CheckUserAndTLSCertRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServer).CheckUserAndTLSCert(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.Auth/CheckUserAndTLSCert", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServer).CheckUserAndTLSCert(ctx, req.(*CheckUserAndTLSCertRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Auth_CheckUserAndPublicKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CheckUserAndPublicKeyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServer).CheckUserAndPublicKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.Auth/CheckUserAndPublicKey", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServer).CheckUserAndPublicKey(ctx, req.(*CheckUserAndPublicKeyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Auth_CheckUserAndKeyboardInteractive_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CheckUserAndKeyboardInteractiveRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServer).CheckUserAndKeyboardInteractive(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.Auth/CheckUserAndKeyboardInteractive", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServer).CheckUserAndKeyboardInteractive(ctx, req.(*CheckUserAndKeyboardInteractiveRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Auth_SendKeyboardAuthRequest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(KeyboardAuthRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServer).SendKeyboardAuthRequest(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.Auth/SendKeyboardAuthRequest", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServer).SendKeyboardAuthRequest(ctx, req.(*KeyboardAuthRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _Auth_serviceDesc = grpc.ServiceDesc{ + ServiceName: "proto.Auth", + HandlerType: (*AuthServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CheckUserAndPass", + Handler: _Auth_CheckUserAndPass_Handler, + }, + { + MethodName: "CheckUserAndTLSCert", + Handler: _Auth_CheckUserAndTLSCert_Handler, + }, + { + MethodName: "CheckUserAndPublicKey", + Handler: _Auth_CheckUserAndPublicKey_Handler, + }, + { + MethodName: "CheckUserAndKeyboardInteractive", + Handler: _Auth_CheckUserAndKeyboardInteractive_Handler, + }, + { + MethodName: "SendKeyboardAuthRequest", + Handler: _Auth_SendKeyboardAuthRequest_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "auth/proto/auth.proto", +} diff --git a/sdk/plugin/auth/proto/auth.proto b/sdk/plugin/auth/proto/auth.proto new file mode 100644 index 00000000..705ba8db --- /dev/null +++ b/sdk/plugin/auth/proto/auth.proto @@ -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); +} \ No newline at end of file diff --git a/sdk/plugin/kms.go b/sdk/plugin/kms.go index 5f8580f7..0b8319a5 100644 --- a/sdk/plugin/kms.go +++ b/sdk/plugin/kms.go @@ -27,7 +27,7 @@ type KMSConfig struct { EncryptedStatus string `json:"encrypted_status" mapstructure:"encrypted_status"` } -func (c *KMSConfig) isValid() error { +func (c *KMSConfig) validate() error { if !util.IsStringInSlice(c.Scheme, validKMSSchemes) { return fmt.Errorf("invalid kms scheme: %v", c.Scheme) } @@ -57,7 +57,7 @@ func newKMSPlugin(config Config) (*kmsPlugin, error) { func (p *kmsPlugin) initialize() error { killProcess(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) } var secureConfig *plugin.SecureConfig diff --git a/sdk/plugin/mkproto.sh b/sdk/plugin/mkproto.sh index 9e20ec63..bf485055 100755 --- a/sdk/plugin/mkproto.sh +++ b/sdk/plugin/mkproto.sh @@ -2,5 +2,6 @@ protoc notifier/proto/notifier.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=../../.. diff --git a/sdk/plugin/notifier/notifier.go b/sdk/plugin/notifier/notifier.go index 2b753763..380d2104 100644 --- a/sdk/plugin/notifier/notifier.go +++ b/sdk/plugin/notifier/notifier.go @@ -1,6 +1,7 @@ // Package notifier defines the implementation for event notifier plugins. -// Notifier plugins allow to receive filesystem events such as file uploads, -// downloads etc. and user events such as add, update, delete. +// Notifier plugins allow to receive notifications for supported filesystem +// events such as file uploads, downloads etc. and user events such as add, +// update, delete. package notifier import ( diff --git a/sdk/plugin/notifier/proto/notifier.pb.go b/sdk/plugin/notifier/proto/notifier.pb.go index 3500a542..1fd5cae2 100644 --- a/sdk/plugin/notifier/proto/notifier.pb.go +++ b/sdk/plugin/notifier/proto/notifier.pb.go @@ -144,7 +144,7 @@ type UserEvent struct { 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"` - 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() { diff --git a/sdk/plugin/notifier/proto/notifier.proto b/sdk/plugin/notifier/proto/notifier.proto index d42f5a13..084860a4 100644 --- a/sdk/plugin/notifier/proto/notifier.proto +++ b/sdk/plugin/notifier/proto/notifier.proto @@ -21,7 +21,7 @@ message FsEvent { message UserEvent { google.protobuf.Timestamp timestamp = 1; string action = 2; - bytes user = 3; // SFTPGo user json serialized + bytes user = 3; // SFTPGo user JSON serialized } service Notifier { diff --git a/sdk/plugin/plugin.go b/sdk/plugin/plugin.go index f01d4a8b..7ec443ce 100644 --- a/sdk/plugin/plugin.go +++ b/sdk/plugin/plugin.go @@ -2,6 +2,8 @@ package plugin import ( + "crypto/x509" + "errors" "fmt" "sync" "sync/atomic" @@ -11,8 +13,10 @@ import ( "github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/logger" + "github.com/drakkan/sftpgo/v2/sdk/plugin/auth" kmsplugin "github.com/drakkan/sftpgo/v2/sdk/plugin/kms" "github.com/drakkan/sftpgo/v2/sdk/plugin/notifier" + "github.com/drakkan/sftpgo/v2/util" ) const ( @@ -38,6 +42,8 @@ type Config struct { NotifierOptions NotifierConfig `json:"notifier_options" mapstructure:"notifier_options"` // KMSOptions defines options for a KMS plugin 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 Cmd string `json:"cmd" mapstructure:"cmd"` // Args to pass to the plugin executable @@ -69,19 +75,23 @@ type Manager struct { closed int32 done chan bool // List of configured plugins - Configs []Config `json:"plugins" mapstructure:"plugins"` - notifLock sync.RWMutex - notifiers []*notifierPlugin - kmsLock sync.RWMutex - kms []*kmsPlugin + Configs []Config `json:"plugins" mapstructure:"plugins"` + notifLock sync.RWMutex + notifiers []*notifierPlugin + kmsLock sync.RWMutex + kms []*kmsPlugin + authLock sync.RWMutex + auths []*authPlugin + authScopes int } // Initialize initializes the configured plugins func Initialize(configs []Config, logVerbose bool) error { Handler = Manager{ - Configs: configs, - done: make(chan bool), - closed: 0, + Configs: configs, + done: make(chan bool), + closed: 0, + authScopes: -1, } if len(configs) == 0 { return nil @@ -117,6 +127,17 @@ func Initialize(configs []Config, logVerbose bool) error { Handler.Configs[idx].newKMSPluginSecretProvider) logger.Debug(logSender, "", "registered secret provider for scheme: %v, encrypted status: %v", 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: 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) } +// 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() { m.notifLock.RLock() for idx, n := range m.notifiers { @@ -203,6 +351,16 @@ func (m *Manager) checkCrashedPlugins() { } } 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) { @@ -239,6 +397,22 @@ func (m *Manager) restartKMSPlugin(config Config, idx int) { 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 func (m *Manager) Cleanup() { atomic.StoreInt32(&m.closed, 1) @@ -256,6 +430,13 @@ func (m *Manager) Cleanup() { k.cleanup() } 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() { diff --git a/sftpd/server.go b/sftpd/server.go index e3f7a0c4..c9d7f3f1 100644 --- a/sftpd/server.go +++ b/sftpd/server.go @@ -21,6 +21,7 @@ import ( "github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/metric" + "github.com/drakkan/sftpgo/v2/sdk/plugin" "github.com/drakkan/sftpgo/v2/util" "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) { - if c.KeyboardInteractiveHook == "" { + if c.KeyboardInteractiveHook == "" && !plugin.Handler.HasAuthScope(plugin.AuthScopeKeyboardInteractive) { return } - if !strings.HasPrefix(c.KeyboardInteractiveHook, "http") { - if !filepath.IsAbs(c.KeyboardInteractiveHook) { - logger.WarnToConsole("invalid keyboard interactive authentication program: %#v must be an absolute path", - c.KeyboardInteractiveHook) - logger.Warn(logSender, "", "invalid keyboard interactive authentication program: %#v must be an absolute path", - c.KeyboardInteractiveHook) - return - } - _, err := os.Stat(c.KeyboardInteractiveHook) - if err != nil { - logger.WarnToConsole("invalid keyboard interactive authentication program:: %v", err) - logger.Warn(logSender, "", "invalid keyboard interactive authentication program:: %v", err) - return + if c.KeyboardInteractiveHook != "" { + if !strings.HasPrefix(c.KeyboardInteractiveHook, "http") { + if !filepath.IsAbs(c.KeyboardInteractiveHook) { + logger.WarnToConsole("invalid keyboard interactive authentication program: %#v must be an absolute path", + c.KeyboardInteractiveHook) + logger.Warn(logSender, "", "invalid keyboard interactive authentication program: %#v must be an absolute path", + c.KeyboardInteractiveHook) + return + } + _, err := os.Stat(c.KeyboardInteractiveHook) + if err != nil { + logger.WarnToConsole("invalid keyboard interactive authentication program:: %v", err) + logger.Warn(logSender, "", "invalid keyboard interactive authentication program:: %v", err) + return + } } } serverConfig.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {