Browse Source

web client UI: add a permission to disable password change

Fixes #528
Nicola Murino 4 years ago
parent
commit
101c2962ab

+ 5 - 0
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 {

+ 20 - 13
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 <ip> -p tcp --dport 22 -m addrtype --dst-type LOCAL -j DNAT --to-destination <ip>:2022
+sudo iptables -t nat -A OUTPUT     -d <ip> -p tcp --dport 22 -m addrtype --dst-type LOCAL -j DNAT --to-destination <ip>:2022
+```
+
 ## Telemetry Server
 
 The telemetry server exposes the following endpoints:

+ 39 - 0
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())

+ 3 - 1
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:

+ 4 - 2
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)

+ 6 - 4
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)}
 )

+ 2 - 0
templates/webclient/credentials.html

@@ -4,6 +4,7 @@
 
 {{define "page_body"}}
 
+{{if .LoggedUser.CanChangePassword}}
 <div class="card shadow mb-4">
     <div class="card-header py-3">
         <h6 class="m-0 font-weight-bold text-primary">Change password</h6>
@@ -41,6 +42,7 @@
         </form>
     </div>
 </div>
+{{end}}
 {{if .LoggedUser.CanManagePublicKeys}}
 <div class="card shadow mb-4">
     <div class="card-header py-3">