add copy permission

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2024-02-20 18:19:09 +01:00
parent e5fc1bd574
commit 51ae2d7301
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
12 changed files with 95 additions and 21 deletions

View file

@ -37,7 +37,7 @@ SFTPGo supports the following built-in SSH commands:
- `scp`, SFTPGo implements the SCP protocol so we can support it for cloud filesystems too and we can avoid the other system commands limitations. SCP between two remote hosts is supported using the `-3` scp option. Wildcard expansion is not supported.
- `md5sum`, `sha1sum`, `sha256sum`, `sha384sum`, `sha512sum`. Useful to check message digests for uploaded files.
- `cd`, `pwd`. Some SFTP clients do not support the SFTP SSH_FXP_REALPATH packet type, so they use `cd` and `pwd` SSH commands to get the initial directory. Currently `cd` does nothing and `pwd` always returns the `/` path. These commands will work with any storage backend but keep in mind that to calculate the hash we need to read the whole file, for remote backends this means downloading the file, for the encrypted backend this means decrypting the file.
- `sftpgo-copy`. This is a built-in copy implementation. It allows server side copy for files and directories. The first argument is the source file/directory and the second one is the destination file/directory, for example `sftpgo-copy <src> <dst>`. :warning: Copying directories that span virtual folders is supported but, for Cloud Storage filesystems, the remote copy API is not currently used.
- `sftpgo-copy`. This is a built-in copy implementation. It allows server side copy for files and directories. The first argument is the source file/directory and the second one is the destination file/directory, for example `sftpgo-copy <src> <dst>`.
- `sftpgo-remove`. This is a built-in remove implementation. It allows to remove single files and to recursively remove directories. The first argument is the file/directory to remove, for example `sftpgo-remove <dst>`. Removing directories spanning virtual folders is not supported.
The following SSH commands are enabled by default:

8
go.mod
View file

