REST API dumpdata: allow to specify the resources to dump

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2023-04-18 18:11:23 +02:00
parent 54462c26f2
commit 712f2053a4
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
15 changed files with 363 additions and 91 deletions

View file

@ -308,19 +308,6 @@ jobs:
go build -trimpath -ldflags "-s -w" -o ipfilter
cd -
- name: Run tests using PostgreSQL provider
run: |
./sftpgo initprovider
./sftpgo resetprovider --force
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -covermode=atomic
env:
SFTPGO_DATA_PROVIDER__DRIVER: postgresql
SFTPGO_DATA_PROVIDER__NAME: sftpgo
SFTPGO_DATA_PROVIDER__HOST: localhost
SFTPGO_DATA_PROVIDER__PORT: 5432
SFTPGO_DATA_PROVIDER__USERNAME: postgres
SFTPGO_DATA_PROVIDER__PASSWORD: postgres
- name: Run tests using MySQL provider
run: |
./sftpgo initprovider
@ -334,6 +321,19 @@ jobs:
SFTPGO_DATA_PROVIDER__USERNAME: sftpgo
SFTPGO_DATA_PROVIDER__PASSWORD: sftpgo
- name: Run tests using PostgreSQL provider
run: |
./sftpgo initprovider
./sftpgo resetprovider --force
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -covermode=atomic
env:
SFTPGO_DATA_PROVIDER__DRIVER: postgresql
SFTPGO_DATA_PROVIDER__NAME: sftpgo
SFTPGO_DATA_PROVIDER__HOST: localhost
SFTPGO_DATA_PROVIDER__PORT: 5432
SFTPGO_DATA_PROVIDER__USERNAME: postgres
SFTPGO_DATA_PROVIDER__PASSWORD: postgres
- name: Run tests using MariaDB provider
run: |
./sftpgo initprovider

4
go.mod
View file

