From 101c2962abfc69b0890e39b705169177de03991a Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Sun, 5 Sep 2021 18:49:13 +0200 Subject: [PATCH] web client UI: add a permission to disable password change Fixes #528 --- dataprovider/user.go | 5 ++++ docs/full-configuration.md | 33 +++++++++++++---------- httpd/httpd_test.go | 39 ++++++++++++++++++++++++++++ httpd/schema/openapi.yaml | 4 ++- httpd/server.go | 6 +++-- sdk/user.go | 10 ++++--- templates/webclient/credentials.html | 2 ++ 7 files changed, 79 insertions(+), 20 deletions(-) diff --git a/dataprovider/user.go b/dataprovider/user.go index 6238aa76..6087bf2a 100644 --- a/dataprovider/user.go +++ b/dataprovider/user.go @@ -719,6 +719,11 @@ func (u *User) CanManageMFA() bool { return len(mfa.GetAvailableTOTPConfigs()) > 0 } +// CanChangePassword returns true if this user is allowed to change its password +func (u *User) CanChangePassword() bool { + return !util.IsStringInSlice(sdk.WebClientPasswordChangeDisabled, u.Filters.WebClient) +} + // CanManagePublicKeys returns true if this user is allowed to manage public keys // from the web client. Used in web client UI func (u *User) CanManagePublicKeys() bool { diff --git a/docs/full-configuration.md b/docs/full-configuration.md index 295c206c..c5ac431c 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -304,19 +304,6 @@ then SFTPGo will try to create `id_rsa`, `id_ecdsa` and `id_ed25519`, if they ar The configuration can be read from JSON, TOML, YAML, HCL, envfile and Java properties config files. If your `config-file` flag is set to `sftpgo` (default value), you need to create a configuration file called `sftpgo.json` or `sftpgo.yaml` and so on inside `config-dir`. -## Binding to privileged ports - -On Linux, if you want to use Internet domain privileged ports (port numbers less than 1024) instead of running the SFTPGo service as root user you can set the `cap_net_bind_service` capability on the `sftpgo` binary. To set the capability you can use the following command: - -```shell -$ sudo setcap cap_net_bind_service=+ep /usr/bin/sftpgo -# Check that the capability is added: -$ getcap /usr/bin/sftpgo -/usr/bin/sftpgo cap_net_bind_service=ep -``` - -Now you can use privileged ports such as 21, 22, 443 etc.. without running the SFTPGo service as root user. You have to set the `cap_net_bind_service` capability each time you update the `sftpgo` binary. - ## Environment variables You can also override all the available configuration options using environment variables. SFTPGo will check for environment variables with a name matching the key uppercased and prefixed with the `SFTPGO_`. You need to use `__` to traverse a struct. @@ -335,6 +322,26 @@ You can select `sha256-simd` setting the environment variable `SFTPGO_MINIO_SHA2 `sha256-simd` is particularly useful if you have an Intel CPU with SHA extensions or an ARM CPU with Cryptography Extensions. +## Binding to privileged ports + +On Linux, if you want to use Internet domain privileged ports (port numbers less than 1024) instead of running the SFTPGo service as root user you can set the `cap_net_bind_service` capability on the `sftpgo` binary. To set the capability you can use the following command: + +```shell +$ sudo setcap cap_net_bind_service=+ep /usr/bin/sftpgo +# Check that the capability is added +$ getcap /usr/bin/sftpgo +/usr/bin/sftpgo cap_net_bind_service=ep +``` + +Now you can use privileged ports such as 21, 22, 443 etc.. without running the SFTPGo service as root user. You have to set the `cap_net_bind_service` capability each time you update the `sftpgo` binary. + +An alternative method is to use `iptables`, for example you run the SFTP service on port `2022` and redirect traffic from port `22` to port `2022`: + +```shell +sudo iptables -t nat -A PREROUTING -d -p tcp --dport 22 -m addrtype --dst-type LOCAL -j DNAT --to-destination :2022 +sudo iptables -t nat -A OUTPUT -d -p tcp --dport 22 -m addrtype --dst-type LOCAL -j DNAT --to-destination :2022 +``` + ## Telemetry Server The telemetry server exposes the following endpoints: diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index 2bbdf749..f80d6c95 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -5854,6 +5854,27 @@ func TestWebAPIChangeUserPwdMock(t *testing.T) { assert.NoError(t, err) assert.NotEmpty(t, token) + // remove the change password permission + user.Filters.WebClient = []string{sdk.WebClientPasswordChangeDisabled} + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") + assert.NoError(t, err) + assert.Len(t, user.Filters.WebClient, 1) + assert.Contains(t, user.Filters.WebClient, sdk.WebClientPasswordChangeDisabled) + + token, err = getJWTAPIUserTokenFromTestServer(defaultUsername, altAdminPassword) + assert.NoError(t, err) + assert.NotEmpty(t, token) + + pwd["current_password"] = altAdminPassword + pwd["new_password"] = defaultPassword + asJSON, err = json.Marshal(pwd) + assert.NoError(t, err) + req, err = http.NewRequest(http.MethodPut, userPwdPath, bytes.NewBuffer(asJSON)) + assert.NoError(t, err) + setBearerForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusForbidden, rr) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) @@ -7568,6 +7589,24 @@ func TestWebClientChangePwd(t *testing.T) { _, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword+"1") assert.NoError(t, err) + // remove the change password permission + user.Filters.WebClient = []string{sdk.WebClientPasswordChangeDisabled} + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") + assert.NoError(t, err) + assert.Len(t, user.Filters.WebClient, 1) + assert.Contains(t, user.Filters.WebClient, sdk.WebClientPasswordChangeDisabled) + + webToken, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword+"1") + assert.NoError(t, err) + form.Set("current_password", defaultPassword+"1") + form.Set("new_password1", defaultPassword) + form.Set("new_password2", defaultPassword) + req, _ = http.NewRequest(http.MethodPost, webChangeClientPwdPath, bytes.NewBuffer([]byte(form.Encode()))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + setJWTCookieForReq(req, webToken) + rr = executeRequest(req) + checkResponseCode(t, http.StatusForbidden, rr) + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) diff --git a/httpd/schema/openapi.yaml b/httpd/schema/openapi.yaml index 8051e719..a3d211a0 100644 --- a/httpd/schema/openapi.yaml +++ b/httpd/schema/openapi.yaml @@ -3025,11 +3025,13 @@ components: - publickey-change-disabled - write-disabled - mfa-disabled + - password-change-disabled description: | Options: * `publickey-change-disabled` - changing SSH public keys is not allowed * `write-disabled` - upload, rename, delete are not allowed even if the user has permissions for these actions - * `mfa-disabled` - the user cannot enable multi-factor authentication. This option cannot be set if the user has MFA already enabled + * `mfa-disabled` - enabling multi-factor authentication is not allowed. This option cannot be set if the user has MFA already enabled + * `password-change-disabled` - changing password is not allowed APIKeyScope: type: integer enum: diff --git a/httpd/server.go b/httpd/server.go index 4e63dbb0..fbd2a063 100644 --- a/httpd/server.go +++ b/httpd/server.go @@ -988,7 +988,8 @@ func (s *httpdServer) initializeRouter() { router.Use(jwtAuthenticatorAPIUser) router.With(forbidAPIKeyAuthentication).Get(userLogoutPath, s.logout) - router.With(forbidAPIKeyAuthentication).Put(userPwdPath, changeUserPassword) + router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)). + Put(userPwdPath, changeUserPassword) router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)). Get(userPublicKeysPath, getUserPublicKeys) router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)). @@ -1089,7 +1090,8 @@ func (s *httpdServer) initializeRouter() { Delete(webClientDirsPath, deleteUserDir) router.With(s.refreshCookie).Get(webClientDownloadZipPath, handleWebClientDownloadZip) router.With(s.refreshCookie).Get(webClientCredentialsPath, handleClientGetCredentials) - router.Post(webChangeClientPwdPath, handleWebClientChangePwdPost) + router.With(checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)). + Post(webChangeClientPwdPath, handleWebClientChangePwdPost) router.Post(webChangeClientAPIKeyAccessPath, handleWebClientManageAPIKeyPost) router.With(checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)). Post(webChangeClientKeysPath, handleWebClientManageKeysPost) diff --git a/sdk/user.go b/sdk/user.go index fd1b5a64..04781a52 100644 --- a/sdk/user.go +++ b/sdk/user.go @@ -9,14 +9,16 @@ import ( // Web Client/user REST API restrictions const ( - WebClientPubKeyChangeDisabled = "publickey-change-disabled" - WebClientWriteDisabled = "write-disabled" - WebClientMFADisabled = "mfa-disabled" + WebClientPubKeyChangeDisabled = "publickey-change-disabled" + WebClientWriteDisabled = "write-disabled" + WebClientMFADisabled = "mfa-disabled" + WebClientPasswordChangeDisabled = "password-change-disabled" ) var ( // WebClientOptions defines the available options for the web client interface/user REST API - WebClientOptions = []string{WebClientPubKeyChangeDisabled, WebClientWriteDisabled, WebClientMFADisabled} + WebClientOptions = []string{WebClientPubKeyChangeDisabled, WebClientWriteDisabled, WebClientMFADisabled, + WebClientPasswordChangeDisabled} // UserTypes defines the supported user type hints for auth plugins UserTypes = []string{string(UserTypeLDAP), string(UserTypeOS)} ) diff --git a/templates/webclient/credentials.html b/templates/webclient/credentials.html index 156bf272..287696ad 100644 --- a/templates/webclient/credentials.html +++ b/templates/webclient/credentials.html @@ -4,6 +4,7 @@ {{define "page_body"}} +{{if .LoggedUser.CanChangePassword}}
Change password
@@ -41,6 +42,7 @@
+{{end}} {{if .LoggedUser.CanManagePublicKeys}}