@ -13,9 +13,9 @@ require (
github.com/aws/aws-sdk-go-v2/config v1.27.0
github.com/aws/aws-sdk-go-v2/credentials v1.17.0
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.0
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.1
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.2
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.20.1
github.com/aws/aws-sdk-go-v2/service/s3 v1.50.0
github.com/aws/aws-sdk-go-v2/service/s3 v1.50.1
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.27.1
github.com/aws/aws-sdk-go-v2/service/sts v1.27.0
github.com/bmatcuk/doublestar/v4 v4.6.1
@ -39,7 +39,7 @@ require (
github.com/jackc/pgx/v5 v5.5.3
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
github.com/klauspost/compress v1.17.6
github.com/lestrrat-go/jwx/v2 v2.0.19
github.com/lestrrat-go/jwx/v2 v2.0.20
github.com/lithammer/shortuuid/v3 v3.0.7
github.com/mattn/go-sqlite3 v1.14.22
github.com/mhale/smtpd v0.8.2
@ -144,7 +144,7 @@ require (
github.com/oklog/run v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
github.com/power-devops/perfstat v0.0.0-20240219145905-2259734c190a // indirect
github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.47.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect

16
go.sum
View file

@ -43,8 +43,8 @@ github.com/aws/aws-sdk-go-v2/credentials v1.17.0 h1:lMW2x6sKBsiAJrpi1doOXqWFyEPo
github.com/aws/aws-sdk-go-v2/credentials v1.17.0/go.mod h1:uT41FIH8cCIxOdUYIL0PYyHlL1NoneDuDSCwg5VE/5o=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.0 h1:xWCwjjvVz2ojYTP4kBKUuUh9ZrXfcAXpflhOUUeXg1k=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.0/go.mod h1:j3fACuqXg4oMTQOR2yY7m0NmJY0yBK4L4sLsRXq1Ins=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.1 h1:FqtJUSBgT2yfZ8kZhTi9AO131qMLOzb4MiH4riAM8XM=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.1/go.mod h1:G3V4qNUPMHKrXW/l149QXmHjf1vlMWBO4UuGPCK4a/c=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.2 h1:VEekE/fJWqAWYozxFQ07B+h8NdvTPAYhV13xIBenuO0=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.2/go.mod h1:8vozqAHmDNmoD4YbuDKIfpnLbByzngczL4My1RELLVo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.0 h1:NPs/EqVO+ajwOoq56EfcGKa3L3ruWuazkIw1BqxwOPw=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.0/go.mod h1:D+duLy2ylgatV+yTlQ8JTuLfDD0BnFvnQRc+o6tbZ4M=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.0 h1:ks7KGMVUMoDzcxNWUlEdI+/lokMFD136EL6DWmUOV80=
@ -63,8 +63,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.0 h1:l5puwOHr7IxECu
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.0/go.mod h1:Oov79flWa/n7Ni+lQC3z+VM7PoRM47omRqbJU9B5Y7E=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.20.1 h1:eHNChn4Sp+g1hdz4rkx96n1l/LpJEQLDuFB0V+fA/yg=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.20.1/go.mod h1:9ev55pJx9xNX3UAOKzZmbmaTbwwuLTCemOJPsd7rUz8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.50.0 h1:jZAdMD1ioZdqirzzVVRhpHHWJmcGGCn8JqDYBs5nmYA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.50.0/go.mod h1:1o/W6JFUuREj2ExoQ21vHJgO7wakvjhol91M9eknFgs=
github.com/aws/aws-sdk-go-v2/service/s3 v1.50.1 h1:bjpWJEXch7moIt3PX2r5XpGROsletl7enqG1Q3Te1Dc=
github.com/aws/aws-sdk-go-v2/service/s3 v1.50.1/go.mod h1:1o/W6JFUuREj2ExoQ21vHJgO7wakvjhol91M9eknFgs=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.27.1 h1:ss/HbHbONu0uscM549++4YanT6MnjNN0BGhE5pZRfG4=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.27.1/go.mod h1:JsJDZFHwLGZu6dxhV9EV1gJrMnCeE4GEXubSZA59xdA=
github.com/aws/aws-sdk-go-v2/service/sso v1.19.0 h1:u6OkVDxtBPnxPkZ9/63ynEe+8kHbtS5IfaC4PzVxzWM=
@ -270,8 +270,8 @@ github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJG
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx/v2 v2.0.19 h1:ekv1qEZE6BVct89QA+pRF6+4pCpfVrOnEJnTnT4RXoY=
github.com/lestrrat-go/jwx/v2 v2.0.19/go.mod h1:l3im3coce1lL2cDeAjqmaR+Awx+X8Ih+2k8BuHNJ4CU=
github.com/lestrrat-go/jwx/v2 v2.0.20 h1:sAgXuWS/t8ykxS9Bi2Qtn5Qhpakw1wrcjxChudjolCc=
github.com/lestrrat-go/jwx/v2 v2.0.20/go.mod h1:UlCSmKqw+agm5BsOBfEAbTvKsEApaGNqHAEUTv5PJC4=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
@ -322,8 +322,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig=
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20240219145905-2259734c190a h1:XCUtNgBnZfUBhdfCX2QK+fslr9vevSsUg3W3peZwlak=
github.com/power-devops/perfstat v0.0.0-20240219145905-2259734c190a/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=

View file

@ -612,6 +612,9 @@ func (c *BaseConnection) checkCopy(srcInfo, dstInfo os.FileInfo, virtualSource,
}
func (c *BaseConnection) copyFile(virtualSourcePath, virtualTargetPath string, srcSize int64) error {
if !c.User.HasPerm(dataprovider.PermCopy, virtualSourcePath) || !c.User.HasPerm(dataprovider.PermCopy, virtualTargetPath) {
return c.GetPermissionDeniedError()
}
if ok, _ := c.User.IsFileAllowed(virtualTargetPath); !ok {
return fmt.Errorf("file %q is not allowed: %w", virtualTargetPath, c.GetPermissionDeniedError())
}

View file

@ -8601,6 +8601,13 @@ func TestCopyAndRemovePermissions(t *testing.T) {
user.Permissions[testDir] = []string{dataprovider.PermListItems}
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
// no copy permission
out, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s %s`, testFileName, restrictedPath), user)
assert.Error(t, err, string(out))
user.Permissions[restrictedPath] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermCopy}
user.Permissions[testDir] = []string{dataprovider.PermListItems, dataprovider.PermCopy}
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
out, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s %s`, testFileName, restrictedPath), user)
assert.NoError(t, err, string(out))
// overwrite will fail, no permission

View file

@ -160,8 +160,8 @@ var (
BoltDataProviderName, MemoryDataProviderName, CockroachDataProviderName}
// ValidPerms defines all the valid permissions for a user
ValidPerms = []string{PermAny, PermListItems, PermDownload, PermUpload, PermOverwrite, PermCreateDirs, PermRename,
PermRenameFiles, PermRenameDirs, PermDelete, PermDeleteFiles, PermDeleteDirs, PermCreateSymlinks, PermChmod,
PermChown, PermChtimes}
PermRenameFiles, PermRenameDirs, PermDelete, PermDeleteFiles, PermDeleteDirs, PermCopy, PermCreateSymlinks,
PermChmod, PermChown, PermChtimes}
// ValidLoginMethods defines all the valid login methods
ValidLoginMethods = []string{SSHLoginMethodPublicKey, LoginMethodPassword, SSHLoginMethodPassword,
SSHLoginMethodKeyboardInteractive, SSHLoginMethodKeyAndPassword, SSHLoginMethodKeyAndKeyboardInt,

View file

@ -72,6 +72,8 @@ const (
PermChown = "chown"
// changing file or directory access and modification time is allowed
PermChtimes = "chtimes"
// copying files or directories is allowed
PermCopy = "copy"
)
// Available login methods
@ -1113,6 +1115,21 @@ func (u *User) CanDeleteFromWeb(target string) bool {
return u.HasAnyPerm(permsDeleteAny, target)
}
// CanCopyFromWeb returns true if the client can copy objects from the web UI.
// The specified src and dest are the source and target directories for the copy.
func (u *User) CanCopyFromWeb(src, dest string) bool {
if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
return false
}
if !u.HasPerm(PermListItems, src) {
return false
}
if !u.HasPerm(PermDownload, src) {
return false
}
return u.HasPerm(PermCopy, src) && u.HasPerm(PermCopy, dest)
}
// PasswordExpiresIn returns the number of days before the password expires.
// The returned value is negative if the password is expired.
// The caller must ensure that a PasswordExpiration is set

View file

@ -16223,6 +16223,21 @@ func TestRenameDifferentResource(t *testing.T) {
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "Cannot perform copy step")
u.Permissions = map[string][]string{
"/": {dataprovider.PermUpload, dataprovider.PermListItems, dataprovider.PermCreateDirs,
dataprovider.PermDownload, dataprovider.PermOverwrite, dataprovider.PermCopy},
}
_, resp, err = httpdtest.UpdateUser(u, http.StatusOK, "")
assert.NoError(t, err, string(resp))
webAPIToken, err = getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPost, userFileActionsPath+"/move?path="+testFileName+"&target="+url.QueryEscape(path.Join("/", "folderPath", testFileName)), nil)
assert.NoError(t, err)
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "Cannot perform remove step")
_, err = httpdtest.RemoveUser(user, http.StatusOK)

