Sfoglia il codice sorgente

user API: allow to disable writes ...

... even if the user has permissions for these actions
Nicola Murino 4 anni fa
parent
commit
83c7453957

+ 1 - 0
httpd/api_http_user.go

@@ -181,6 +181,7 @@ func uploadUserFiles(w http.ResponseWriter, r *http.Request) {
 		sendAPIResponse(w, r, err, "Unable to parse multipart form", http.StatusBadRequest)
 		return
 	}
+	defer r.MultipartForm.RemoveAll() //nolint:errcheck
 
 	parentDir := util.CleanPath(r.URL.Query().Get("path"))
 	files := r.MultipartForm.File["filename"]

+ 82 - 1
httpd/httpd_test.go

@@ -373,7 +373,8 @@ func TestBasicUserHandling(t *testing.T) {
 	user.ExpirationDate = util.GetTimeAsMsSinceEpoch(time.Now())
 	user.AdditionalInfo = "some free text"
 	user.Filters.TLSUsername = sdk.TLSUsernameCN
-	user.Filters.WebClient = append(user.Filters.WebClient, sdk.WebClientPubKeyChangeDisabled)
+	user.Filters.WebClient = append(user.Filters.WebClient, sdk.WebClientPubKeyChangeDisabled,
+		sdk.WebClientWriteDisabled)
 	originalUser := user
 	user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
 	assert.NoError(t, err)
@@ -6352,6 +6353,86 @@ func TestWebAPIVFolder(t *testing.T) {
 	assert.NoError(t, err)
 }
 
+func TestWebAPIWritePermission(t *testing.T) {
+	u := getTestUser()
+	u.Filters.WebClient = append(u.Filters.WebClient, sdk.WebClientWriteDisabled)
+	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
+	assert.NoError(t, err)
+	webAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
+	assert.NoError(t, err)
+
+	body := new(bytes.Buffer)
+	writer := multipart.NewWriter(body)
+	part, err := writer.CreateFormFile("filename", "file.txt")
+	assert.NoError(t, err)
+	_, err = part.Write([]byte(""))
+	assert.NoError(t, err)
+	err = writer.Close()
+	assert.NoError(t, err)
+	reader := bytes.NewReader(body.Bytes())
+
+	req, err := http.NewRequest(http.MethodPost, userFilePath, reader)
+	assert.NoError(t, err)
+	req.Header.Add("Content-Type", writer.FormDataContentType())
+	setBearerForReq(req, webAPIToken)
+	rr := executeRequest(req)
+	checkResponseCode(t, http.StatusForbidden, rr)
+
+	req, err = http.NewRequest(http.MethodPatch, userFilePath+"?path=a&target=b", nil)
+	assert.NoError(t, err)
+	req.Header.Add("Content-Type", writer.FormDataContentType())
+	setBearerForReq(req, webAPIToken)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusForbidden, rr)
+
+	req, err = http.NewRequest(http.MethodDelete, userFilePath+"?path=a", nil)
+	assert.NoError(t, err)
+	req.Header.Add("Content-Type", writer.FormDataContentType())
+	setBearerForReq(req, webAPIToken)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusForbidden, rr)
+
+	req, err = http.NewRequest(http.MethodGet, userFilePath+"?path=a.txt", nil)
+	assert.NoError(t, err)
+	req.Header.Add("Content-Type", writer.FormDataContentType())
+	setBearerForReq(req, webAPIToken)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusNotFound, rr)
+
+	req, err = http.NewRequest(http.MethodGet, userFolderPath, nil)
+	assert.NoError(t, err)
+	req.Header.Add("Content-Type", writer.FormDataContentType())
+	setBearerForReq(req, webAPIToken)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusOK, rr)
+
+	req, err = http.NewRequest(http.MethodPost, userFolderPath+"?path=dir", nil)
+	assert.NoError(t, err)
+	req.Header.Add("Content-Type", writer.FormDataContentType())
+	setBearerForReq(req, webAPIToken)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusForbidden, rr)
+
+	req, err = http.NewRequest(http.MethodPatch, userFolderPath+"?path=dir&target=dir1", nil)
+	assert.NoError(t, err)
+	req.Header.Add("Content-Type", writer.FormDataContentType())
+	setBearerForReq(req, webAPIToken)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusForbidden, rr)
+
+	req, err = http.NewRequest(http.MethodDelete, userFolderPath+"?path=dir", nil)
+	assert.NoError(t, err)
+	req.Header.Add("Content-Type", writer.FormDataContentType())
+	setBearerForReq(req, webAPIToken)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusForbidden, rr)
+
+	_, err = httpdtest.RemoveUser(user, http.StatusOK)
+	assert.NoError(t, err)
+	err = os.RemoveAll(user.GetHomeDir())
+	assert.NoError(t, err)
+}
+
 func TestWebAPICryptFs(t *testing.T) {
 	u := getTestUser()
 	u.QuotaSize = 65535

+ 0 - 1
httpd/middleware.go

@@ -123,7 +123,6 @@ func jwtAuthenticatorWebClient(next http.Handler) http.Handler {
 	})
 }
 
