user API: allow to disable writes ...

... even if the user has permissions for these actions
This commit is contained in:
Nicola Murino
2021-07-23 21:41:02 +02:00
parent 85a47810ff
commit 83c7453957
7 changed files with 97 additions and 13 deletions

View File

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

View File

@@ -373,7 +373,8 @@ func TestBasicUserHandling(t *testing.T) {
user.ExpirationDate = util.GetTimeAsMsSinceEpoch(time.Now()) user.ExpirationDate = util.GetTimeAsMsSinceEpoch(time.Now())
user.AdditionalInfo = "some free text" user.AdditionalInfo = "some free text"
user.Filters.TLSUsername = sdk.TLSUsernameCN 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 originalUser := user
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err) assert.NoError(t, err)
@@ -6352,6 +6353,86 @@ func TestWebAPIVFolder(t *testing.T) {
assert.NoError(t, err) 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) { func TestWebAPICryptFs(t *testing.T) {
u := getTestUser() u := getTestUser()
u.QuotaSize = 65535 u.QuotaSize = 65535

View File

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

View File

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

View File

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

View File

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

View File

@@ -538,7 +538,7 @@
</div> </div>
<div class="form-group row"> <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"> <div class="col-sm-10">
<select class="form-control" id="idWebClient" name="web_client_options" multiple> <select class="form-control" id="idWebClient" name="web_client_options" multiple>
{{range $option := .WebClientOptions}} {{range $option := .WebClientOptions}}