View file

@ -135,6 +135,7 @@ type filesPage struct {
CanDelete bool
CanDownload bool
CanShare bool
CanCopy bool
ShareUploadBaseURL string
Error *util.I18nError
Paths []dirMapping
@ -755,6 +756,7 @@ func (s *httpdServer) renderSharedFilesPage(w http.ResponseWriter, r *http.Reque
CanDelete: false,
CanDownload: share.Scope != dataprovider.ShareScopeWrite,
CanShare: false,
CanCopy: false,
Paths: getDirMapping(dirName, currentURL),
QuotaUsage: newUserQuotaUsage(&dataprovider.User{}),
}
@ -797,6 +799,7 @@ func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, di
CanDelete: user.CanDeleteFromWeb(dirName),
CanDownload: user.HasPerm(dataprovider.PermDownload, dirName),
CanShare: user.CanManageShares(),
CanCopy: user.CanCopyFromWeb(dirName, dirName),
ShareUploadBaseURL: "",
Paths: getDirMapping(dirName, webClientFilesPath),
QuotaUsage: newUserQuotaUsage(user),

View file

@ -9280,7 +9280,7 @@ func TestSSHCopyPermissions(t *testing.T) {
u := getTestUser(usePubKey)
u.Permissions["/dir1"] = []string{dataprovider.PermUpload, dataprovider.PermDownload, dataprovider.PermListItems}
u.Permissions["/dir2"] = []string{dataprovider.PermCreateDirs, dataprovider.PermUpload, dataprovider.PermDownload,
dataprovider.PermListItems}
dataprovider.PermListItems, dataprovider.PermCopy}
u.Permissions["/dir3"] = []string{dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermDownload,
dataprovider.PermListItems}
user, _, err := httpdtest.AddUser(u, http.StatusCreated)

View file

@ -4904,6 +4904,7 @@ components:
- chmod
- chown
- chtimes
- copy
description: |
Permissions:
* `*` - all permissions are granted
@ -4922,6 +4923,7 @@ components:
* `chmod` changing file or directory permissions is allowed
* `chown` changing file or directory owner and group is allowed
* `chtimes` changing file or directory access and modification time is allowed
* `copy`, copying files or directories is allowed
AdminPermissions:
type: string
enum:

View file

@ -17,6 +17,20 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- define "page_body"}}
{{- template "errmsg" .Error}}
{{- $move_copy_msg := ""}}
{{- if and .CanRename .CanCopy}}
{{- $move_copy_msg = "fs.move_copy"}}
{{- else}}
{{- if .CanRename}}
{{- $move_copy_msg = "fs.move.msg"}}
{{- else}}
{{- if .CanCopy}}
{{- $move_copy_msg = "fs.copy.msg"}}
{{- end}}
{{- end}}
{{- end}}
<div class="card card-flush shadow-sm">
<div class="card-header pt-8">
<div class="card-title">
@ -91,7 +105,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- if not .ShareUploadBaseURL}}
{{- if or .CanRename .CanAddFiles}}
<div class="menu-item px-3">
<a data-i18n="fs.move_copy" href="#" class="menu-link px-3 fs-6" data-kt-filemanager-table-select="move_or_copy_selected">
<a data-i18n="{{$move_copy_msg}}" href="#" class="menu-link px-3 fs-6" data-kt-filemanager-table-select="move_or_copy_selected">
Move or copy
</a>
</div>
@ -208,6 +222,19 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/vendor/glightbox/glightbox.min.js"></script>
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/vendor/pdfobject/pdfobject.min.js"></script>
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
//{{- $move_copy_msg := ""}}
//{{- if and .CanRename .CanCopy}}
//{{- $move_copy_msg = "fs.move_copy"}}
//{{- else}}
//{{- if .CanRename}}
//{{- $move_copy_msg = "fs.move.msg"}}
//{{- else}}
//{{- if .CanCopy}}
//{{- $move_copy_msg = "fs.copy.msg"}}
//{{- end}}
//{{- end}}
//{{- end}}
//{{- if not .ShareUploadBaseURL}}
const supportedEditExtensions = ["csv", "bat", "dyalog", "apl", "asc", "pgp", "sig", "asn", "asn1", "b", "bf",
"c", "h", "ino", "cpp", "c++", "cc", "cxx", "hpp", "h++", "hh", "hxx", "cob", "cpy", "cbl", "cs", "clj",
@ -673,9 +700,9 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<a data-i18n="general.rename" href="#" class="menu-link px-3" data-kt-filemanager-table-action="rename">Rename</a>
</div>
{{- end}}
{{- if or .CanRename .CanAddFiles}}
{{- if or .CanRename .CanCopy}}
<div class="menu-item px-3">
<a data-i18n="fs.move_copy" href="#" class="menu-link px-3" data-kt-filemanager-table-action="move_or_copy">Move or copy</a>
<a data-i18n="{{$move_copy_msg}}" href="#" class="menu-link px-3" data-kt-filemanager-table-action="move_or_copy">Move or copy</a>
</div>
{{- end}}
{{- if .CanShare}}
@ -2398,8 +2425,8 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div>
<div class="modal-footer border-0">
{{- if .CanAddFiles }}
<button id="id_copy_button" type="button" class="btn btn-light-primary me-5" data-bs-dismiss="modal">
{{- if .CanCopy }}
<button id="id_copy_button" type="button" class="btn {{if .CanRename}}btn-light-primary me-5{{else}}btn-primary{{end}}" data-bs-dismiss="modal">
<span data-i18n="fs.copy.msg">Copy</span>
</button>
{{- end}}