-//nolint:unparam
 func checkHTTPUserPerm(perm string) func(next http.Handler) http.Handler {
 	return func(next http.Handler) http.Handler {
 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

+ 3 - 1
httpd/schema/openapi.yaml

@@ -2228,9 +2228,11 @@ components:
       type: string
       enum:
         - publickey-change-disabled
+        - write-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
     PatternsFilter:
       type: object
       properties:
@@ -2321,7 +2323,7 @@ components:
           type: array
           items:
             $ref: '#/components/schemas/WebClientOptions'
-          description: WebClient related configuration options
+          description: WebClient/user REST API related configuration options
       description: Additional user options
     Secret:
       type: object

+ 6 - 6
httpd/server.go

@@ -632,13 +632,13 @@ func (s *httpdServer) initializeRouter() {
 		router.With(checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).Get(userPublicKeysPath, getUserPublicKeys)
 		router.With(checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).Put(userPublicKeysPath, setUserPublicKeys)
 		router.Get(userFolderPath, readUserFolder)
-		router.Post(userFolderPath, createUserDir)
-		router.Patch(userFolderPath, renameUserDir)
-		router.Delete(userFolderPath, deleteUserDir)
+		router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Post(userFolderPath, createUserDir)
+		router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Patch(userFolderPath, renameUserDir)
+		router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Delete(userFolderPath, deleteUserDir)
 		router.Get(userFilePath, getUserFile)
-		router.Post(userFilePath, uploadUserFiles)
-		router.Patch(userFilePath, renameUserFile)
-		router.Delete(userFilePath, deleteUserFile)
+		router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Post(userFilePath, uploadUserFiles)
+		router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Patch(userFilePath, renameUserFile)
+		router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Delete(userFilePath, deleteUserFile)
 		router.Post(userStreamZipPath, getUserFilesAsZipStream)
 	})
 

+ 4 - 3
sdk/user.go

@@ -6,14 +6,15 @@ import (
 	"github.com/drakkan/sftpgo/v2/util"
 )
 
-// Web Client restrictions
+// Web Client/user REST API restrictions
 const (
 	WebClientPubKeyChangeDisabled = "publickey-change-disabled"
+	WebClientWriteDisabled        = "write-disabled"
 )
 
 var (
-	// WebClientOptions defines the available options for the web client interface
-	WebClientOptions = []string{WebClientPubKeyChangeDisabled}
+	// WebClientOptions defines the available options for the web client interface/user REST API
+	WebClientOptions = []string{WebClientPubKeyChangeDisabled, WebClientWriteDisabled}
 )
 
 // TLSUsername defines the TLS certificate attribute to use as username

+ 1 - 1
templates/webadmin/user.html

@@ -538,7 +538,7 @@
             </div>
 
             <div class="form-group row">
-                <label for="idWebClient" class="col-sm-2 col-form-label">Web client</label>
+                <label for="idWebClient" class="col-sm-2 col-form-label">Web client/REST API</label>
                 <div class="col-sm-10">
                     <select class="form-control" id="idWebClient" name="web_client_options" multiple>
                         {{range $option := .WebClientOptions}}