diff --git a/.travis.yml b/.travis.yml index 82b460cc..8beb780f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ env: - GO111MODULE=on before_script: - - sqlite3 sftpgo.db 'CREATE TABLE "users" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "username" varchar(255) NOT NULL UNIQUE, "password" varchar(255) NULL, "public_key" text NULL, "home_dir" varchar(255) NOT NULL, "uid" integer NOT NULL, "gid" integer NOT NULL, "max_sessions" integer NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL, "permissions" text NOT NULL, "used_quota_size" bigint NOT NULL, "used_quota_files" integer NOT NULL, "last_quota_update" bigint NOT NULL, "upload_bandwidth" integer NOT NULL, "download_bandwidth" integer NOT NULL);' + - sqlite3 sftpgo.db 'CREATE TABLE "users" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "username" varchar(255) NOT NULL UNIQUE, "password" varchar(255) NULL, "public_keys" text NULL, "home_dir" varchar(255) NOT NULL, "uid" integer NOT NULL, "gid" integer NOT NULL, "max_sessions" integer NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL, "permissions" text NOT NULL, "used_quota_size" bigint NOT NULL, "used_quota_files" integer NOT NULL, "last_quota_update" bigint NOT NULL, "upload_bandwidth" integer NOT NULL, "download_bandwidth" integer NOT NULL);' install: - go get -v -t ./... diff --git a/api/api_test.go b/api/api_test.go index 4589a89b..30ad443c 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -124,7 +124,7 @@ func TestBasicUserHandling(t *testing.T) { func TestAddUserNoCredentials(t *testing.T) { u := getTestUser() u.Password = "" - u.PublicKey = []string{} + u.PublicKeys = []string{} _, _, err := api.AddUser(u, http.StatusBadRequest) if err != nil { t.Errorf("unexpected error adding user with no credentials: %v", err) @@ -180,22 +180,22 @@ func TestUserPublicKey(t *testing.T) { u := getTestUser() invalidPubKey := "invalid" validPubKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0= nicola@p1" - u.PublicKey = []string{invalidPubKey} + u.PublicKeys = []string{invalidPubKey} _, _, err := api.AddUser(u, http.StatusBadRequest) if err != nil { t.Errorf("unexpected error adding user with invalid pub key: %v", err) } - u.PublicKey = []string{validPubKey} + u.PublicKeys = []string{validPubKey} user, _, err := api.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } - user.PublicKey = []string{validPubKey, invalidPubKey} + user.PublicKeys = []string{validPubKey, invalidPubKey} _, _, err = api.UpdateUser(user, http.StatusBadRequest) if err != nil { t.Errorf("update user with invalid public key must fail: %v", err) } - user.PublicKey = []string{validPubKey, validPubKey, validPubKey} + user.PublicKeys = []string{validPubKey, validPubKey, validPubKey} _, _, err = api.UpdateUser(user, http.StatusOK) if err != nil { t.Errorf("unable to update user: %v", err) @@ -236,7 +236,7 @@ func TestUpdateUserNoCredentials(t *testing.T) { t.Errorf("unable to add user: %v", err) } user.Password = "" - user.PublicKey = []string{} + user.PublicKeys = []string{} // password and public key will be omitted from json serialization if empty and so they will remain unchanged // and no validation error will be raised _, _, err = api.UpdateUser(user, http.StatusOK) diff --git a/api/api_utils.go b/api/api_utils.go index 3db54809..9e5f961a 100644 --- a/api/api_utils.go +++ b/api/api_utils.go @@ -257,8 +257,8 @@ func checkUser(expected dataprovider.User, actual dataprovider.User) error { if len(actual.Password) > 0 { return errors.New("User password must not be visible") } - if len(actual.PublicKey) > 0 { - return errors.New("User public key must not be visible") + if len(actual.PublicKeys) > 0 { + return errors.New("User public keys must not be visible") } if expected.ID <= 0 { if actual.ID <= 0 { diff --git a/api/internal_test.go b/api/internal_test.go index 430edc87..976db669 100644 --- a/api/internal_test.go +++ b/api/internal_test.go @@ -47,12 +47,12 @@ func TestCheckUser(t *testing.T) { t.Errorf("actual password must be nil") } actual.Password = "" - actual.PublicKey = []string{"pub key"} + actual.PublicKeys = []string{"pub key"} err = checkUser(expected, actual) if err == nil { t.Errorf("actual public key must be nil") } - actual.PublicKey = []string{} + actual.PublicKeys = []string{} err = checkUser(expected, actual) if err == nil { t.Errorf("actual ID must be > 0") diff --git a/api/schema/openapi.yaml b/api/schema/openapi.yaml index 9dd3fc98..8d1fde98 100644 --- a/api/schema/openapi.yaml +++ b/api/schema/openapi.yaml @@ -523,7 +523,7 @@ components: type: string nullable: true description: password or public key are mandatory. For security reasons this field is omitted when you search/get users - public_key: + public_keys: type: array items: type: string diff --git a/api/user.go b/api/user.go index e2071513..6516d334 100644 --- a/api/user.go +++ b/api/user.go @@ -65,7 +65,7 @@ func getUserByID(w http.ResponseWriter, r *http.Request) { user, err := dataprovider.GetUserByID(dataProvider, userID) if err == nil { user.Password = "" - user.PublicKey = []string{} + user.PublicKeys = []string{} render.JSON(w, r, user) } else if err == sql.ErrNoRows { sendAPIResponse(w, r, err, "", http.StatusNotFound) @@ -86,7 +86,7 @@ func addUser(w http.ResponseWriter, r *http.Request) { user, err = dataprovider.UserExists(dataProvider, user.Username) if err == nil { user.Password = "" - user.PublicKey = []string{} + user.PublicKeys = []string{} render.JSON(w, r, user) } else { sendAPIResponse(w, r, err, "", http.StatusInternalServerError) diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index 6f8b6c1a..0a92d95e 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -212,8 +212,8 @@ func validateUser(user *User) error { if len(user.Username) == 0 || len(user.HomeDir) == 0 { return &ValidationError{err: "Mandatory parameters missing"} } - if len(user.Password) == 0 && len(user.PublicKey) == 0 { - return &ValidationError{err: "Please set password or public_key"} + if len(user.Password) == 0 && len(user.PublicKeys) == 0 { + return &ValidationError{err: "Please set password or at least a public_key"} } if len(user.Permissions) == 0 { return &ValidationError{err: "Please grant some permissions to this user"} @@ -233,7 +233,7 @@ func validateUser(user *User) error { } user.Password = pwd } - for i, k := range user.PublicKey { + for i, k := range user.PublicKeys { _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k)) if err != nil { return &ValidationError{err: fmt.Sprintf("Could not parse key nr. %d: %s", i, err)} diff --git a/dataprovider/sqlcommon.go b/dataprovider/sqlcommon.go index b77e4ef2..31bf88f1 100644 --- a/dataprovider/sqlcommon.go +++ b/dataprovider/sqlcommon.go @@ -78,11 +78,11 @@ func sqlCommonValidateUserAndPubKey(username string, pubKey string) (User, error logger.Warn(logSender, "error authenticating user: %v, error: %v", username, err) return user, err } - if len(user.PublicKey) == 0 { + if len(user.PublicKeys) == 0 { return user, errors.New("Invalid credentials") } - for i, k := range user.PublicKey { + for i, k := range user.PublicKeys { storedPubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k)) if err != nil { logger.Warn(logSender, "error parsing stored public key %d for user %v: %v", i, username, err) @@ -242,7 +242,7 @@ func sqlCommonGetUsers(limit int, offset int, order string, username string) ([] u, err := getUserFromDbRow(nil, rows) // hide password and public key u.Password = "" - u.PublicKey = []string{} + u.PublicKeys = []string{} if err == nil { users = append(users, u) } else { @@ -280,13 +280,7 @@ func getUserFromDbRow(row *sql.Row, rows *sql.Rows) (User, error) { var list []string err = json.Unmarshal([]byte(publicKey.String), &list) if err == nil { - user.PublicKey = list - } else { - // compatibility layer: initially we store public keys as string newline delimited - // we need to remove this code in future - user.PublicKey = strings.Split(publicKey.String, "\n") - logger.Warn(logSender, "public keys loaded using compatibility mode, this will not work in future versions! "+ - "Number of public keys loaded: %v, username: %v", len(user.PublicKey), user.Username) + user.PublicKeys = list } } if permissions.Valid { diff --git a/dataprovider/sqlqueries.go b/dataprovider/sqlqueries.go index 4dd4d866..aceaf018 100644 --- a/dataprovider/sqlqueries.go +++ b/dataprovider/sqlqueries.go @@ -3,7 +3,7 @@ package dataprovider import "fmt" const ( - selectUserFields = "id,username,password,public_key,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions," + + selectUserFields = "id,username,password,public_keys,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions," + "used_quota_size,used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth" ) @@ -51,7 +51,7 @@ func getQuotaQuery() string { } func getAddUserQuery() string { - return fmt.Sprintf(`INSERT INTO %v (username,password,public_key,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions, + return fmt.Sprintf(`INSERT INTO %v (username,password,public_keys,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions, used_quota_size,used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth) VALUES (%v,%v,%v,%v,%v,%v,%v,%v,%v,%v,0,0,0,%v,%v)`, config.UsersTable, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], @@ -59,7 +59,7 @@ func getAddUserQuery() string { } func getUpdateUserQuery() string { - return fmt.Sprintf(`UPDATE %v SET password=%v,public_key=%v,home_dir=%v,uid=%v,gid=%v,max_sessions=%v,quota_size=%v, + return fmt.Sprintf(`UPDATE %v SET password=%v,public_keys=%v,home_dir=%v,uid=%v,gid=%v,max_sessions=%v,quota_size=%v, quota_files=%v,permissions=%v,upload_bandwidth=%v,download_bandwidth=%v WHERE id = %v`, config.UsersTable, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9], sqlPlaceholders[10], sqlPlaceholders[11]) diff --git a/dataprovider/user.go b/dataprovider/user.go index a89c4193..b39f5a63 100644 --- a/dataprovider/user.go +++ b/dataprovider/user.go @@ -39,8 +39,8 @@ type User struct { // Currently, as fallback, there is a clear text password checking but you should not store passwords // as clear text and this support could be removed at any time, so please don't depend on it. Password string `json:"password,omitempty"` - // PublicKey used for public key authentication. At least one between password and a public key is mandatory - PublicKey []string `json:"public_key,omitempty"` + // PublicKeys used for public key authentication. At least one between password and a public key is mandatory + PublicKeys []string `json:"public_keys,omitempty"` // The user cannot upload or download files outside this directory. Must be an absolute path HomeDir string `json:"home_dir"` // If sftpgo runs as root system user then the created files and directories will be assigned to this system UID @@ -82,7 +82,7 @@ func (u *User) GetPermissionsAsJSON() ([]byte, error) { // GetPublicKeysAsJSON returns the public keys as json byte array func (u *User) GetPublicKeysAsJSON() ([]byte, error) { - return json.Marshal(u.PublicKey) + return json.Marshal(u.PublicKeys) } // GetUID returns a validate uid, suitable for use with os.Chown diff --git a/scripts/sftpgo_api_cli.py b/scripts/sftpgo_api_cli.py old mode 100644 new mode 100755 index 624e4724..1fcb32cf --- a/scripts/sftpgo_api_cli.py +++ b/scripts/sftpgo_api_cli.py @@ -40,7 +40,7 @@ class SFTPGoApiRequests: else: print(r.text) - def buildUserObject(self, user_id=0, username="", password="", public_key="", home_dir="", uid=0, + def buildUserObject(self, user_id=0, username="", password="", public_keys="", home_dir="", uid=0, gid=0, max_sessions=0, quota_size=0, quota_files=0, permissions=[], upload_bandwidth=0, download_bandwidth=0): user = {"id":user_id, "username":username, "home_dir":home_dir, "uid":uid, "gid":gid, @@ -49,8 +49,8 @@ class SFTPGoApiRequests: "download_bandwidth":download_bandwidth} if password: user.update({"password":password}) - if public_key: - user.update({"public_key":public_key}) + if public_keys: + user.update({"public_keys":public_keys}) return user def getUsers(self, limit=100, offset=0, order="ASC", username=""): @@ -62,17 +62,17 @@ class SFTPGoApiRequests: r = requests.get(urlparse.urljoin(self.userPath, "user/" + str(user_id)), auth=self.auth, verify=self.verify) self.printResponse(r) - def addUser(self, username="", password="", public_key="", home_dir="", uid=0, gid=0, max_sessions=0, + def addUser(self, username="", password="", public_keys="", home_dir="", uid=0, gid=0, max_sessions=0, quota_size=0, quota_files=0, permissions=[], upload_bandwidth=0, download_bandwidth=0): - u = self.buildUserObject(0, username, password, public_key, home_dir, uid, gid, max_sessions, + u = self.buildUserObject(0, username, password, public_keys, home_dir, uid, gid, max_sessions, quota_size, quota_files, permissions, upload_bandwidth, download_bandwidth) r = requests.post(self.userPath, json=u, auth=self.auth, verify=self.verify) self.printResponse(r) - def updateUser(self, user_id, username="", password="", public_key="", home_dir="", uid=0, gid=0, + def updateUser(self, user_id, username="", password="", public_keys="", home_dir="", uid=0, gid=0, max_sessions=0, quota_size=0, quota_files=0, permissions=[], upload_bandwidth=0, download_bandwidth=0): - u = self.buildUserObject(user_id, username, password, public_key, home_dir, uid, gid, max_sessions, + u = self.buildUserObject(user_id, username, password, public_keys, home_dir, uid, gid, max_sessions, quota_size, quota_files, permissions, upload_bandwidth, download_bandwidth) r = requests.put(urlparse.urljoin(self.userPath, "user/" + str(user_id)), json=u, auth=self.auth, verify=self.verify) self.printResponse(r) @@ -102,7 +102,7 @@ class SFTPGoApiRequests: def addCommonUserArguments(parser): parser.add_argument('username', type=str) parser.add_argument('--password', type=str, default="", help="default: %(default)s") - parser.add_argument('--public_key', type=str, nargs='+', default=[], help="default: %(default)s") + parser.add_argument('--public_keys', type=str, nargs='+', default=[], help="default: %(default)s") parser.add_argument('--home_dir', type=str, default="", help="default: %(default)s") parser.add_argument('--uid', type=int, default=0, help="default: %(default)s") parser.add_argument('--gid', type=int, default=0, help="default: %(default)s") @@ -170,11 +170,11 @@ if __name__ == '__main__': api = SFTPGoApiRequests(args.debug, args.base_url, args.auth_type, args.auth_user, args.auth_password, args.verify) if args.command == "add_user": - api.addUser(args.username, args.password, args.public_key, args.home_dir, + api.addUser(args.username, args.password, args.public_keys, args.home_dir, args.uid, args.gid, args.max_sessions, args.quota_size, args.quota_files, args.permissions, args.upload_bandwidth, args.download_bandwidth) elif args.command == "update_user": - api.updateUser(args.id, args.username, args.password, args.public_key, args.home_dir, + api.updateUser(args.id, args.username, args.password, args.public_keys, args.home_dir, args.uid, args.gid, args.max_sessions, args.quota_size, args.quota_files, args.permissions, args.upload_bandwidth, args.download_bandwidth) elif args.command == "delete_user": diff --git a/sftpd/sftpd_test.go b/sftpd/sftpd_test.go index fbd82a5f..969060dc 100644 --- a/sftpd/sftpd_test.go +++ b/sftpd/sftpd_test.go @@ -499,7 +499,7 @@ func TestHomeSpecialChars(t *testing.T) { func TestLogin(t *testing.T) { u := getTestUser(false) - u.PublicKey = []string{testPubKey} + u.PublicKeys = []string{testPubKey} user, _, err := api.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) @@ -531,7 +531,7 @@ func TestLogin(t *testing.T) { defer client.Close() } // testPubKey1 is not authorized - user.PublicKey = []string{testPubKey1} + user.PublicKeys = []string{testPubKey1} user.Password = "" _, _, err = api.UpdateUser(user, http.StatusOK) if err != nil { @@ -543,7 +543,7 @@ func TestLogin(t *testing.T) { defer client.Close() } // login a user with multiple public keys, only the second one is valid - user.PublicKey = []string{testPubKey1, testPubKey} + user.PublicKeys = []string{testPubKey1, testPubKey} user.Password = "" _, _, err = api.UpdateUser(user, http.StatusOK) if err != nil { @@ -572,7 +572,7 @@ func TestLoginAfterUserUpdateEmptyPwd(t *testing.T) { t.Errorf("unable to add user: %v", err) } user.Password = "" - user.PublicKey = []string{} + user.PublicKeys = []string{} // password and public key should remain unchanged _, _, err = api.UpdateUser(user, http.StatusOK) if err != nil { @@ -605,7 +605,7 @@ func TestLoginAfterUserUpdateEmptyPubKey(t *testing.T) { t.Errorf("unable to add user: %v", err) } user.Password = "" - user.PublicKey = []string{} + user.PublicKeys = []string{} // password and public key should remain unchanged _, _, err = api.UpdateUser(user, http.StatusOK) if err != nil { @@ -1287,7 +1287,7 @@ func getTestUser(usePubKey bool) dataprovider.User { Permissions: allPerms, } if usePubKey { - user.PublicKey = []string{testPubKey} + user.PublicKeys = []string{testPubKey} user.Password = "" } return user diff --git a/sql/mysql/20190807.sql b/sql/mysql/20190807.sql new file mode 100644 index 00000000..5763ac37 --- /dev/null +++ b/sql/mysql/20190807.sql @@ -0,0 +1,6 @@ +BEGIN; +-- +-- Rename field public_key on user to public_keys +-- +ALTER TABLE `users` CHANGE `public_key` `public_keys` longtext NULL; +COMMIT; \ No newline at end of file diff --git a/sql/pgsql/20190807.sql b/sql/pgsql/20190807.sql new file mode 100644 index 00000000..0b81888e --- /dev/null +++ b/sql/pgsql/20190807.sql @@ -0,0 +1,6 @@ +BEGIN; +-- +-- Rename field public_key on user to public_keys +-- +ALTER TABLE "users" RENAME COLUMN "public_key" TO "public_keys"; +COMMIT; \ No newline at end of file diff --git a/sql/sqlite/20190807.sql b/sql/sqlite/20190807.sql new file mode 100644 index 00000000..0b81888e --- /dev/null +++ b/sql/sqlite/20190807.sql @@ -0,0 +1,6 @@ +BEGIN; +-- +-- Rename field public_key on user to public_keys +-- +ALTER TABLE "users" RENAME COLUMN "public_key" TO "public_keys"; +COMMIT; \ No newline at end of file