From 5f49af1780aa65f5660a3a51c2d5399ecde81547 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Fri, 26 Mar 2021 15:19:01 +0100 Subject: [PATCH] external auth: allow to inspect and preserve an existing user --- .github/workflows/development.yml | 2 +- dataprovider/bolt.go | 4 +- dataprovider/dataprovider.go | 87 +++++++++++++++++++---------- docs/external-auth.md | 18 +++++- go.mod | 14 ++--- go.sum | 29 +++++----- sftpd/sftpd_test.go | 92 ++++++++++++++++++++++++++++--- utils/utils.go | 27 +++++++++ 8 files changed, 209 insertions(+), 64 deletions(-) diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml index 46f466c7..b077be51 100644 --- a/.github/workflows/development.yml +++ b/.github/workflows/development.yml @@ -265,7 +265,7 @@ jobs: - name: Run tests using CockroachDB provider run: | - docker run --name crdb --health-cmd "curl -I http://127.0.0.1:8080" --health-interval 10s --health-timeout 5s --health-retries 6 -p 26257:26257 -d cockroachdb/cockroach:latest start-single-node --insecure --listen-addr 0.0.0.0:26257 + docker run --rm --name crdb --health-cmd "curl -I http://127.0.0.1:8080" --health-interval 10s --health-timeout 5s --health-retries 6 -p 26257:26257 -d cockroachdb/cockroach:latest start-single-node --insecure --listen-addr 0.0.0.0:26257 docker exec crdb cockroach sql --insecure -e 'create database "sftpgo"' go test -v -p 1 -timeout 10m ./... -covermode=atomic docker stop crdb diff --git a/dataprovider/bolt.go b/dataprovider/bolt.go index f84fb1c4..f09030d3 100644 --- a/dataprovider/bolt.go +++ b/dataprovider/bolt.go @@ -397,7 +397,7 @@ func (p *BoltProvider) userExists(username string) (User, error) { } u := bucket.Get([]byte(username)) if u == nil { - return &RecordNotFoundError{err: fmt.Sprintf("username %v does not exist", username)} + return &RecordNotFoundError{err: fmt.Sprintf("username %#v does not exist", username)} } folderBucket, err := getFolderBucket(tx) if err != nil { @@ -465,7 +465,7 @@ func (p *BoltProvider) updateUser(user *User) error { } var u []byte if u = bucket.Get([]byte(user.Username)); u == nil { - return &RecordNotFoundError{err: fmt.Sprintf("username %v does not exist", user.Username)} + return &RecordNotFoundError{err: fmt.Sprintf("username %#v does not exist", user.Username)} } var oldUser User err = json.Unmarshal(u, &oldUser) diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index 5097fb94..500622b7 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -2153,17 +2153,7 @@ func getPreLoginHookResponse(loginMethod, ip, protocol string, userAsJSON []byte } func executePreLoginHook(username, loginMethod, ip, protocol string) (User, error) { - u, err := provider.userExists(username) - if err != nil { - if _, ok := err.(*RecordNotFoundError); !ok { - return u, err - } - u = User{ - ID: 0, - Username: username, - } - } - userAsJSON, err := json.Marshal(u) + u, userAsJSON, err := getUserAndJSONForHook(username) if err != nil { return u, err } @@ -2173,11 +2163,11 @@ func executePreLoginHook(username, loginMethod, ip, protocol string) (User, erro return u, fmt.Errorf("pre-login hook error: %v, elapsed %v", err, time.Since(startTime)) } providerLog(logger.LevelDebug, "pre-login hook completed, elapsed: %v", time.Since(startTime)) - if strings.TrimSpace(string(out)) == "" { + if utils.IsByteArrayEmpty(out) { providerLog(logger.LevelDebug, "empty response from pre-login hook, no modification requested for user %#v id: %v", username, u.ID) if u.ID == 0 { - return u, &RecordNotFoundError{err: fmt.Sprintf("username %v does not exist", username)} + return u, &RecordNotFoundError{err: fmt.Sprintf("username %#v does not exist", username)} } return u, nil } @@ -2276,7 +2266,15 @@ func ExecutePostLoginHook(user *User, loginMethod, ip, protocol string, err erro }() } -func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, protocol, tlsCert string) ([]byte, error) { +func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, protocol string, cert *x509.Certificate, userAsJSON []byte) ([]byte, error) { + var tlsCert string + if cert != nil { + var err error + tlsCert, err = utils.EncodeTLSCertToPem(cert) + if err != nil { + return nil, err + } + } if strings.HasPrefix(config.ExternalAuthHook, "http") { var url *url.URL var result []byte @@ -2294,6 +2292,9 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, authRequest["protocol"] = protocol authRequest["keyboard_interactive"] = keyboardInteractive authRequest["tls_cert"] = tlsCert + if len(userAsJSON) > 0 { + authRequest["user"] = string(userAsJSON) + } authRequestAsJSON, err := json.Marshal(authRequest) if err != nil { providerLog(logger.LevelWarn, "error serializing external auth request: %v", err) @@ -2317,6 +2318,7 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, cmd := exec.CommandContext(ctx, config.ExternalAuthHook) cmd.Env = append(os.Environ(), fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%v", username), + fmt.Sprintf("SFTPGO_AUTHD_USER=%v", string(userAsJSON)), fmt.Sprintf("SFTPGO_AUTHD_IP=%v", ip), fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%v", password), fmt.Sprintf("SFTPGO_AUTHD_PUBLIC_KEY=%v", pkey), @@ -2328,27 +2330,30 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, func doExternalAuth(username, password string, pubKey []byte, keyboardInteractive, ip, protocol string, tlsCert *x509.Certificate) (User, error) { var user User - var pkey, cert string - if len(pubKey) > 0 { - k, err := ssh.ParsePublicKey(pubKey) - if err != nil { - return user, err - } - pkey = string(ssh.MarshalAuthorizedKey(k)) + + pkey, err := utils.GetSSHPublicKeyAsString(pubKey) + if err != nil { + return user, err } - if tlsCert != nil { - var err error - cert, err = utils.EncodeTLSCertToPem(tlsCert) - if err != nil { - return user, err - } + u, userAsJSON, err := getUserAndJSONForHook(username) + if err != nil { + return user, err } + startTime := time.Now() - out, err := getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, protocol, cert) + out, err := getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, protocol, tlsCert, userAsJSON) if err != nil { return user, fmt.Errorf("external auth error: %v, elapsed: %v", err, time.Since(startTime)) } providerLog(logger.LevelDebug, "external auth completed, elapsed: %v", time.Since(startTime)) + if utils.IsByteArrayEmpty(out) { + providerLog(logger.LevelDebug, "empty response from external hook, no modification requested for user %#v id: %v", + username, u.ID) + if u.ID == 0 { + return u, &RecordNotFoundError{err: fmt.Sprintf("username %#v does not exist", username)} + } + return u, nil + } err = json.Unmarshal(out, &user) if err != nil { return user, fmt.Errorf("invalid external auth response: %v", err) @@ -2366,8 +2371,10 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv // for example an SFTP user logins using "user1" or "user2" and the external auth // returns "user" in both cases, so we use the username returned from // external auth and not the one used to login - u, err := provider.userExists(user.Username) - if err == nil { + if user.Username != username { + u, err = provider.userExists(user.Username) + } + if u.ID > 0 && err == nil { user.ID = u.ID user.UsedQuotaSize = u.UsedQuotaSize user.UsedQuotaFiles = u.UsedQuotaFiles @@ -2383,6 +2390,26 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv return provider.userExists(user.Username) } +func getUserAndJSONForHook(username string) (User, []byte, error) { + var userAsJSON []byte + u, err := provider.userExists(username) + if err != nil { + if _, ok := err.(*RecordNotFoundError); !ok { + return u, userAsJSON, err + } + u = User{ + ID: 0, + Username: username, + } + return u, userAsJSON, nil + } + userAsJSON, err = json.Marshal(u) + if err != nil { + return u, userAsJSON, err + } + return u, userAsJSON, err +} + func providerLog(level logger.LogLevel, format string, v ...interface{}) { logger.Log(level, logSender, "", format, v...) } diff --git a/docs/external-auth.md b/docs/external-auth.md index d71b8496..99be473b 100644 --- a/docs/external-auth.md +++ b/docs/external-auth.md @@ -5,6 +5,7 @@ To enable external authentication, you must set the absolute path of your authen The external program can read the following environment variables to get info about the user trying to authenticate: - `SFTPGO_AUTHD_USERNAME` +- `SFTPGO_AUTHD_USER`, STPGo user serialized as JSON, empty if the user does not exist within the data provider - `SFTPGO_AUTHD_IP` - `SFTPGO_AUTHD_PROTOCOL`, possible values are `SSH`, `FTP`, `DAV` - `SFTPGO_AUTHD_PASSWORD`, not empty for password authentication @@ -13,21 +14,32 @@ The external program can read the following environment variables to get info ab - `SFTPGO_AUTHD_TLS_CERT`, TLS client certificate PEM encoded. Not empty for TLS certificate authentication Previous global environment variables aren't cleared when the script is called. The content of these variables is _not_ quoted. They may contain special characters. They are under the control of a possibly malicious remote user. -The program must write, on its standard output, a valid SFTPGo user serialized as JSON if the authentication succeeds or a user with an empty username if the authentication fails. +The program can inspect the SFTPGo user, if it exists, using the `SFTPGO_AUTHD_USER` environment variable. +The program must write, on its standard output: + +- a valid SFTPGo user serialized as JSON if the authentication succeeds. The user will be added/updated within the defined data provider +- an empty string, or no response at all, if authentication succeeds and the existing SFTPGo user does not need to be updated +- a user with an empty username if the authentication fails If the hook is an HTTP URL then it will be invoked as HTTP POST. The request body will contain a JSON serialized struct with the following fields: - `username` - `ip` +- `user`, STPGo user serialized as JSON, omitted if the user does not exist within the data provider - `protocol`, possible values are `SSH`, `FTP`, `DAV` - `password`, not empty for password authentication - `public_key`, not empty for public key authentication - `keyboard_interactive`, not empty for keyboard interactive authentication - `tls_cert`, TLS client certificate PEM encoded. Not empty for TLS certificate authentication -If authentication succeeds the HTTP response code must be 200 and the response body a valid SFTPGo user serialized as JSON. If the authentication fails the HTTP response code must be != 200 or the response body must be empty. +If authentication succeeds the HTTP response code must be 200 and the response body can be: -If the authentication succeeds, the user will be automatically added/updated inside the defined data provider. Actions defined for users added/updated will not be executed in this case and an already logged in user with the same username will not be disconnected, you have to handle these things yourself. +- a valid SFTPGo user serialized as JSON. The user will be added/updated within the defined data provider +- empty, the existing SFTPGo user does not need to be updated + +If the authentication fails the HTTP response code must be != 200. + +Actions defined for users added/updated will not be executed in this case and an already logged in user with the same username will not be disconnected. The program hook must finish within 30 seconds, the HTTP hook timeout will use the global configuration for HTTP clients. diff --git a/go.mod b/go.mod index 97f4aeef..d2b6c94d 100644 --- a/go.mod +++ b/go.mod @@ -8,14 +8,14 @@ require ( github.com/Azure/azure-storage-blob-go v0.13.0 github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect - github.com/alexedwards/argon2id v0.0.0-20201228115903-cf543ebc1f7b - github.com/aws/aws-sdk-go v1.38.3 + github.com/alexedwards/argon2id v0.0.0-20210326052512-e2135f7c9c77 + github.com/aws/aws-sdk-go v1.38.6 github.com/cockroachdb/cockroach-go/v2 v2.1.0 github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect github.com/eikenb/pipeat v0.0.0-20200430215831-470df5986b6d github.com/fclairamb/ftpserverlib v0.13.0 github.com/frankban/quicktest v1.11.3 // indirect - github.com/go-chi/chi/v5 v5.0.1 + github.com/go-chi/chi/v5 v5.0.2 github.com/go-chi/jwtauth/v5 v5.0.0 github.com/go-chi/render v1.0.1 github.com/go-ole/go-ole v1.2.5 // indirect @@ -30,11 +30,11 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.6.8 github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126 - github.com/klauspost/cpuid/v2 v2.0.5 // indirect + github.com/klauspost/cpuid/v2 v2.0.6 // indirect github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect github.com/lestrrat-go/jwx v1.1.5 github.com/lib/pq v1.10.0 - github.com/magiconair/properties v1.8.4 // indirect + github.com/magiconair/properties v1.8.5 // indirect github.com/mattn/go-sqlite3 v1.14.6 github.com/miekg/dns v1.1.41 // indirect github.com/minio/sha256-simd v1.0.0 @@ -48,7 +48,7 @@ require ( github.com/prometheus/common v0.20.0 // indirect github.com/rs/cors v1.7.1-0.20200626170627-8b4a00bd362b github.com/rs/xid v1.2.1 - github.com/rs/zerolog v1.20.0 + github.com/rs/zerolog v1.21.0 github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shirou/gopsutil/v3 v3.21.2 github.com/spf13/afero v1.6.0 @@ -69,7 +69,7 @@ require ( golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 // indirect golang.org/x/sys v0.0.0-20210324051608-47abb6519492 golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect - google.golang.org/api v0.42.0 + google.golang.org/api v0.43.0 gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect diff --git a/go.sum b/go.sum index fb09826c..4b1ad51a 100644 --- a/go.sum +++ b/go.sum @@ -105,8 +105,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/alexedwards/argon2id v0.0.0-20201228115903-cf543ebc1f7b h1:jEg+fE+POnmUy40B+aSKEPqZDmsdl55hZU0YKXEzz1k= -github.com/alexedwards/argon2id v0.0.0-20201228115903-cf543ebc1f7b/go.mod h1:Kmn5t2Rb93Q4NTprN4+CCgARGvigKMJyxP0WckpTUp0= +github.com/alexedwards/argon2id v0.0.0-20210326052512-e2135f7c9c77 h1:X6U+/fhTYeDYS3sN4xHcoORJhhar+zSgrNeraapuRK4= +github.com/alexedwards/argon2id v0.0.0-20210326052512-e2135f7c9c77/go.mod h1:Kmn5t2Rb93Q4NTprN4+CCgARGvigKMJyxP0WckpTUp0= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -118,8 +118,8 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.36.1/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= -github.com/aws/aws-sdk-go v1.38.3 h1:QCL/le04oAz2jELMRSuJVjGT7H+4hhoQc66eMPCfU/k= -github.com/aws/aws-sdk-go v1.38.3/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go v1.38.6 h1:h0AKIaz/A1kEJ50HxCv7tL1GW+KbxYbp75+lZ/nvFOI= +github.com/aws/aws-sdk-go v1.38.6/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -217,8 +217,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= -github.com/go-chi/chi/v5 v5.0.1 h1:ALxjCrTf1aflOlkhMnCUP86MubbWFrzB3gkRPReLpTo= -github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.0.2 h1:4xKeALZdMEsuI5s05PU2Bm89Uc5iM04qFubUCl5LfAQ= +github.com/go-chi/chi/v5 v5.0.2/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/jwtauth/v5 v5.0.0 h1:FjyoBQ0sH6/OSBCTXdRMMd7Eis3UjmsX2wKTSucFn+g= github.com/go-chi/jwtauth/v5 v5.0.0/go.mod h1:wdYCsXCBuihmcGwLdfVgZ4LhDLOZHfyF+Fd5mEjiGPM= github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= @@ -497,8 +497,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.5 h1:qnfhwbFriwDIX51QncuNU5mEMf+6KE3t7O8V2KQl3Dg= -github.com/klauspost/cpuid/v2 v2.0.5/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.6 h1:dQ5ueTiftKxp0gyjKSx5+8BtPWkyQbd95m8Gys/RarI= +github.com/klauspost/cpuid/v2 v2.0.6/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -542,8 +542,8 @@ github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-b github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY= -github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -692,8 +692,8 @@ github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= -github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs= -github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= +github.com/rs/zerolog v1.21.0 h1:Q3vdXlfLNT+OftyBHsU0Y445MD+8m8axjKgf2si0QcM= +github.com/rs/zerolog v1.21.0/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -974,7 +974,6 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1054,8 +1053,9 @@ google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.42.0 h1:uqATLkpxiBrhrvFoebXUjvyzE9nQf+pVyy0Z0IHE+fc= google.golang.org/api v0.42.0/go.mod h1:+Oj4s6ch2SEGtPjGqfUfZonBH0GjQH89gTeKKAEGZKI= +google.golang.org/api v0.43.0 h1:4sAyIHT6ZohtAQDoxws+ez7bROYmUlOVvsUscYCDTqA= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1113,6 +1113,7 @@ google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210312152112-fc591d9ea70f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210323160006-e668133fea6a h1:XVaQ1+BDKvrRcgppHhtAaniHCKyV5xJAvymwsPHHFaE= google.golang.org/genproto v0.0.0-20210323160006-e668133fea6a/go.mod h1:f2Bd7+2PlaVKmvKQ52aspJZXIDaRQBVdOOBfJ5i8OEs= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= diff --git a/sftpd/sftpd_test.go b/sftpd/sftpd_test.go index 1c379371..c3a7c28d 100644 --- a/sftpd/sftpd_test.go +++ b/sftpd/sftpd_test.go @@ -2175,7 +2175,7 @@ func TestLoginExternalAuthPwdAndPubKey(t *testing.T) { err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf := config.GetProviderConf() - err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, ""), os.ModePerm) + err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, ""), os.ModePerm) assert.NoError(t, err) providerConf.ExternalAuthHook = extAuthPath providerConf.ExternalAuthScope = 0 @@ -2202,7 +2202,7 @@ func TestLoginExternalAuthPwdAndPubKey(t *testing.T) { usePubKey = false u = getTestUser(usePubKey) u.PublicKeys = []string{} - err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, ""), os.ModePerm) + err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, ""), os.ModePerm) assert.NoError(t, err) client, err = getSftpClient(u, usePubKey) if assert.NoError(t, err) { @@ -2231,6 +2231,81 @@ func TestLoginExternalAuthPwdAndPubKey(t *testing.T) { assert.NoError(t, err) } +func TestExternalAuthEmptyResponse(t *testing.T) { + if runtime.GOOS == osWindows { + t.Skip("this test is not available on Windows") + } + usePubKey := false + u := getTestUser(usePubKey) + u.QuotaFiles = 1000 + err := dataprovider.Close() + assert.NoError(t, err) + err = config.LoadConfig(configDir, "") + assert.NoError(t, err) + providerConf := config.GetProviderConf() + err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, ""), os.ModePerm) + assert.NoError(t, err) + providerConf.ExternalAuthHook = extAuthPath + providerConf.ExternalAuthScope = 0 + err = dataprovider.Initialize(providerConf, configDir, true) + assert.NoError(t, err) + + testFileSize := int64(65535) + // the user will be created + client, err := getSftpClient(u, usePubKey) + if assert.NoError(t, err) { + defer client.Close() + testFilePath := filepath.Join(homeBasePath, testFileName) + err = createTestFile(testFilePath, testFileSize) + assert.NoError(t, err) + err = sftpUploadFile(testFilePath, testFileName, testFileSize, client) + assert.NoError(t, err) + err = os.Remove(testFilePath) + assert.NoError(t, err) + } + + user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK) + assert.NoError(t, err) + assert.Equal(t, 0, len(user.PublicKeys)) + assert.Equal(t, testFileSize, user.UsedQuotaSize) + assert.Equal(t, 1, user.UsedQuotaFiles) + // now modify the user + user.MaxSessions = 10 + user.QuotaFiles = 100 + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") + assert.NoError(t, err) + + err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, true, ""), os.ModePerm) + assert.NoError(t, err) + + client, err = getSftpClient(u, usePubKey) + if assert.NoError(t, err) { + defer client.Close() + err = checkBasicSFTP(client) + assert.NoError(t, err) + } + + user, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusOK) + assert.NoError(t, err) + assert.Equal(t, 10, user.MaxSessions) + assert.Equal(t, 100, user.QuotaFiles) + + _, err = httpdtest.RemoveUser(user, http.StatusOK) + assert.NoError(t, err) + err = os.RemoveAll(user.GetHomeDir()) + assert.NoError(t, err) + + err = dataprovider.Close() + assert.NoError(t, err) + err = config.LoadConfig(configDir, "") + assert.NoError(t, err) + providerConf = config.GetProviderConf() + err = dataprovider.Initialize(providerConf, configDir, true) + assert.NoError(t, err) + err = os.Remove(extAuthPath) + assert.NoError(t, err) +} + func TestExternalAuthDifferentUsername(t *testing.T) { if runtime.GOOS == osWindows { t.Skip("this test is not available on Windows") @@ -2244,7 +2319,7 @@ func TestExternalAuthDifferentUsername(t *testing.T) { err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf := config.GetProviderConf() - err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, extAuthUsername), os.ModePerm) + err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, extAuthUsername), os.ModePerm) assert.NoError(t, err) providerConf.ExternalAuthHook = extAuthPath providerConf.ExternalAuthScope = 0 @@ -2327,7 +2402,7 @@ func TestLoginExternalAuth(t *testing.T) { err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf := config.GetProviderConf() - err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, ""), os.ModePerm) + err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, ""), os.ModePerm) assert.NoError(t, err) providerConf.ExternalAuthHook = extAuthPath providerConf.ExternalAuthScope = authScope @@ -2389,7 +2464,7 @@ func TestLoginExternalAuthInteractive(t *testing.T) { err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf := config.GetProviderConf() - err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, ""), os.ModePerm) + err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, ""), os.ModePerm) assert.NoError(t, err) providerConf.ExternalAuthHook = extAuthPath providerConf.ExternalAuthScope = 4 @@ -2443,7 +2518,7 @@ func TestLoginExternalAuthErrors(t *testing.T) { err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf := config.GetProviderConf() - err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, true, ""), os.ModePerm) + err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, true, false, ""), os.ModePerm) assert.NoError(t, err) providerConf.ExternalAuthHook = extAuthPath providerConf.ExternalAuthScope = 0 @@ -8978,8 +9053,11 @@ func getKeyboardInteractiveScriptContent(questions []string, sleepTime int, nonJ return content } -func getExtAuthScriptContent(user dataprovider.User, nonJSONResponse bool, username string) []byte { +func getExtAuthScriptContent(user dataprovider.User, nonJSONResponse, emptyResponse bool, username string) []byte { extAuthContent := []byte("#!/bin/sh\n\n") + if emptyResponse { + return extAuthContent + } extAuthContent = append(extAuthContent, []byte(fmt.Sprintf("if test \"$SFTPGO_AUTHD_USERNAME\" = \"%v\"; then\n", user.Username))...) if len(username) > 0 { user.Username = username diff --git a/utils/utils.go b/utils/utils.go index eae594c2..29cc2795 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -2,6 +2,7 @@ package utils import ( + "bytes" "crypto/aes" "crypto/cipher" "crypto/ecdsa" @@ -489,3 +490,29 @@ func CheckTCP4Port(port int) { } listener.Close() } + +// IsByteArrayEmpty return true if the byte array is empty or a new line +func IsByteArrayEmpty(b []byte) bool { + if len(b) == 0 { + return true + } + if bytes.Equal(b, []byte("\n")) { + return true + } + if bytes.Equal(b, []byte("\r\n")) { + return true + } + return false +} + +// GetSSHPublicKeyAsString returns an SSH public key serialized as string +func GetSSHPublicKeyAsString(pubKey []byte) (string, error) { + if len(pubKey) == 0 { + return "", nil + } + k, err := ssh.ParsePublicKey(pubKey) + if err != nil { + return "", err + } + return string(ssh.MarshalAuthorizedKey(k)), nil +}