@ -38,7 +38,7 @@ require (
github.com/hashicorp/go-retryablehttp v0.7.2
github.com/jackc/pgx/v5 v5.3.2-0.20230411230705-2cf1541bb90a
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
github.com/klauspost/compress v1.16.4
github.com/klauspost/compress v1.16.5
github.com/lestrrat-go/jwx/v2 v2.0.9
github.com/lithammer/shortuuid/v3 v3.0.7
github.com/mattn/go-sqlite3 v1.14.16
@ -99,7 +99,7 @@ require (
github.com/aws/smithy-go v1.13.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect

7
go.sum
View file

@ -653,8 +653,9 @@ github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3k
github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
@ -1442,8 +1443,8 @@ github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdY
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.16.4 h1:91KN02FnsOYhuunwU4ssRe8lc2JosWmizWa91B5v1PU=
github.com/klauspost/compress v1.16.4/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=

View file

@ -631,12 +631,12 @@ func (p *EventParams) getStatusString() string {
// getUsers returns users with group settings not applied
func (p *EventParams) getUsers() ([]dataprovider.User, error) {
if p.sender == "" {
users, err := dataprovider.DumpUsers()
dump, err := dataprovider.DumpData([]string{dataprovider.DumpScopeUsers})
if err != nil {
eventManagerLog(logger.LevelError, "unable to get users: %+v", err)
return users, errors.New("unable to get users")
return nil, errors.New("unable to get users")
}
return users, nil
return dump.Users, nil
}
user, err := p.getUserFromSender()
if err != nil {
@ -668,7 +668,8 @@ func (p *EventParams) getUserFromSender() (dataprovider.User, error) {
func (p *EventParams) getFolders() ([]vfs.BaseVirtualFolder, error) {
if p.sender == "" {
return dataprovider.DumpFolders()
dump, err := dataprovider.DumpData([]string{dataprovider.DumpScopeFolders})
return dump.Folders, err
}
folder, err := dataprovider.GetFolderByName(p.sender)
if err != nil {

View file

@ -130,6 +130,21 @@ const (
protocolHTTP = "HTTP"
)
// Dump scopes
const (
DumpScopeUsers = "users"
DumpScopeFolders = "folders"
DumpScopeGroups = "groups"
DumpScopeAdmins = "admins"
DumpScopeAPIKeys = "api_keys"
DumpScopeShares = "shares"
DumpScopeActions = "actions"
DumpScopeRules = "rules"
DumpScopeRoles = "roles"
DumpScopeIPLists = "ip_lists"
DumpScopeConfigs = "configs"
)
var (
// SupportedProviders defines the supported data providers
SupportedProviders = []string{SQLiteDataProviderName, PGSQLDataProviderName, MySQLDataProviderName,
@ -541,7 +556,7 @@ func (c *Config) doBackup() (string, error) {
providerLog(logger.LevelError, "unable to create backup dir %q: %v", outputFile, err)
return outputFile, fmt.Errorf("unable to create backup dir: %w", err)
}
backup, err := DumpData()
backup, err := DumpData(nil)
if err != nil {
providerLog(logger.LevelError, "unable to execute backup: %v", err)
return outputFile, fmt.Errorf("unable to dump backup data: %w", err)
@ -2289,76 +2304,168 @@ func GetFolders(limit, offset int, order string, minimal bool) ([]vfs.BaseVirtua
return provider.getFolders(limit, offset, order, minimal)
}
// DumpUsers returns all users, including confidential data
func DumpUsers() ([]User, error) {
return provider.dumpUsers()
func dumpUsers(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeUsers) {
users, err := provider.dumpUsers()
if err != nil {
return err
}
data.Users = users
}
return nil
}
// DumpFolders returns all folders, including confidential data
func DumpFolders() ([]vfs.BaseVirtualFolder, error) {
return provider.dumpFolders()
func dumpFolders(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeFolders) {
folders, err := provider.dumpFolders()
if err != nil {
return err
}
data.Folders = folders
}
return nil
}
// DumpData returns all users, groups, folders, admins, api keys, shares, actions, rules
func DumpData() (BackupData, error) {
var data BackupData
groups, err := provider.dumpGroups()
if err != nil {
func dumpGroups(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeGroups) {
groups, err := provider.dumpGroups()
if err != nil {
return err
}
data.Groups = groups
}
return nil
}
func dumpAdmins(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeAdmins) {
admins, err := provider.dumpAdmins()
if err != nil {
return err
}
data.Admins = admins
}
return nil
}
func dumpAPIKeys(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeAPIKeys) {
apiKeys, err := provider.dumpAPIKeys()
if err != nil {
return err
}
data.APIKeys = apiKeys
}
return nil
}
func dumpShares(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeShares) {
shares, err := provider.dumpShares()
if err != nil {
return err
}
data.Shares = shares
}
return nil
}
func dumpActions(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeActions) {
actions, err := provider.dumpEventActions()
if err != nil {
return err
}
data.EventActions = actions
}
return nil
}
func dumpRules(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeRules) {
rules, err := provider.dumpEventRules()
if err != nil {
return err
}
data.EventRules = rules
}
return nil
}
func dumpRoles(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeRoles) {
roles, err := provider.dumpRoles()
if err != nil {
return err
}
data.Roles = roles
}
return nil
}
func dumpIPLists(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeIPLists) {
ipLists, err := provider.dumpIPListEntries()
if err != nil {
return err
}
data.IPLists = ipLists
}
return nil
}
func dumpConfigs(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeConfigs) {
configs, err := provider.getConfigs()
if err != nil {
return err
}
data.Configs = &configs
}
return nil
}
// DumpData returns a dump containing the requested scopes.
// Empty scopes means all
func DumpData(scopes []string) (BackupData, error) {
data := BackupData{
Version: DumpVersion,
}
if err := dumpGroups(&data, scopes); err != nil {
return data, err
}
users, err := provider.dumpUsers()
if err != nil {
if err := dumpUsers(&data, scopes); err != nil {
return data, err
}
folders, err := provider.dumpFolders()
if err != nil {
if err := dumpFolders(&data, scopes); err != nil {
return data, err
}
admins, err := provider.dumpAdmins()
if err != nil {
if err := dumpAdmins(&data, scopes); err != nil {
return data, err
}
apiKeys, err := provider.dumpAPIKeys()
if err != nil {
if err := dumpAPIKeys(&data, scopes); err != nil {
return data, err
}
shares, err := provider.dumpShares()
if err != nil {
if err := dumpShares(&data, scopes); err != nil {
return data, err
}
actions, err := provider.dumpEventActions()
if err != nil {
if err := dumpActions(&data, scopes); err != nil {
return data, err
}
rules, err := provider.dumpEventRules()
if err != nil {
if err := dumpRules(&data, scopes); err != nil {
return data, err
}
roles, err := provider.dumpRoles()
if err != nil {
if err := dumpRoles(&data, scopes); err != nil {
return data, err
}
ipLists, err := provider.dumpIPListEntries()
if err != nil {
if err := dumpIPLists(&data, scopes); err != nil {
return data, err
}
configs, err := provider.getConfigs()
if err != nil {
if err := dumpConfigs(&data, scopes); err != nil {
return data, err
}
data.Users = users
data.Groups = groups
data.Folders = folders
data.Admins = admins
data.APIKeys = apiKeys
data.Shares = shares
data.EventActions = actions
data.EventRules = rules
data.Roles = roles
data.IPLists = ipLists
data.Configs = &configs
data.Version = DumpVersion
return data, err
return data, nil
}
// ParseDumpData tries to parse data as BackupData

View file

@ -42,13 +42,15 @@ func getEventActions(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, actions)
}
func renderEventAction(w http.ResponseWriter, r *http.Request, name string, status int) {
func renderEventAction(w http.ResponseWriter, r *http.Request, name string, claims *jwtTokenClaims, status int) {
action, err := dataprovider.EventActionExists(name)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
action.PrepareForRendering()
if hideConfidentialData(claims, r) {
action.PrepareForRendering()
}
if status != http.StatusOK {
ctx := context.WithValue(r.Context(), render.StatusCtxKey, status)
render.JSON(w, r.WithContext(ctx), action)
@ -59,8 +61,13 @@ func renderEventAction(w http.ResponseWriter, r *http.Request, name string, stat
func getEventActionByName(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
return
}
name := getURLParam(r, "name")
renderEventAction(w, r, name, http.StatusOK)
renderEventAction(w, r, name, &claims, http.StatusOK)
}
func addEventAction(w http.ResponseWriter, r *http.Request) {
@ -84,7 +91,7 @@ func addEventAction(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Add("Location", fmt.Sprintf("%s/%s", eventActionsPath, url.PathEscape(action.Name)))
renderEventAction(w, r, action.Name, http.StatusCreated)
renderEventAction(w, r, action.Name, &claims, http.StatusCreated)
}
func updateEventAction(w http.ResponseWriter, r *http.Request) {
@ -158,13 +165,15 @@ func getEventRules(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, rules)
}
func renderEventRule(w http.ResponseWriter, r *http.Request, name string, status int) {
func renderEventRule(w http.ResponseWriter, r *http.Request, name string, claims *jwtTokenClaims, status int) {
rule, err := dataprovider.EventRuleExists(name)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
rule.PrepareForRendering()
if hideConfidentialData(claims, r) {
rule.PrepareForRendering()
}
if status != http.StatusOK {
ctx := context.WithValue(r.Context(), render.StatusCtxKey, status)
render.JSON(w, r.WithContext(ctx), rule)
@ -175,8 +184,13 @@ func renderEventRule(w http.ResponseWriter, r *http.Request, name string, status
func getEventRuleByName(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
return
}
name := getURLParam(r, "name")
renderEventRule(w, r, name, http.StatusOK)
renderEventRule(w, r, name, &claims, http.StatusOK)
}
func addEventRule(w http.ResponseWriter, r *http.Request) {
@ -199,7 +213,7 @@ func addEventRule(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Add("Location", fmt.Sprintf("%s/%s", eventRulesPath, url.PathEscape(rule.Name)))
renderEventRule(w, r, rule.Name, http.StatusCreated)
renderEventRule(w, r, rule.Name, &claims, http.StatusCreated)
}
func updateEventRule(w http.ResponseWriter, r *http.Request) {

View file

@ -62,7 +62,7 @@ func addFolder(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Add("Location", fmt.Sprintf("%s/%s", folderPath, url.PathEscape(folder.Name)))
renderFolder(w, r, folder.Name, http.StatusCreated)
renderFolder(w, r, folder.Name, &claims, http.StatusCreated)
}
func updateFolder(w http.ResponseWriter, r *http.Request) {
@ -103,13 +103,15 @@ func updateFolder(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, nil, "Folder updated", http.StatusOK)
}
func renderFolder(w http.ResponseWriter, r *http.Request, name string, status int) {
func renderFolder(w http.ResponseWriter, r *http.Request, name string, claims *jwtTokenClaims, status int) {
folder, err := dataprovider.GetFolderByName(name)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
folder.PrepareForRendering()
if hideConfidentialData(claims, r) {
folder.PrepareForRendering()
}
if status != http.StatusOK {
ctx := context.WithValue(r.Context(), render.StatusCtxKey, status)
render.JSON(w, r.WithContext(ctx), folder)
@ -120,8 +122,13 @@ func renderFolder(w http.ResponseWriter, r *http.Request, name string, status in
func getFolderByName(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
return
}
name := getURLParam(r, "name")
renderFolder(w, r, name, http.StatusOK)
renderFolder(w, r, name, &claims, http.StatusOK)
}
func deleteFolder(w http.ResponseWriter, r *http.Request) {

View file

@ -61,7 +61,7 @@ func addGroup(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Add("Location", fmt.Sprintf("%s/%s", groupPath, url.PathEscape(group.Name)))
renderGroup(w, r, group.Name, http.StatusCreated)
renderGroup(w, r, group.Name, &claims, http.StatusCreated)
}
func updateGroup(w http.ResponseWriter, r *http.Request) {
@ -111,13 +111,15 @@ func updateGroup(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, nil, "Group updated", http.StatusOK)
}
func renderGroup(w http.ResponseWriter, r *http.Request, name string, status int) {
func renderGroup(w http.ResponseWriter, r *http.Request, name string, claims *jwtTokenClaims, status int) {
group, err := dataprovider.GroupExists(name)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
group.PrepareForRendering()
if hideConfidentialData(claims, r) {
group.PrepareForRendering()
}
if status != http.StatusOK {
ctx := context.WithValue(r.Context(), render.StatusCtxKey, status)
render.JSON(w, r.WithContext(ctx), group)
@ -128,8 +130,13 @@ func renderGroup(w http.ResponseWriter, r *http.Request, name string, status int
func getGroupByName(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
return
}
name := getURLParam(r, "name")
renderGroup(w, r, name, http.StatusOK)
renderGroup(w, r, name, &claims, http.StatusOK)
}
func deleteGroup(w http.ResponseWriter, r *http.Request) {

View file

@ -51,6 +51,7 @@ func validateBackupFile(outputFile string) (string, error) {
func dumpData(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
var outputFile, outputData, indent string
var scopes []string
if _, ok := r.URL.Query()["output-file"]; ok {
outputFile = strings.TrimSpace(r.URL.Query().Get("output-file"))
}
@ -60,6 +61,9 @@ func dumpData(w http.ResponseWriter, r *http.Request) {
if _, ok := r.URL.Query()["indent"]; ok {
indent = strings.TrimSpace(r.URL.Query().Get("indent"))
}
if _, ok := r.URL.Query()["scopes"]; ok {
scopes = getCommaSeparatedQueryParam(r, "scopes")
}
if outputData != "1" {
var err error
@ -78,7 +82,7 @@ func dumpData(w http.ResponseWriter, r *http.Request) {
logger.Debug(logSender, "", "dumping data to: %q", outputFile)
}
backup, err := dataprovider.DumpData()
backup, err := dataprovider.DumpData(scopes)
if err != nil {
logger.Error(logSender, "", "dumping data error: %v, output file: %q", err, outputFile)
sendAPIResponse(w, r, err, "", getRespStatus(err))

View file

@ -62,16 +62,18 @@ func getUserByUsername(w http.ResponseWriter, r *http.Request) {
return
}
username := getURLParam(r, "username")
renderUser(w, r, username, claims.Role, http.StatusOK)
renderUser(w, r, username, &claims, http.StatusOK)
}
func renderUser(w http.ResponseWriter, r *http.Request, username, role string, status int) {
user, err := dataprovider.UserExists(username, role)
func renderUser(w http.ResponseWriter, r *http.Request, username string, claims *jwtTokenClaims, status int) {
user, err := dataprovider.UserExists(username, claims.Role)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
user.PrepareForRendering()
if hideConfidentialData(claims, r) {
user.PrepareForRendering()
}
if status != http.StatusOK {
ctx := context.WithValue(r.Context(), render.StatusCtxKey, status)
render.JSON(w, r.WithContext(ctx), user)
@ -116,7 +118,7 @@ func addUser(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Add("Location", fmt.Sprintf("%s/%s", userPath, url.PathEscape(user.Username)))
renderUser(w, r, user.Username, claims.Role, http.StatusCreated)
renderUser(w, r, user.Username, &claims, http.StatusCreated)
}
func disableUser2FA(w http.ResponseWriter, r *http.Request) {

View file

@ -771,3 +771,10 @@ func getProtocolFromRequest(r *http.Request) string {
}
return common.ProtocolHTTP
}
func hideConfidentialData(claims *jwtTokenClaims, r *http.Request) bool {
if !claims.hasPerm(dataprovider.PermAdminManageSystem) {
return true
}
return r.URL.Query().Get("confidential_data") != "1"
}

View file

@ -7972,6 +7972,22 @@ func TestLoaddata(t *testing.T) {
assert.Equal(t, int64(789), folder.LastQuotaUpdate)
assert.Equal(t, folderDesc, folder.Description)
assert.Len(t, folder.Users, 1)
response, _, err = httpdtest.Dumpdata("", "1", "0", http.StatusOK, dataprovider.DumpScopeUsers)
assert.NoError(t, err)
dumpedData = dataprovider.BackupData{}
data, err = json.Marshal(response)
assert.NoError(t, err)
err = json.Unmarshal(data, &dumpedData)
assert.NoError(t, err)
assert.Greater(t, len(dumpedData.Users), 0)
assert.Len(t, dumpedData.Admins, 0)
assert.Len(t, dumpedData.Folders, 0)
assert.Len(t, dumpedData.Groups, 0)
assert.Len(t, dumpedData.Roles, 0)
assert.Len(t, dumpedData.EventRules, 0)
assert.Len(t, dumpedData.EventActions, 0)
assert.Len(t, dumpedData.IPLists, 0)
_, err = httpdtest.RemoveFolder(folder, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)

View file

@ -600,6 +600,11 @@ func TestInvalidToken(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
rr = httptest.NewRecorder()
getFolderByName(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
rr = httptest.NewRecorder()
deleteFolder(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
@ -660,6 +665,11 @@ func TestInvalidToken(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
rr = httptest.NewRecorder()
getGroupByName(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
rr = httptest.NewRecorder()
deleteGroup(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
@ -670,6 +680,11 @@ func TestInvalidToken(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
rr = httptest.NewRecorder()
getEventActionByName(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
rr = httptest.NewRecorder()
updateEventAction(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
@ -680,6 +695,11 @@ func TestInvalidToken(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
rr = httptest.NewRecorder()
getEventRuleByName(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
rr = httptest.NewRecorder()
addEventRule(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
@ -1606,7 +1626,7 @@ func TestChangePwdValidationErrors(t *testing.T) {
func TestRenderUnexistingFolder(t *testing.T) {
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, folderPath, nil)
renderFolder(rr, req, "path not mapped", http.StatusOK)
renderFolder(rr, req, "path not mapped", &jwtTokenClaims{}, http.StatusOK)
assert.Equal(t, http.StatusNotFound, rr.Code)
}

View file

@ -1470,7 +1470,7 @@ func RemoveDefenderHostByIP(ip string, expectedStatusCode int) ([]byte, error) {
// Dumpdata requests a backup to outputFile.
// outputFile is relative to the configured backups_path
func Dumpdata(outputFile, outputData, indent string, expectedStatusCode int) (map[string]any, []byte, error) {
func Dumpdata(outputFile, outputData, indent string, expectedStatusCode int, scopes ...string) (map[string]any, []byte, error) {
var response map[string]any
var body []byte
url, err := url.Parse(buildURLRelativeToBase(dumpDataPath))
@ -1487,6 +1487,9 @@ func Dumpdata(outputFile, outputData, indent string, expectedStatusCode int) (ma
if indent != "" {
q.Add("indent", indent)
}
if len(scopes) > 0 {
q.Add("scopes", strings.Join(scopes, ","))
}
url.RawQuery = q.Encode()
resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "", getDefaultToken())
if err != nil {

View file

@ -1571,6 +1571,12 @@ paths:
summary: Add folder
operationId: add_folder
description: Adds a new folder. A quota scan is required to update the used files/size
parameters:
- in: query
name: confidential_data
schema:
type: integer
description: 'If set to 1 confidential data will not be hidden. This means that the response will contain the key and additional data for secrets. If a master key is not set or an external KMS is used, the data returned are enough to get the secrets in cleartext. Ignored if the manage_system permission is not granted.'
requestBody:
required: true
content:
@ -1613,6 +1619,12 @@ paths:
summary: Find folders by name
description: Returns the folder with the given name if it exists.
operationId: get_folder_by_name
parameters:
- in: query
name: confidential_data
schema:
type: integer
description: 'If set to 1 confidential data will not be hidden. This means that the response will contain the key and additional data for secrets. If a master key is not set or an external KMS is used, the data returned are enough to get the secrets in cleartext. Ignored if the manage_system permission is not granted.'
responses:
'200':
description: successful operation
@ -1751,6 +1763,12 @@ paths:
summary: Add group
operationId: add_group
description: Adds a new group
parameters:
- in: query
name: confidential_data
schema:
type: integer
description: 'If set to 1 confidential data will not be hidden. This means that the response will contain the key and additional data for secrets. If a master key is not set or an external KMS is used, the data returned are enough to get the secrets in cleartext. Ignored if the manage_system permission is not granted.'
requestBody:
required: true
content:
@ -1793,6 +1811,12 @@ paths:
summary: Find groups by name
description: Returns the group with the given name if it exists.
operationId: get_group_by_name
parameters:
- in: query
name: confidential_data
schema:
type: integer
description: 'If set to 1 confidential data will not be hidden. This means that the response will contain the key and additional data for secrets. If a master key is not set or an external KMS is used, the data returned are enough to get the secrets in cleartext. Ignored if the manage_system permission is not granted.'
responses:
'200':
description: successful operation
@ -2111,6 +2135,12 @@ paths:
summary: Add event action
operationId: add_event_action
description: Adds a new event actions
parameters:
- in: query
name: confidential_data
schema:
type: integer
description: 'If set to 1 confidential data will not be hidden. This means that the response will contain the key and additional data for secrets. If a master key is not set or an external KMS is used, the data returned are enough to get the secrets in cleartext. Ignored if the manage_system permission is not granted.'
requestBody:
required: true
content:
@ -2153,6 +2183,12 @@ paths:
summary: Find event actions by name
description: Returns the event action with the given name if it exists.
operationId: get_event_action_by_name
parameters:
- in: query
name: confidential_data
schema:
type: integer
description: 'If set to 1 confidential data will not be hidden. This means that the response will contain the key and additional data for secrets. If a master key is not set or an external KMS is used, the data returned are enough to get the secrets in cleartext. Ignored if the manage_system permission is not granted.'
responses:
'200':
description: successful operation
@ -2291,6 +2327,12 @@ paths:
summary: Add event rule
operationId: add_event_rule
description: Adds a new event rule
parameters:
- in: query
name: confidential_data
schema:
type: integer
description: 'If set to 1 confidential data will not be hidden. This means that the response will contain the key and additional data for secrets. If a master key is not set or an external KMS is used, the data returned are enough to get the secrets in cleartext. Ignored if the manage_system permission is not granted.'
requestBody:
required: true
content:
@ -2333,6 +2375,12 @@ paths:
summary: Find event rules by name
description: Returns the event rule with the given name if it exists.
operationId: get_event_rile_by_name
parameters:
- in: query
name: confidential_data
schema:
type: integer
description: 'If set to 1 confidential data will not be hidden. This means that the response will contain the key and additional data for secrets. If a master key is not set or an external KMS is used, the data returned are enough to get the secrets in cleartext. Ignored if the manage_system permission is not granted.'
responses:
'200':
description: successful operation
@ -3303,6 +3351,12 @@ paths:
summary: Add user
description: 'Adds a new user.Recovery codes and TOTP configuration cannot be set using this API: each user must use the specific APIs'
operationId: add_user
parameters:
- in: query
name: confidential_data
schema:
type: integer
description: 'If set to 1 confidential data will not be hidden. This means that the response will contain the hash of the password and the key and additional data for secrets. If a master key is not set or an external KMS is used, the data returned are enough to get the secrets in cleartext. Ignored if the manage_system permission is not granted.'
requestBody:
required: true
content:
@ -3345,6 +3399,12 @@ paths:
summary: Find users by username
description: Returns the user with the given username if it exists. For security reasons the hashed password is omitted in the response
operationId: get_user_by_username
parameters:
- in: query
name: confidential_data
schema:
type: integer
description: 'If set to 1 confidential data will not be hidden. This means that the response will contain the hash of the password and the key and additional data for secrets. If a master key is not set or an external KMS is used, the data returned are enough to get the secrets in cleartext. Ignored if the manage_system permission is not granted.'
responses:
'200':
description: successful operation
@ -3609,6 +3669,15 @@ paths:
indent:
* `0` no indentation. This is the default
* `1` format the output JSON
- in: query
name: scopes
schema:
type: array
items:
$ref: '#/components/schemas/DumpDataScopes'
description: 'You can limit the dump contents to the specified scopes. Empty or missing means any supported scope. Scopes must be specified comma separated'
explode: false
required: false
responses:
'200':
description: successful operation
@ -5056,6 +5125,20 @@ components:
- LDAPUser
- OSUser
description: This is an hint for authentication plugins. It is ignored when using SFTPGo internal authentication
DumpDataScopes:
type: string
enum:
- users
- folders
- groups
- admins
- api_keys
- shares
- actions
- rules
- roles
- ip_lists
- configs
FsEventStatus:
type: integer
enum: