external auth: allow to inspect and preserve an existing user

This commit is contained in:
Nicola Murino 2021-03-26 15:19:01 +01:00
parent d5f092284a
commit 5f49af1780
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
8 changed files with 209 additions and 64 deletions

View file

@ -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

View file

@ -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)

View file

@ -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...)
}

View file

@ -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.

14
go.mod
View file

@ -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

29
go.sum
View file

@ -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=

View file

@ -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

View file

@ -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
}