Browse Source

REST API: remove merging of fields on updates

we use PUT verb not PATCH. We keep merging only to allow to preserve
hidden/encrypted fields.

This is a backward incompatible change, but is necessary to avoid unexpected
issues.
You have to pass complete objects on updates.

Fixes #1088

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino 2 years ago
parent
commit
0841c7d7bd

+ 7 - 7
go.mod

@@ -9,13 +9,13 @@ require (
 	github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
 	github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
 	github.com/aws/aws-sdk-go-v2 v1.17.3
-	github.com/aws/aws-sdk-go-v2/config v1.18.6
-	github.com/aws/aws-sdk-go-v2/credentials v1.13.6
+	github.com/aws/aws-sdk-go-v2/config v1.18.7
+	github.com/aws/aws-sdk-go-v2/credentials v1.13.7
 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21
-	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.45
+	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.46
 	github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.26
 	github.com/aws/aws-sdk-go-v2/service/s3 v1.29.6
-	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.10
+	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.11
 	github.com/aws/aws-sdk-go-v2/service/sts v1.17.7
 	github.com/cockroachdb/cockroach-go/v2 v2.2.19
 	github.com/coreos/go-oidc/v3 v3.4.0
@@ -92,7 +92,7 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.22 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.21 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.11.27 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.11.28 // indirect
 	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.11 // indirect
 	github.com/aws/smithy-go v1.13.5 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
@@ -144,7 +144,7 @@ require (
 	github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
 	github.com/prometheus/client_model v0.3.0 // indirect
 	github.com/prometheus/common v0.39.0 // indirect
-	github.com/prometheus/procfs v0.8.0 // indirect
+	github.com/prometheus/procfs v0.9.0 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/spf13/cast v1.5.0 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
@@ -171,5 +171,5 @@ require (
 replace (
 	github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20221203115213-ba73c775a9fd
 	github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
-	golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20221220153730-5f47589cce28
+	golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20221223081523-be6917ff6f72
 )

+ 14 - 14
go.sum

@@ -233,17 +233,17 @@ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3/go.mod h1:gNsR5CaXK
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
 github.com/aws/aws-sdk-go-v2/config v1.15.15/go.mod h1:A1Lzyy/o21I5/s2FbyX5AevQfSVXpvvIDCoVFD0BC4E=
-github.com/aws/aws-sdk-go-v2/config v1.18.6 h1:iSuEAeervBWMHA7Aaq5hCNfwuN2m7x2VuQCnEbbQg68=
-github.com/aws/aws-sdk-go-v2/config v1.18.6/go.mod h1:qyjgnyqpKnNGT+C62zMsrZ/Mn2OodYqwIH0DpXiW8f8=
+github.com/aws/aws-sdk-go-v2/config v1.18.7 h1:V94lTcix6jouwmAsgQMAEBozVAGJMFhVj+6/++xfe3E=
+github.com/aws/aws-sdk-go-v2/config v1.18.7/go.mod h1:OZYsyHFL5PB9UpyS78NElgKs11qI/B5KJau2XOJDXHA=
 github.com/aws/aws-sdk-go-v2/credentials v1.12.10/go.mod h1:g5eIM5XRs/OzIIK81QMBl+dAuDyoLN0VYaLP+tBqEOk=
-github.com/aws/aws-sdk-go-v2/credentials v1.13.6 h1:BXOMvv3O82/4JLggIi67WKlTO56f0rliCKBT4CKyf0o=
-github.com/aws/aws-sdk-go-v2/credentials v1.13.6/go.mod h1:VbnUvhw31DUu6aiubViixQwWCBNO/st84dhPeOkmdls=
+github.com/aws/aws-sdk-go-v2/credentials v1.13.7 h1:qUUcNS5Z1092XBFT66IJM7mYkMwgZ8fcC8YDIbEwXck=
+github.com/aws/aws-sdk-go-v2/credentials v1.13.7/go.mod h1:AdCcbZXHQCjJh6NaH3pFaw8LUeBFn5+88BZGMVGuBT8=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.9/go.mod h1:KDCCm4ONIdHtUloDcFvK2+vshZvx4Zmj7UMDfusuz5s=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21 h1:j9wi1kQ8b+e0FBVHxCqCGo4kxDU175hoDHcWAi0sauU=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21/go.mod h1:ugwW57Z5Z48bpvUyZuaPy4Kv+vEfJWnIrky7RmkBvJg=
 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.21/go.mod h1:iIYPrQ2rYfZiB/iADYlhj9HHZ9TTi6PqKQPAqygohbE=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.45 h1:ckFtXy51PT613d/KLKPxFiwRqgGIxDhVbNLof6x/XLo=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.45/go.mod h1:xar61xizdVU4pQygvQrNdZY1VCLNcOIvm87KzdZmWrE=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.46 h1:OCX1pQ4pcqhsDV7B92HzdLWjHWOQsILvjLinpaUWhcc=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.46/go.mod h1:MxCBOcyNXGJRvfpPiH+L6n/BF9zbowthGSUZdDvQF/c=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.15/go.mod h1:pWrr2OoHlT7M/Pd2y4HV3gJyPb3qj5qMmnPkKSNPYK4=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27 h1:I3cakv2Uy1vNmmhRQmFptYDxOvBnwCdNwyw63N0RaRU=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27/go.mod h1:a1/UpzeyBBerajpnP5nGZa9mGzsBn5cOKxm6NWQsvoI=
@@ -275,14 +275,14 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.27.2/go.mod h1:u+566cosFI+d+motIz3USX
 github.com/aws/aws-sdk-go-v2/service/s3 v1.29.6 h1:W8pLcSn6Uy0eXgDBUUl8M8Kxv7JCoP68ZKTD04OXLEA=
 github.com/aws/aws-sdk-go-v2/service/s3 v1.29.6/go.mod h1:L2l2/q76teehcW7YEsgsDjqdsDTERJeX3nOMIFlgGUE=
 github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.14/go.mod h1:xakbH8KMsQQKqzX87uyyzTHshc/0/Df8bsTneTS5pFU=
-github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.10 h1:6obimjQAiRlEUZT7a2Q1ikH7ck4cPO3phGz4wqI5f2w=
-github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.10/go.mod h1:jAeo/PdIJZuDSwsvxJS94G4d6h8tStj7WXVuKwLHWU8=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.11 h1:77V7vnw/NC4DORHVgA97+Ky2p1ri0+ZVYXh6ordUZU0=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.11/go.mod h1:jAeo/PdIJZuDSwsvxJS94G4d6h8tStj7WXVuKwLHWU8=
 github.com/aws/aws-sdk-go-v2/service/sns v1.17.10/go.mod h1:uITsRNVMeCB3MkWpXxXw0eDz8pW4TYLzj+eyQtbhSxM=
 github.com/aws/aws-sdk-go-v2/service/sqs v1.19.1/go.mod h1:A94o564Gj+Yn+7QO1eLFeI7UVv3riy/YBFOfICVqFvU=
 github.com/aws/aws-sdk-go-v2/service/ssm v1.27.6/go.mod h1:fiFzQgj4xNOg4/wqmAiPvzgDMXPD+cUEplX/CYn+0j0=
 github.com/aws/aws-sdk-go-v2/service/sso v1.11.13/go.mod h1:d7ptRksDDgvXaUvxyHZ9SYh+iMDymm94JbVcgvSYSzU=
-github.com/aws/aws-sdk-go-v2/service/sso v1.11.27 h1:Nmvn0DJKg00TBmoBweK253Kdsuy4V5Rs68yL/H15uBQ=
-github.com/aws/aws-sdk-go-v2/service/sso v1.11.27/go.mod h1:wo/B7uUm/7zw/dWhBJ4FXuw1sySU5lyIhVg1Bu2yL9A=
+github.com/aws/aws-sdk-go-v2/service/sso v1.11.28 h1:gItLq3zBYyRDPmqAClgzTH8PBjDQGeyptYGHIwtYYNA=
+github.com/aws/aws-sdk-go-v2/service/sso v1.11.28/go.mod h1:wo/B7uUm/7zw/dWhBJ4FXuw1sySU5lyIhVg1Bu2yL9A=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.11 h1:KCacyVSs/wlcPGx37hcbT3IGYO8P8Jx+TgSDhAXtQMY=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.11/go.mod h1:TZSH7xLO7+phDtViY/KUp9WGCJMQkLJ/VpgkTFd5gh8=
 github.com/aws/aws-sdk-go-v2/service/sts v1.16.10/go.mod h1:cftkHYN6tCDNfkSasAmclSfl4l7cySoay8vz7p/ce0E=
@@ -538,8 +538,8 @@ github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
 github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
 github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
-github.com/drakkan/crypto v0.0.0-20221220153730-5f47589cce28 h1:QTEjJpZpyqRFlN4Yh9GIumPHDJ9LYWka9aWRPz1RiOk=
-github.com/drakkan/crypto v0.0.0-20221220153730-5f47589cce28/go.mod h1:cy6DFZ6nHFw1bTHZksT/gYKmdxPdzr7Rw7xcJFSayo4=
+github.com/drakkan/crypto v0.0.0-20221223081523-be6917ff6f72 h1:Ivant8yrd81A5y3tQOS7vqwL9QaOdlGonHNOfRR3rsQ=
+github.com/drakkan/crypto v0.0.0-20221223081523-be6917ff6f72/go.mod h1:cy6DFZ6nHFw1bTHZksT/gYKmdxPdzr7Rw7xcJFSayo4=
 github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
 github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
 github.com/drakkan/ftpserverlib v0.0.0-20221203115213-ba73c775a9fd h1:wu/ys+33GwD9PyRO8QDCUpI2WBZtwFiDk8QkFPW8rhQ=
@@ -1409,8 +1409,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
 github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
 github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
 github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
-github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
+github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
+github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
 github.com/prometheus/prometheus v0.35.0/go.mod h1:7HaLx5kEPKJ0GDgbODG0fZgXbQ8K/XjZNJXQmbmgQlY=
 github.com/prometheus/prometheus v0.37.0/go.mod h1:egARUgz+K93zwqsVIAneFlLZefyGOON44WyAp4Xqbbk=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=

+ 14 - 15
internal/httpd/api_admin.go

@@ -116,13 +116,8 @@ func updateAdmin(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	adminID := admin.ID
-	username = admin.Username
-	totpConfig := admin.Filters.TOTPConfig
-	recoveryCodes := admin.Filters.RecoveryCodes
-	admin.Filters.TOTPConfig = dataprovider.AdminTOTPConfig{}
-	admin.Filters.RecoveryCodes = nil
-	err = render.DecodeJSON(r.Body, &admin)
+	var updatedAdmin dataprovider.Admin
+	err = render.DecodeJSON(r.Body, &updatedAdmin)
 	if err != nil {
 		sendAPIResponse(w, r, err, "", http.StatusBadRequest)
 		return
@@ -139,24 +134,28 @@ func updateAdmin(w http.ResponseWriter, r *http.Request) {
 				http.StatusBadRequest)
 			return
 		}
-		if claims.isCriticalPermRemoved(admin.Permissions) {
+		if claims.isCriticalPermRemoved(updatedAdmin.Permissions) {
 			sendAPIResponse(w, r, errors.New("you cannot remove these permissions to yourself"), "", http.StatusBadRequest)
 			return
 		}
-		if admin.Status == 0 {
+		if updatedAdmin.Status == 0 {
 			sendAPIResponse(w, r, errors.New("you cannot disable yourself"), "", http.StatusBadRequest)
 			return
 		}
-		if admin.Role != claims.Role {
+		if updatedAdmin.Role != claims.Role {
 			sendAPIResponse(w, r, errors.New("you cannot add/change your role"), "", http.StatusBadRequest)
 			return
 		}
 	}
-	admin.ID = adminID
-	admin.Username = username
-	admin.Filters.TOTPConfig = totpConfig
-	admin.Filters.RecoveryCodes = recoveryCodes
-	if err := dataprovider.UpdateAdmin(&admin, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role); err != nil {
+	updatedAdmin.ID = admin.ID
+	updatedAdmin.Username = admin.Username
+	if updatedAdmin.Password == "" {
+		updatedAdmin.Password = admin.Password
+	}
+	updatedAdmin.Filters.TOTPConfig = admin.Filters.TOTPConfig
+	updatedAdmin.Filters.RecoveryCodes = admin.Filters.RecoveryCodes
+	err = dataprovider.UpdateAdmin(&updatedAdmin, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)
+	if err != nil {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
 		return
 	}

+ 17 - 22
internal/httpd/api_eventrule.go

@@ -96,32 +96,30 @@ func updateEventAction(w http.ResponseWriter, r *http.Request) {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
 		return
 	}
-	actionID := action.ID
-	name = action.Name
-	currentHTTPPassword := action.Options.HTTPConfig.Password
-	action.Options = dataprovider.BaseEventActionOptions{}
 
-	err = render.DecodeJSON(r.Body, &action)
+	var updatedAction dataprovider.BaseEventAction
+	err = render.DecodeJSON(r.Body, &updatedAction)
 	if err != nil {
 		sendAPIResponse(w, r, err, "", http.StatusBadRequest)
 		return
 	}
-	action.ID = actionID
-	action.Name = name
-	action.Options.SetEmptySecretsIfNil()
-	switch action.Type {
+	updatedAction.ID = action.ID
+	updatedAction.Name = action.Name
+	updatedAction.Options.SetEmptySecretsIfNil()
+
+	switch updatedAction.Type {
 	case dataprovider.ActionTypeHTTP:
-		if action.Options.HTTPConfig.Password.IsNotPlainAndNotEmpty() {
-			action.Options.HTTPConfig.Password = currentHTTPPassword
+		if updatedAction.Options.HTTPConfig.Password.IsNotPlainAndNotEmpty() {
+			updatedAction.Options.HTTPConfig.Password = action.Options.HTTPConfig.Password
 		}
 	}
 
-	err = dataprovider.UpdateEventAction(&action, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)
+	err = dataprovider.UpdateEventAction(&updatedAction, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)
 	if err != nil {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
 		return
 	}
-	sendAPIResponse(w, r, nil, "Event target updated", http.StatusOK)
+	sendAPIResponse(w, r, nil, "Event action updated", http.StatusOK)
 }
 
 func deleteEventAction(w http.ResponseWriter, r *http.Request) {
@@ -206,25 +204,22 @@ func updateEventRule(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	name := getURLParam(r, "name")
-	rule, err := dataprovider.EventRuleExists(name)
+	rule, err := dataprovider.EventRuleExists(getURLParam(r, "name"))
 	if err != nil {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
 		return
 	}
-	ruleID := rule.ID
-	name = rule.Name
-	rule.Actions = nil
 
-	err = render.DecodeJSON(r.Body, &rule)
+	var updatedRule dataprovider.EventRule
+	err = render.DecodeJSON(r.Body, &updatedRule)
 	if err != nil {
 		sendAPIResponse(w, r, err, "", http.StatusBadRequest)
 		return
 	}
-	rule.ID = ruleID
-	rule.Name = name
+	updatedRule.ID = rule.ID
+	updatedRule.Name = rule.Name
 
-	err = dataprovider.UpdateEventRule(&rule, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)
+	err = dataprovider.UpdateEventRule(&updatedRule, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)
 	if err != nil {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
 		return

+ 12 - 28
internal/httpd/api_folder.go

@@ -76,39 +76,23 @@ func updateFolder(w http.ResponseWriter, r *http.Request) {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
 		return
 	}
-	users := folder.Users
-	groups := folder.Groups
-	folderID := folder.ID
-	name = folder.Name
-	currentS3AccessSecret := folder.FsConfig.S3Config.AccessSecret
-	currentAzAccountKey := folder.FsConfig.AzBlobConfig.AccountKey
-	currentAzSASUrl := folder.FsConfig.AzBlobConfig.SASURL
-	currentGCSCredentials := folder.FsConfig.GCSConfig.Credentials
-	currentCryptoPassphrase := folder.FsConfig.CryptConfig.Passphrase
-	currentSFTPPassword := folder.FsConfig.SFTPConfig.Password
-	currentSFTPKey := folder.FsConfig.SFTPConfig.PrivateKey
-	currentSFTPKeyPassphrase := folder.FsConfig.SFTPConfig.KeyPassphrase
-	currentHTTPPassword := folder.FsConfig.HTTPConfig.Password
-	currentHTTPAPIKey := folder.FsConfig.HTTPConfig.APIKey
 
-	folder.FsConfig.S3Config = vfs.S3FsConfig{}
-	folder.FsConfig.AzBlobConfig = vfs.AzBlobFsConfig{}
-	folder.FsConfig.GCSConfig = vfs.GCSFsConfig{}
-	folder.FsConfig.CryptConfig = vfs.CryptFsConfig{}
-	folder.FsConfig.SFTPConfig = vfs.SFTPFsConfig{}
-	folder.FsConfig.HTTPConfig = vfs.HTTPFsConfig{}
-	err = render.DecodeJSON(r.Body, &folder)
+	var updatedFolder vfs.BaseVirtualFolder
+	err = render.DecodeJSON(r.Body, &updatedFolder)
 	if err != nil {
 		sendAPIResponse(w, r, err, "", http.StatusBadRequest)
 		return
 	}
-	folder.ID = folderID
-	folder.Name = name
-	folder.FsConfig.SetEmptySecretsIfNil()
-	updateEncryptedSecrets(&folder.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl, currentGCSCredentials,
-		currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase, currentHTTPPassword,
-		currentHTTPAPIKey)
-	err = dataprovider.UpdateFolder(&folder, users, groups, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)
+	updatedFolder.ID = folder.ID
+	updatedFolder.Name = folder.Name
+	updatedFolder.FsConfig.SetEmptySecretsIfNil()
+	updateEncryptedSecrets(&updatedFolder.FsConfig, folder.FsConfig.S3Config.AccessSecret, folder.FsConfig.AzBlobConfig.AccountKey,
+		folder.FsConfig.AzBlobConfig.SASURL, folder.FsConfig.GCSConfig.Credentials, folder.FsConfig.CryptConfig.Passphrase,
+		folder.FsConfig.SFTPConfig.Password, folder.FsConfig.SFTPConfig.PrivateKey, folder.FsConfig.SFTPConfig.KeyPassphrase,
+		folder.FsConfig.HTTPConfig.Password, folder.FsConfig.HTTPConfig.APIKey)
+
+	err = dataprovider.UpdateFolder(&updatedFolder, folder.Users, folder.Groups, claims.Username,
+		util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)
 	if err != nil {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
 		return

+ 9 - 16
internal/httpd/api_group.go

@@ -22,7 +22,6 @@ import (
 
 	"github.com/drakkan/sftpgo/v2/internal/dataprovider"
 	"github.com/drakkan/sftpgo/v2/internal/util"
-	"github.com/drakkan/sftpgo/v2/internal/vfs"
 )
 
 func getGroups(w http.ResponseWriter, r *http.Request) {
@@ -76,9 +75,7 @@ func updateGroup(w http.ResponseWriter, r *http.Request) {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
 		return
 	}
-	users := group.Users
-	groupID := group.ID
-	name = group.Name
+
 	currentS3AccessSecret := group.UserSettings.FsConfig.S3Config.AccessSecret
 	currentAzAccountKey := group.UserSettings.FsConfig.AzBlobConfig.AccountKey
 	currentAzSASUrl := group.UserSettings.FsConfig.AzBlobConfig.SASURL
@@ -90,24 +87,20 @@ func updateGroup(w http.ResponseWriter, r *http.Request) {
 	currentHTTPPassword := group.UserSettings.FsConfig.HTTPConfig.Password
 	currentHTTPAPIKey := group.UserSettings.FsConfig.HTTPConfig.APIKey
 
-	group.UserSettings.FsConfig.S3Config = vfs.S3FsConfig{}
-	group.UserSettings.FsConfig.AzBlobConfig = vfs.AzBlobFsConfig{}
-	group.UserSettings.FsConfig.GCSConfig = vfs.GCSFsConfig{}
-	group.UserSettings.FsConfig.CryptConfig = vfs.CryptFsConfig{}
-	group.UserSettings.FsConfig.SFTPConfig = vfs.SFTPFsConfig{}
-	group.UserSettings.FsConfig.HTTPConfig = vfs.HTTPFsConfig{}
-	err = render.DecodeJSON(r.Body, &group)
+	var updatedGroup dataprovider.Group
+	err = render.DecodeJSON(r.Body, &updatedGroup)
 	if err != nil {
 		sendAPIResponse(w, r, err, "", http.StatusBadRequest)
 		return
 	}
-	group.ID = groupID
-	group.Name = name
-	group.UserSettings.FsConfig.SetEmptySecretsIfNil()
-	updateEncryptedSecrets(&group.UserSettings.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl,
+	updatedGroup.ID = group.ID
+	updatedGroup.Name = group.Name
+	updatedGroup.UserSettings.FsConfig.SetEmptySecretsIfNil()
+	updateEncryptedSecrets(&updatedGroup.UserSettings.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl,
 		currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase,
 		currentHTTPPassword, currentHTTPAPIKey)
-	err = dataprovider.UpdateGroup(&group, users, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)
+	err = dataprovider.UpdateGroup(&updatedGroup, group.Users, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr),
+		claims.Role)
 	if err != nil {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
 		return

+ 6 - 3
internal/httpd/api_keys.go

@@ -98,14 +98,17 @@ func updateAPIKey(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	err = render.DecodeJSON(r.Body, &apiKey)
+	var updatedAPIKey dataprovider.APIKey
+	err = render.DecodeJSON(r.Body, &updatedAPIKey)
 	if err != nil {
 		sendAPIResponse(w, r, err, "", http.StatusBadRequest)
 		return
 	}
 
-	apiKey.KeyID = keyID
-	if err := dataprovider.UpdateAPIKey(&apiKey, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role); err != nil {
+	updatedAPIKey.KeyID = keyID
+	updatedAPIKey.Key = apiKey.Key
+	err = dataprovider.UpdateAPIKey(&updatedAPIKey, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)
+	if err != nil {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
 		return
 	}

+ 7 - 6
internal/httpd/api_role.go

@@ -75,16 +75,17 @@ func updateRole(w http.ResponseWriter, r *http.Request) {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
 		return
 	}
-	roleID := role.ID
-	name = role.Name
-	err = render.DecodeJSON(r.Body, &role)
+
+	var updatedRole dataprovider.Role
+	err = render.DecodeJSON(r.Body, &updatedRole)
 	if err != nil {
 		sendAPIResponse(w, r, err, "", http.StatusBadRequest)
 		return
 	}
-	role.ID = roleID
-	role.Name = name
-	err = dataprovider.UpdateRole(&role, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)
+
+	updatedRole.ID = role.ID
+	updatedRole.Name = role.Name
+	err = dataprovider.UpdateRole(&updatedRole, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)
 	if err != nil {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
 		return

+ 9 - 8
internal/httpd/api_shares.go

@@ -131,26 +131,27 @@ func updateShare(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	oldPassword := share.Password
-	err = render.DecodeJSON(r.Body, &share)
+	var updatedShare dataprovider.Share
+	err = render.DecodeJSON(r.Body, &updatedShare)
 	if err != nil {
 		sendAPIResponse(w, r, err, "", http.StatusBadRequest)
 		return
 	}
 
-	share.ShareID = shareID
-	share.Username = claims.Username
-	if share.Password == redactedSecret {
-		share.Password = oldPassword
+	updatedShare.ShareID = shareID
+	updatedShare.Username = claims.Username
+	if updatedShare.Password == redactedSecret {
+		updatedShare.Password = share.Password
 	}
-	if share.Password == "" {
+	if updatedShare.Password == "" {
 		if util.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
 			sendAPIResponse(w, r, nil, "You are not authorized to share files/folders without a password",
 				http.StatusForbidden)
 			return
 		}
 	}
-	if err := dataprovider.UpdateShare(&share, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role); err != nil {
+	err = dataprovider.UpdateShare(&updatedShare, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)
+	if err != nil {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
 		return
 	}

+ 15 - 42
internal/httpd/api_user.go

@@ -163,55 +163,28 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
 		return
 	}
-	userID := user.ID
-	username = user.Username
-	lastPwdChange := user.LastPasswordChange
-	totpConfig := user.Filters.TOTPConfig
-	recoveryCodes := user.Filters.RecoveryCodes
-	currentPermissions := user.Permissions
-	currentS3AccessSecret := user.FsConfig.S3Config.AccessSecret
-	currentAzAccountKey := user.FsConfig.AzBlobConfig.AccountKey
-	currentAzSASUrl := user.FsConfig.AzBlobConfig.SASURL
-	currentGCSCredentials := user.FsConfig.GCSConfig.Credentials
-	currentCryptoPassphrase := user.FsConfig.CryptConfig.Passphrase
-	currentSFTPPassword := user.FsConfig.SFTPConfig.Password
-	currentSFTPKey := user.FsConfig.SFTPConfig.PrivateKey
-	currentSFTPKeyPassphrase := user.FsConfig.SFTPConfig.KeyPassphrase
-	currentHTTPPassword := user.FsConfig.HTTPConfig.Password
-	currentHTTPAPIKey := user.FsConfig.HTTPConfig.APIKey
 
-	user.Permissions = make(map[string][]string)
-	user.FsConfig.S3Config = vfs.S3FsConfig{}
-	user.FsConfig.AzBlobConfig = vfs.AzBlobFsConfig{}
-	user.FsConfig.GCSConfig = vfs.GCSFsConfig{}
-	user.FsConfig.CryptConfig = vfs.CryptFsConfig{}
-	user.FsConfig.SFTPConfig = vfs.SFTPFsConfig{}
-	user.FsConfig.HTTPConfig = vfs.HTTPFsConfig{}
-	user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{}
-	user.Filters.RecoveryCodes = nil
-	user.VirtualFolders = nil
-	err = render.DecodeJSON(r.Body, &user)
+	var updatedUser dataprovider.User
+	updatedUser.Password = user.Password
+	err = render.DecodeJSON(r.Body, &updatedUser)
 	if err != nil {
 		sendAPIResponse(w, r, err, "", http.StatusBadRequest)
 		return
 	}
-	user.ID = userID
-	user.Username = username
-	user.Filters.TOTPConfig = totpConfig
-	user.Filters.RecoveryCodes = recoveryCodes
-	user.LastPasswordChange = lastPwdChange
-	user.SetEmptySecretsIfNil()
-	// we use new Permissions if passed otherwise the old ones
-	if len(user.Permissions) == 0 {
-		user.Permissions = currentPermissions
-	}
-	updateEncryptedSecrets(&user.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl,
-		currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase,
-		currentHTTPPassword, currentHTTPAPIKey)
+	updatedUser.ID = user.ID
+	updatedUser.Username = user.Username
+	updatedUser.Filters.RecoveryCodes = user.Filters.RecoveryCodes
+	updatedUser.Filters.TOTPConfig = user.Filters.TOTPConfig
+	updatedUser.LastPasswordChange = user.LastPasswordChange
+	updatedUser.SetEmptySecretsIfNil()
+	updateEncryptedSecrets(&updatedUser.FsConfig, user.FsConfig.S3Config.AccessSecret, user.FsConfig.AzBlobConfig.AccountKey,
+		user.FsConfig.AzBlobConfig.SASURL, user.FsConfig.GCSConfig.Credentials, user.FsConfig.CryptConfig.Passphrase,
+		user.FsConfig.SFTPConfig.Password, user.FsConfig.SFTPConfig.PrivateKey, user.FsConfig.SFTPConfig.KeyPassphrase,
+		user.FsConfig.HTTPConfig.Password, user.FsConfig.HTTPConfig.APIKey)
 	if claims.Role != "" {
-		user.Role = claims.Role
+		updatedUser.Role = claims.Role
 	}
-	err = dataprovider.UpdateUser(&user, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)
+	err = dataprovider.UpdateUser(&updatedUser, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)
 	if err != nil {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
 		return

+ 62 - 47
internal/httpd/httpd_test.go

@@ -668,8 +668,8 @@ func TestRoleRelations(t *testing.T) {
 	admin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)
 	assert.NoError(t, err)
 	admin.Role = "invalid role"
-	_, _, err = httpdtest.UpdateAdmin(admin, http.StatusInternalServerError)
-	assert.NoError(t, err)
+	_, resp, err = httpdtest.UpdateAdmin(admin, http.StatusInternalServerError)
+	assert.NoError(t, err, string(resp))
 	admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)
 	assert.NoError(t, err)
 	assert.Equal(t, role.Name, admin.Role)
@@ -813,21 +813,38 @@ func TestBasicGroupHandling(t *testing.T) {
 	group.UserSettings.FsConfig.SFTPConfig.Endpoint = sftpServerAddr
 	group.UserSettings.FsConfig.SFTPConfig.Username = defaultUsername
 	group.UserSettings.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword)
+	group.UserSettings.Permissions = map[string][]string{
+		"/": {dataprovider.PermAny},
+	}
+	group.UserSettings.Filters.AllowedIP = []string{"10.0.0.0/8"}
 	group, _, err = httpdtest.UpdateGroup(group, http.StatusOK)
 	assert.NoError(t, err)
+	groupGet, _, err = httpdtest.GetGroupByName(group.Name, http.StatusOK)
+	assert.NoError(t, err)
+	assert.Len(t, groupGet.UserSettings.Permissions, 1)
+	assert.Len(t, groupGet.UserSettings.Filters.AllowedIP, 1)
+
 	// update again and check that the password was preserved
 	dbGroup, err := dataprovider.GroupExists(group.Name)
 	assert.NoError(t, err)
 	group.UserSettings.FsConfig.SFTPConfig.Password = kms.NewSecret(
 		dbGroup.UserSettings.FsConfig.SFTPConfig.Password.GetStatus(),
 		dbGroup.UserSettings.FsConfig.SFTPConfig.Password.GetPayload(), "", "")
+	group.UserSettings.Permissions = nil
+	group.UserSettings.Filters.AllowedIP = nil
 	group, _, err = httpdtest.UpdateGroup(group, http.StatusOK)
 	assert.NoError(t, err)
+	assert.Len(t, group.UserSettings.Permissions, 0)
+	assert.Len(t, group.UserSettings.Filters.AllowedIP, 0)
 	dbGroup, err = dataprovider.GroupExists(group.Name)
 	assert.NoError(t, err)
 	err = dbGroup.UserSettings.FsConfig.SFTPConfig.Password.Decrypt()
 	assert.NoError(t, err)
 	assert.Equal(t, defaultPassword, dbGroup.UserSettings.FsConfig.SFTPConfig.Password.GetPayload())
+	// check the group permissions
+	groupGet, _, err = httpdtest.GetGroupByName(group.Name, http.StatusOK)
+	assert.NoError(t, err)
+	assert.Len(t, groupGet.UserSettings.Permissions, 0)
 
 	group.UserSettings.HomeDir = "relative path"
 	_, _, err = httpdtest.UpdateGroup(group, http.StatusBadRequest)
@@ -4191,7 +4208,11 @@ func TestUpdateUserEmptyPassword(t *testing.T) {
 	assert.NotEmpty(t, dbUser.Password)
 	assert.True(t, dbUser.IsPasswordHashed())
 	// now update the user and set an empty password
-	customUser := make(map[string]any)
+	data, err := json.Marshal(dbUser)
+	assert.NoError(t, err)
+	var customUser map[string]any
+	err = json.Unmarshal(data, &customUser)
+	assert.NoError(t, err)
 	customUser["password"] = ""
 	asJSON, err := json.Marshal(customUser)
 	assert.NoError(t, err)
@@ -4208,6 +4229,30 @@ func TestUpdateUserEmptyPassword(t *testing.T) {
 	assert.NoError(t, err)
 }
 
+func TestUpdateUserNoPassword(t *testing.T) {
+	u := getTestUser()
+	u.PublicKeys = []string{testPubKey}
+	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
+	assert.NoError(t, err)
+	// the password is not empty
+	dbUser, err := dataprovider.UserExists(u.Username, "")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, dbUser.Password)
+	assert.True(t, dbUser.IsPasswordHashed())
+	// now update the user and remove the password field, old password should be preserved
+	user.Password = "" // password has the omitempty tag
+	_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
+	assert.NoError(t, err)
+	// the password is preserved
+	dbUser, err = dataprovider.UserExists(u.Username, "")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, dbUser.Password)
+	assert.True(t, dbUser.IsPasswordHashed())
+
+	_, err = httpdtest.RemoveUser(user, http.StatusOK)
+	assert.NoError(t, err)
+}
+
 func TestUpdateUser(t *testing.T) {
 	u := getTestUser()
 	u.UsedQuotaFiles = 1
@@ -5947,10 +5992,11 @@ func TestNamingRules(t *testing.T) {
 	assert.Equal(t, "文件夹ab", folder.Name)
 	folder.Name = f.Name
 	folder.Description = folder.Name
-	folder, resp, err = httpdtest.UpdateFolder(folder, http.StatusOK)
+	_, resp, err = httpdtest.UpdateFolder(folder, http.StatusOK)
 	assert.NoError(t, err, string(resp))
 	folder, resp, err = httpdtest.GetFolderByName(f.Name, http.StatusOK)
 	assert.NoError(t, err, string(resp))
+	assert.Equal(t, "文件夹AB", folder.Description)
 	_, err = httpdtest.RemoveFolder(f, http.StatusOK)
 	assert.NoError(t, err)
 	token, err := getJWTWebClientTokenFromTestServer(u.Username, defaultPassword)
@@ -9620,7 +9666,10 @@ func TestWebAPIChangeUserProfileMock(t *testing.T) {
 	assert.Equal(t, email, profileReq["email"].(string))
 	assert.Equal(t, description, profileReq["description"].(string))
 	assert.True(t, profileReq["allow_api_key_auth"].(bool))
-	assert.Len(t, profileReq["public_keys"].([]any), 2)
+	val, ok := profileReq["public_keys"].([]any)
+	if assert.True(t, ok, profileReq) {
+		assert.Len(t, val, 2)
+	}
 	// set an invalid email
 	profileReq = make(map[string]any)
 	profileReq["email"] = "notavalidemail"
@@ -9644,6 +9693,8 @@ func TestWebAPIChangeUserProfileMock(t *testing.T) {
 	checkResponseCode(t, http.StatusBadRequest, rr)
 	assert.Contains(t, rr.Body.String(), "Validation error: could not parse key")
 
+	user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
+	assert.NoError(t, err)
 	user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled, sdk.WebClientPubKeyChangeDisabled}
 	user.Email = email
 	user.Description = description
@@ -9678,8 +9729,13 @@ func TestWebAPIChangeUserProfileMock(t *testing.T) {
 	assert.Equal(t, email, profileReq["email"].(string))
 	assert.Equal(t, description+"_mod", profileReq["description"].(string))
 	assert.True(t, profileReq["allow_api_key_auth"].(bool))
-	assert.Len(t, profileReq["public_keys"].([]any), 2)
+	val, ok = profileReq["public_keys"].([]any)
+	if assert.True(t, ok, profileReq) {
+		assert.Len(t, val, 2)
+	}
 
+	user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
+	assert.NoError(t, err)
 	user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled, sdk.WebClientInfoChangeDisabled}
 	user.Description = description + "_mod"
 	_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
@@ -10299,47 +10355,6 @@ func TestUserHandlingWithAPIKey(t *testing.T) {
 	assert.NoError(t, err)
 }
 
-func TestUpdateUserMock(t *testing.T) {
-	token, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
-	assert.NoError(t, err)
-	user := getTestUser()
-	userAsJSON := getUserAsJSON(t, user)
-	req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
-	setBearerForReq(req, token)
-	rr := executeRequest(req)
-	checkResponseCode(t, http.StatusCreated, rr)
-	err = render.DecodeJSON(rr.Body, &user)
-	assert.NoError(t, err)
-	// permissions should not change if empty or nil
-	permissions := user.Permissions
-	user.Permissions = make(map[string][]string)
-	userAsJSON = getUserAsJSON(t, user)
-	req, _ = http.NewRequest(http.MethodPut, userPath+"/"+user.Username, bytes.NewBuffer(userAsJSON))
-	setBearerForReq(req, token)
-	rr = executeRequest(req)
-	checkResponseCode(t, http.StatusOK, rr)
-	req, _ = http.NewRequest(http.MethodGet, userPath+"/"+user.Username, nil)
-	setBearerForReq(req, token)
-	rr = executeRequest(req)
-	checkResponseCode(t, http.StatusOK, rr)
-	var updatedUser dataprovider.User
-	err = render.DecodeJSON(rr.Body, &updatedUser)
-	assert.NoError(t, err)
-	for dir, perms := range permissions {
-		if actualPerms, ok := updatedUser.Permissions[dir]; ok {
-			for _, v := range actualPerms {
-				assert.True(t, util.Contains(perms, v))
-			}
-		} else {
-			assert.Fail(t, "Permissions directories mismatch")
-		}
-	}
-	req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)
-	setBearerForReq(req, token)
-	rr = executeRequest(req)
-	checkResponseCode(t, http.StatusOK, rr)
-}
-
 func TestUpdateUserQuotaUsageMock(t *testing.T) {
 	token, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
 	assert.NoError(t, err)

+ 1 - 23
internal/sftpd/sftpd_test.go

@@ -2668,29 +2668,7 @@ func TestLoginAfterUserUpdateEmptyPwd(t *testing.T) {
 	user, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)
 	assert.NoError(t, err)
 	user.Password = ""
-	user.PublicKeys = []string{}
-	// password and public key should remain unchanged
-	_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
-	assert.NoError(t, err)
-	conn, client, err := getSftpClient(user, usePubKey)
-	if assert.NoError(t, err) {
-		defer conn.Close()
-		defer client.Close()
-		assert.NoError(t, checkBasicSFTP(client))
-	}
-	_, err = httpdtest.RemoveUser(user, http.StatusOK)
-	assert.NoError(t, err)
-	err = os.RemoveAll(user.GetHomeDir())
-	assert.NoError(t, err)
-}
-
-func TestLoginAfterUserUpdateEmptyPubKey(t *testing.T) {
-	usePubKey := true
-	user, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)
-	assert.NoError(t, err)
-	user.Password = ""
-	user.PublicKeys = []string{}
-	// password and public key should remain unchanged
+	// password should remain unchanged
 	_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
 	assert.NoError(t, err)
 	conn, client, err := getSftpClient(user, usePubKey)