mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-07 23:00:55 +03:00
user API: allow to disable writes ...
... even if the user has permissions for these actions
This commit is contained in:
@@ -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"]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}}
|
||||||
|
|||||||
Reference in New Issue
Block a user