user API: allow to disable writes ...
... even if the user has permissions for these actions
This commit is contained in:
parent
85a47810ff
commit
83c7453957
7 changed files with 97 additions and 13 deletions
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}}
|
||||
|
|
Loading…
Reference in a new issue