diff --git a/dataprovider/admin.go b/dataprovider/admin.go index b116e59c..56fbaf26 100644 --- a/dataprovider/admin.go +++ b/dataprovider/admin.go @@ -86,36 +86,36 @@ func (a *Admin) checkPassword() error { func (a *Admin) validate() error { if a.Username == "" { - return &ValidationError{err: "username is mandatory"} + return utils.NewValidationError("username is mandatory") } if a.Password == "" { - return &ValidationError{err: "please set a password"} + return utils.NewValidationError("please set a password") } if !config.SkipNaturalKeysValidation && !usernameRegex.MatchString(a.Username) { - return &ValidationError{err: fmt.Sprintf("username %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~", a.Username)} + return utils.NewValidationError(fmt.Sprintf("username %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~", a.Username)) } if err := a.checkPassword(); err != nil { return err } a.Permissions = utils.RemoveDuplicates(a.Permissions) if len(a.Permissions) == 0 { - return &ValidationError{err: "please grant some permissions to this admin"} + return utils.NewValidationError("please grant some permissions to this admin") } if utils.IsStringInSlice(PermAdminAny, a.Permissions) { a.Permissions = []string{PermAdminAny} } for _, perm := range a.Permissions { if !utils.IsStringInSlice(perm, validAdminPerms) { - return &ValidationError{err: fmt.Sprintf("invalid permission: %#v", perm)} + return utils.NewValidationError(fmt.Sprintf("invalid permission: %#v", perm)) } } if a.Email != "" && !emailRegex.MatchString(a.Email) { - return &ValidationError{err: fmt.Sprintf("email %#v is not valid", a.Email)} + return utils.NewValidationError(fmt.Sprintf("email %#v is not valid", a.Email)) } for _, IPMask := range a.Filters.AllowList { _, _, err := net.ParseCIDR(IPMask) if err != nil { - return &ValidationError{err: fmt.Sprintf("could not parse allow list entry %#v : %v", IPMask, err)} + return utils.NewValidationError(fmt.Sprintf("could not parse allow list entry %#v : %v", IPMask, err)) } } diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index ab67af69..f7ba7917 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -366,23 +366,6 @@ type checkPasswordResponse struct { ToVerify string `json:"to_verify"` } -// ValidationError raised if input data is not valid -type ValidationError struct { - err string -} - -// Validation error details -func (e *ValidationError) Error() string { - return fmt.Sprintf("Validation error: %s", e.err) -} - -// NewValidationError returns a validation errors -func NewValidationError(error string) *ValidationError { - return &ValidationError{ - err: error, - } -} - // MethodDisabledError raised if a method is disabled in config file. // For example, if user management is disabled, this error is raised // every time a user operation is done using the REST API @@ -453,11 +436,6 @@ type Provider interface { revertDatabase(targetVersion int) error } -type fsValidatorHelper interface { - GetGCSCredentialsFilePath() string - GetEncrytionAdditionalData() string -} - // SetTempPath sets the path for temporary files func SetTempPath(fsPath string) { tempPath = fsPath @@ -1164,14 +1142,14 @@ func isMappedDirOverlapped(dir1, dir2 string, fullCheck bool) bool { func validateFolderQuotaLimits(folder vfs.VirtualFolder) error { if folder.QuotaSize < -1 { - return &ValidationError{err: fmt.Sprintf("invalid quota_size: %v folder path %#v", folder.QuotaSize, folder.MappedPath)} + return utils.NewValidationError(fmt.Sprintf("invalid quota_size: %v folder path %#v", folder.QuotaSize, folder.MappedPath)) } if folder.QuotaFiles < -1 { - return &ValidationError{err: fmt.Sprintf("invalid quota_file: %v folder path %#v", folder.QuotaFiles, folder.MappedPath)} + return utils.NewValidationError(fmt.Sprintf("invalid quota_file: %v folder path %#v", folder.QuotaFiles, folder.MappedPath)) } if (folder.QuotaSize == -1 && folder.QuotaFiles != -1) || (folder.QuotaFiles == -1 && folder.QuotaSize != -1) { - return &ValidationError{err: fmt.Sprintf("virtual folder quota_size and quota_files must be both -1 or >= 0, quota_size: %v quota_files: %v", - folder.QuotaFiles, folder.QuotaSize)} + return utils.NewValidationError(fmt.Sprintf("virtual folder quota_size and quota_files must be both -1 or >= 0, quota_size: %v quota_files: %v", + folder.QuotaFiles, folder.QuotaSize)) } return nil } @@ -1207,7 +1185,7 @@ func validateUserVirtualFolders(user *User) error { for _, v := range user.VirtualFolders { cleanedVPath := filepath.ToSlash(path.Clean(v.VirtualPath)) if !path.IsAbs(cleanedVPath) || cleanedVPath == "/" { - return &ValidationError{err: fmt.Sprintf("invalid virtual folder %#v", v.VirtualPath)} + return utils.NewValidationError(fmt.Sprintf("invalid virtual folder %#v", v.VirtualPath)) } if err := validateFolderQuotaLimits(v); err != nil { return err @@ -1219,21 +1197,21 @@ func validateUserVirtualFolders(user *User) error { cleanedMPath := folder.MappedPath if folder.IsLocalOrLocalCrypted() { if isMappedDirOverlapped(cleanedMPath, user.GetHomeDir(), true) { - return &ValidationError{err: fmt.Sprintf("invalid mapped folder %#v cannot be inside or contain the user home dir %#v", - folder.MappedPath, user.GetHomeDir())} + return utils.NewValidationError(fmt.Sprintf("invalid mapped folder %#v cannot be inside or contain the user home dir %#v", + folder.MappedPath, user.GetHomeDir())) } for mPath := range mappedPaths { if folder.IsLocalOrLocalCrypted() && isMappedDirOverlapped(mPath, cleanedMPath, false) { - return &ValidationError{err: fmt.Sprintf("invalid mapped folder %#v overlaps with mapped folder %#v", - v.MappedPath, mPath)} + return utils.NewValidationError(fmt.Sprintf("invalid mapped folder %#v overlaps with mapped folder %#v", + v.MappedPath, mPath)) } } mappedPaths[cleanedMPath] = true } for vPath := range virtualPaths { if isVirtualDirOverlapped(vPath, cleanedVPath, false) { - return &ValidationError{err: fmt.Sprintf("invalid virtual folder %#v overlaps with virtual folder %#v", - v.VirtualPath, vPath)} + return utils.NewValidationError(fmt.Sprintf("invalid virtual folder %#v overlaps with virtual folder %#v", + v.VirtualPath, vPath)) } } virtualPaths[cleanedVPath] = true @@ -1250,22 +1228,22 @@ func validateUserVirtualFolders(user *User) error { func validatePermissions(user *User) error { if len(user.Permissions) == 0 { - return &ValidationError{err: "please grant some permissions to this user"} + return utils.NewValidationError("please grant some permissions to this user") } permissions := make(map[string][]string) if _, ok := user.Permissions["/"]; !ok { - return &ValidationError{err: "permissions for the root dir \"/\" must be set"} + return utils.NewValidationError("permissions for the root dir \"/\" must be set") } for dir, perms := range user.Permissions { if len(perms) == 0 && dir == "/" { - return &ValidationError{err: fmt.Sprintf("no permissions granted for the directory: %#v", dir)} + return utils.NewValidationError(fmt.Sprintf("no permissions granted for the directory: %#v", dir)) } if len(perms) > len(ValidPerms) { - return &ValidationError{err: "invalid permissions"} + return utils.NewValidationError("invalid permissions") } for _, p := range perms { if !utils.IsStringInSlice(p, ValidPerms) { - return &ValidationError{err: fmt.Sprintf("invalid permission: %#v", p)} + return utils.NewValidationError(fmt.Sprintf("invalid permission: %#v", p)) } } cleanedDir := filepath.ToSlash(path.Clean(dir)) @@ -1273,10 +1251,10 @@ func validatePermissions(user *User) error { cleanedDir = strings.TrimSuffix(cleanedDir, "/") } if !path.IsAbs(cleanedDir) { - return &ValidationError{err: fmt.Sprintf("cannot set permissions for non absolute path: %#v", dir)} + return utils.NewValidationError(fmt.Sprintf("cannot set permissions for non absolute path: %#v", dir)) } if dir != cleanedDir && cleanedDir == "/" { - return &ValidationError{err: fmt.Sprintf("cannot set permissions for invalid subdirectory: %#v is an alias for \"/\"", dir)} + return utils.NewValidationError(fmt.Sprintf("cannot set permissions for invalid subdirectory: %#v is an alias for \"/\"", dir)) } if utils.IsStringInSlice(PermAny, perms) { permissions[cleanedDir] = []string{PermAny} @@ -1299,7 +1277,7 @@ func validatePublicKeys(user *User) error { } _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k)) if err != nil { - return &ValidationError{err: fmt.Sprintf("could not parse key nr. %d: %s", i+1, err)} + return utils.NewValidationError(fmt.Sprintf("could not parse key nr. %d: %s", i+1, err)) } validatedKeys = append(validatedKeys, k) } @@ -1317,13 +1295,13 @@ func validateFiltersPatternExtensions(user *User) error { for _, f := range user.Filters.FilePatterns { cleanedPath := filepath.ToSlash(path.Clean(f.Path)) if !path.IsAbs(cleanedPath) { - return &ValidationError{err: fmt.Sprintf("invalid path %#v for file patterns filter", f.Path)} + return utils.NewValidationError(fmt.Sprintf("invalid path %#v for file patterns filter", f.Path)) } if utils.IsStringInSlice(cleanedPath, filteredPaths) { - return &ValidationError{err: fmt.Sprintf("duplicate file patterns filter for path %#v", f.Path)} + return utils.NewValidationError(fmt.Sprintf("duplicate file patterns filter for path %#v", f.Path)) } if len(f.AllowedPatterns) == 0 && len(f.DeniedPatterns) == 0 { - return &ValidationError{err: fmt.Sprintf("empty file patterns filter for path %#v", f.Path)} + return utils.NewValidationError(fmt.Sprintf("empty file patterns filter for path %#v", f.Path)) } f.Path = cleanedPath allowed := make([]string, 0, len(f.AllowedPatterns)) @@ -1331,14 +1309,14 @@ func validateFiltersPatternExtensions(user *User) error { for _, pattern := range f.AllowedPatterns { _, err := path.Match(pattern, "abc") if err != nil { - return &ValidationError{err: fmt.Sprintf("invalid file pattern filter %#v", pattern)} + return utils.NewValidationError(fmt.Sprintf("invalid file pattern filter %#v", pattern)) } allowed = append(allowed, strings.ToLower(pattern)) } for _, pattern := range f.DeniedPatterns { _, err := path.Match(pattern, "abc") if err != nil { - return &ValidationError{err: fmt.Sprintf("invalid file pattern filter %#v", pattern)} + return utils.NewValidationError(fmt.Sprintf("invalid file pattern filter %#v", pattern)) } denied = append(denied, strings.ToLower(pattern)) } @@ -1371,45 +1349,45 @@ func validateFilters(user *User) error { for _, IPMask := range user.Filters.DeniedIP { _, _, err := net.ParseCIDR(IPMask) if err != nil { - return &ValidationError{err: fmt.Sprintf("could not parse denied IP/Mask %#v : %v", IPMask, err)} + return utils.NewValidationError(fmt.Sprintf("could not parse denied IP/Mask %#v : %v", IPMask, err)) } } for _, IPMask := range user.Filters.AllowedIP { _, _, err := net.ParseCIDR(IPMask) if err != nil { - return &ValidationError{err: fmt.Sprintf("could not parse allowed IP/Mask %#v : %v", IPMask, err)} + return utils.NewValidationError(fmt.Sprintf("could not parse allowed IP/Mask %#v : %v", IPMask, err)) } } if len(user.Filters.DeniedLoginMethods) >= len(ValidLoginMethods) { - return &ValidationError{err: "invalid denied_login_methods"} + return utils.NewValidationError("invalid denied_login_methods") } for _, loginMethod := range user.Filters.DeniedLoginMethods { if !utils.IsStringInSlice(loginMethod, ValidLoginMethods) { - return &ValidationError{err: fmt.Sprintf("invalid login method: %#v", loginMethod)} + return utils.NewValidationError(fmt.Sprintf("invalid login method: %#v", loginMethod)) } } if len(user.Filters.DeniedProtocols) >= len(ValidProtocols) { - return &ValidationError{err: "invalid denied_protocols"} + return utils.NewValidationError("invalid denied_protocols") } for _, p := range user.Filters.DeniedProtocols { if !utils.IsStringInSlice(p, ValidProtocols) { - return &ValidationError{err: fmt.Sprintf("invalid protocol: %#v", p)} + return utils.NewValidationError(fmt.Sprintf("invalid protocol: %#v", p)) } } if user.Filters.TLSUsername != "" { if !utils.IsStringInSlice(string(user.Filters.TLSUsername), validTLSUsernames) { - return &ValidationError{err: fmt.Sprintf("invalid TLS username: %#v", user.Filters.TLSUsername)} + return utils.NewValidationError(fmt.Sprintf("invalid TLS username: %#v", user.Filters.TLSUsername)) } } for _, opts := range user.Filters.WebClient { if !utils.IsStringInSlice(opts, WebClientOptions) { - return &ValidationError{err: fmt.Sprintf("invalid web client options %#v", opts)} + return utils.NewValidationError(fmt.Sprintf("invalid web client options %#v", opts)) } } return validateFiltersPatternExtensions(user) } -func saveGCSCredentials(fsConfig *vfs.Filesystem, helper fsValidatorHelper) error { +func saveGCSCredentials(fsConfig *vfs.Filesystem, helper vfs.ValidatorHelper) error { if fsConfig.Provider != vfs.GCSFilesystemProvider { return nil } @@ -1418,7 +1396,7 @@ func saveGCSCredentials(fsConfig *vfs.Filesystem, helper fsValidatorHelper) erro } if config.PreferDatabaseCredentials { if fsConfig.GCSConfig.Credentials.IsPlain() { - fsConfig.GCSConfig.Credentials.SetAdditionalData(helper.GetEncrytionAdditionalData()) + fsConfig.GCSConfig.Credentials.SetAdditionalData(helper.GetEncryptionAdditionalData()) err := fsConfig.GCSConfig.Credentials.Encrypt() if err != nil { return err @@ -1427,113 +1405,45 @@ func saveGCSCredentials(fsConfig *vfs.Filesystem, helper fsValidatorHelper) erro return nil } if fsConfig.GCSConfig.Credentials.IsPlain() { - fsConfig.GCSConfig.Credentials.SetAdditionalData(helper.GetEncrytionAdditionalData()) + fsConfig.GCSConfig.Credentials.SetAdditionalData(helper.GetEncryptionAdditionalData()) err := fsConfig.GCSConfig.Credentials.Encrypt() if err != nil { - return &ValidationError{err: fmt.Sprintf("could not encrypt GCS credentials: %v", err)} + return utils.NewValidationError(fmt.Sprintf("could not encrypt GCS credentials: %v", err)) } } creds, err := json.Marshal(fsConfig.GCSConfig.Credentials) if err != nil { - return &ValidationError{err: fmt.Sprintf("could not marshal GCS credentials: %v", err)} + return utils.NewValidationError(fmt.Sprintf("could not marshal GCS credentials: %v", err)) } credentialsFilePath := helper.GetGCSCredentialsFilePath() err = os.MkdirAll(filepath.Dir(credentialsFilePath), 0700) if err != nil { - return &ValidationError{err: fmt.Sprintf("could not create GCS credentials dir: %v", err)} + return utils.NewValidationError(fmt.Sprintf("could not create GCS credentials dir: %v", err)) } err = os.WriteFile(credentialsFilePath, creds, 0600) if err != nil { - return &ValidationError{err: fmt.Sprintf("could not save GCS credentials: %v", err)} + return utils.NewValidationError(fmt.Sprintf("could not save GCS credentials: %v", err)) } fsConfig.GCSConfig.Credentials = kms.NewEmptySecret() return nil } -func validateFilesystemConfig(fsConfig *vfs.Filesystem, helper fsValidatorHelper) error { - if fsConfig.Provider == vfs.S3FilesystemProvider { - if err := fsConfig.S3Config.Validate(); err != nil { - return &ValidationError{err: fmt.Sprintf("could not validate s3config: %v", err)} - } - if err := fsConfig.S3Config.EncryptCredentials(helper.GetEncrytionAdditionalData()); err != nil { - return &ValidationError{err: fmt.Sprintf("could not encrypt s3 access secret: %v", err)} - } - fsConfig.GCSConfig = vfs.GCSFsConfig{} - fsConfig.AzBlobConfig = vfs.AzBlobFsConfig{} - fsConfig.CryptConfig = vfs.CryptFsConfig{} - fsConfig.SFTPConfig = vfs.SFTPFsConfig{} - return nil - } else if fsConfig.Provider == vfs.GCSFilesystemProvider { - if err := fsConfig.GCSConfig.Validate(helper.GetGCSCredentialsFilePath()); err != nil { - return &ValidationError{err: fmt.Sprintf("could not validate GCS config: %v", err)} - } - fsConfig.S3Config = vfs.S3FsConfig{} - fsConfig.AzBlobConfig = vfs.AzBlobFsConfig{} - fsConfig.CryptConfig = vfs.CryptFsConfig{} - fsConfig.SFTPConfig = vfs.SFTPFsConfig{} - return nil - } else if fsConfig.Provider == vfs.AzureBlobFilesystemProvider { - if err := fsConfig.AzBlobConfig.Validate(); err != nil { - return &ValidationError{err: fmt.Sprintf("could not validate Azure Blob config: %v", err)} - } - if err := fsConfig.AzBlobConfig.EncryptCredentials(helper.GetEncrytionAdditionalData()); err != nil { - return &ValidationError{err: fmt.Sprintf("could not encrypt Azure blob account key: %v", err)} - } - fsConfig.S3Config = vfs.S3FsConfig{} - fsConfig.GCSConfig = vfs.GCSFsConfig{} - fsConfig.CryptConfig = vfs.CryptFsConfig{} - fsConfig.SFTPConfig = vfs.SFTPFsConfig{} - return nil - } else if fsConfig.Provider == vfs.CryptedFilesystemProvider { - if err := fsConfig.CryptConfig.Validate(); err != nil { - return &ValidationError{err: fmt.Sprintf("could not validate Crypt fs config: %v", err)} - } - if err := fsConfig.CryptConfig.EncryptCredentials(helper.GetEncrytionAdditionalData()); err != nil { - return &ValidationError{err: fmt.Sprintf("could not encrypt Crypt fs passphrase: %v", err)} - } - fsConfig.S3Config = vfs.S3FsConfig{} - fsConfig.GCSConfig = vfs.GCSFsConfig{} - fsConfig.AzBlobConfig = vfs.AzBlobFsConfig{} - fsConfig.SFTPConfig = vfs.SFTPFsConfig{} - return nil - } else if fsConfig.Provider == vfs.SFTPFilesystemProvider { - if err := fsConfig.SFTPConfig.Validate(); err != nil { - return &ValidationError{err: fmt.Sprintf("could not validate SFTP fs config: %v", err)} - } - if err := fsConfig.SFTPConfig.EncryptCredentials(helper.GetEncrytionAdditionalData()); err != nil { - return &ValidationError{err: fmt.Sprintf("could not encrypt SFTP fs credentials: %v", err)} - } - fsConfig.S3Config = vfs.S3FsConfig{} - fsConfig.GCSConfig = vfs.GCSFsConfig{} - fsConfig.AzBlobConfig = vfs.AzBlobFsConfig{} - fsConfig.CryptConfig = vfs.CryptFsConfig{} - return nil - } - fsConfig.Provider = vfs.LocalFilesystemProvider - fsConfig.S3Config = vfs.S3FsConfig{} - fsConfig.GCSConfig = vfs.GCSFsConfig{} - fsConfig.AzBlobConfig = vfs.AzBlobFsConfig{} - fsConfig.CryptConfig = vfs.CryptFsConfig{} - fsConfig.SFTPConfig = vfs.SFTPFsConfig{} - return nil -} - func validateBaseParams(user *User) error { if user.Username == "" { - return &ValidationError{err: "username is mandatory"} + return utils.NewValidationError("username is mandatory") } if !config.SkipNaturalKeysValidation && !usernameRegex.MatchString(user.Username) { - return &ValidationError{err: fmt.Sprintf("username %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~", - user.Username)} + return utils.NewValidationError(fmt.Sprintf("username %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~", + user.Username)) } if user.HomeDir == "" { - return &ValidationError{err: "home_dir is mandatory"} + return utils.NewValidationError("home_dir is mandatory") } if user.Password == "" && len(user.PublicKeys) == 0 { - return &ValidationError{err: "please set a password or at least a public_key"} + return utils.NewValidationError("please set a password or at least a public_key") } if !filepath.IsAbs(user.HomeDir) { - return &ValidationError{err: fmt.Sprintf("home_dir must be an absolute path, actual value: %v", user.HomeDir)} + return utils.NewValidationError(fmt.Sprintf("home_dir must be an absolute path, actual value: %v", user.HomeDir)) } return nil } @@ -1561,24 +1471,24 @@ func createUserPasswordHash(user *User) error { // FIXME: this should be defined as Folder struct method func ValidateFolder(folder *vfs.BaseVirtualFolder) error { if folder.Name == "" { - return &ValidationError{err: "folder name is mandatory"} + return utils.NewValidationError("folder name is mandatory") } if !config.SkipNaturalKeysValidation && !usernameRegex.MatchString(folder.Name) { - return &ValidationError{err: fmt.Sprintf("folder name %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~", - folder.Name)} + return utils.NewValidationError(fmt.Sprintf("folder name %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~", + folder.Name)) } if folder.FsConfig.Provider == vfs.LocalFilesystemProvider || folder.FsConfig.Provider == vfs.CryptedFilesystemProvider || folder.MappedPath != "" { cleanedMPath := filepath.Clean(folder.MappedPath) if !filepath.IsAbs(cleanedMPath) { - return &ValidationError{err: fmt.Sprintf("invalid folder mapped path %#v", folder.MappedPath)} + return utils.NewValidationError(fmt.Sprintf("invalid folder mapped path %#v", folder.MappedPath)) } folder.MappedPath = cleanedMPath } if folder.HasRedactedSecret() { return errors.New("cannot save a folder with a redacted secret") } - if err := validateFilesystemConfig(&folder.FsConfig, folder); err != nil { + if err := folder.FsConfig.Validate(folder); err != nil { return err } return saveGCSCredentials(&folder.FsConfig, folder) @@ -1598,14 +1508,14 @@ func ValidateUser(user *User) error { if user.hasRedactedSecret() { return errors.New("cannot save a user with a redacted secret") } - if err := validateFilesystemConfig(&user.FsConfig, user); err != nil { + if err := user.FsConfig.Validate(user); err != nil { return err } if err := validateUserVirtualFolders(user); err != nil { return err } if user.Status < 0 || user.Status > 1 { - return &ValidationError{err: fmt.Sprintf("invalid user status: %v", user.Status)} + return utils.NewValidationError(fmt.Sprintf("invalid user status: %v", user.Status)) } if err := createUserPasswordHash(user); err != nil { return err diff --git a/dataprovider/user.go b/dataprovider/user.go index a1789a01..eb85b15c 100644 --- a/dataprovider/user.go +++ b/dataprovider/user.go @@ -342,20 +342,7 @@ func (u *User) isFsEqual(other *User) bool { // hideConfidentialData hides user confidential data func (u *User) hideConfidentialData() { u.Password = "" - switch u.FsConfig.Provider { - case vfs.S3FilesystemProvider: - u.FsConfig.S3Config.AccessSecret.Hide() - case vfs.GCSFilesystemProvider: - u.FsConfig.GCSConfig.Credentials.Hide() - case vfs.AzureBlobFilesystemProvider: - u.FsConfig.AzBlobConfig.AccountKey.Hide() - u.FsConfig.AzBlobConfig.SASURL.Hide() - case vfs.CryptedFilesystemProvider: - u.FsConfig.CryptConfig.Passphrase.Hide() - case vfs.SFTPFilesystemProvider: - u.FsConfig.SFTPConfig.Password.Hide() - u.FsConfig.SFTPConfig.PrivateKey.Hide() - } + u.FsConfig.HideConfidentialData() } // GetSubDirPermissions returns permissions for sub directories @@ -387,33 +374,8 @@ func (u *User) PrepareForRendering() { } func (u *User) hasRedactedSecret() bool { - switch u.FsConfig.Provider { - case vfs.S3FilesystemProvider: - if u.FsConfig.S3Config.AccessSecret.IsRedacted() { - return true - } - case vfs.GCSFilesystemProvider: - if u.FsConfig.GCSConfig.Credentials.IsRedacted() { - return true - } - case vfs.AzureBlobFilesystemProvider: - if u.FsConfig.AzBlobConfig.AccountKey.IsRedacted() { - return true - } - if u.FsConfig.AzBlobConfig.SASURL.IsRedacted() { - return true - } - case vfs.CryptedFilesystemProvider: - if u.FsConfig.CryptConfig.Passphrase.IsRedacted() { - return true - } - case vfs.SFTPFilesystemProvider: - if u.FsConfig.SFTPConfig.Password.IsRedacted() { - return true - } - if u.FsConfig.SFTPConfig.PrivateKey.IsRedacted() { - return true - } + if u.FsConfig.HasRedactedSecret() { + return true } for idx := range u.VirtualFolders { @@ -1189,8 +1151,8 @@ func (u *User) getNotificationFieldsAsSlice(action string) []string { } } -// GetEncrytionAdditionalData returns the additional data to use for AEAD -func (u *User) GetEncrytionAdditionalData() string { +// GetEncryptionAdditionalData returns the additional data to use for AEAD +func (u *User) GetEncryptionAdditionalData() string { return u.Username } diff --git a/httpd/api_admin.go b/httpd/api_admin.go index a64c9742..7cab4d01 100644 --- a/httpd/api_admin.go +++ b/httpd/api_admin.go @@ -9,6 +9,7 @@ import ( "github.com/go-chi/render" "github.com/drakkan/sftpgo/dataprovider" + "github.com/drakkan/sftpgo/utils" ) func getAdmins(w http.ResponseWriter, r *http.Request) { @@ -140,13 +141,13 @@ func changeAdminPassword(w http.ResponseWriter, r *http.Request) { func doChangeAdminPassword(r *http.Request, currentPassword, newPassword, confirmNewPassword string) error { if currentPassword == "" || newPassword == "" || confirmNewPassword == "" { - return dataprovider.NewValidationError("please provide the current password and the new one two times") + return utils.NewValidationError("please provide the current password and the new one two times") } if newPassword != confirmNewPassword { - return dataprovider.NewValidationError("the two password fields do not match") + return utils.NewValidationError("the two password fields do not match") } if currentPassword == newPassword { - return dataprovider.NewValidationError("the new password must be different from the current one") + return utils.NewValidationError("the new password must be different from the current one") } claims, err := getTokenClaims(r) if err != nil { @@ -158,7 +159,7 @@ func doChangeAdminPassword(r *http.Request, currentPassword, newPassword, confir } match, err := admin.CheckPassword(currentPassword) if !match || err != nil { - return dataprovider.NewValidationError("current password does not match") + return utils.NewValidationError("current password does not match") } admin.Password = newPassword diff --git a/httpd/api_http_user.go b/httpd/api_http_user.go index 6a9a0782..790cc234 100644 --- a/httpd/api_http_user.go +++ b/httpd/api_http_user.go @@ -215,13 +215,13 @@ func changeUserPassword(w http.ResponseWriter, r *http.Request) { func doChangeUserPassword(r *http.Request, currentPassword, newPassword, confirmNewPassword string) error { if currentPassword == "" || newPassword == "" || confirmNewPassword == "" { - return dataprovider.NewValidationError("please provide the current password and the new one two times") + return utils.NewValidationError("please provide the current password and the new one two times") } if newPassword != confirmNewPassword { - return dataprovider.NewValidationError("the two password fields do not match") + return utils.NewValidationError("the two password fields do not match") } if currentPassword == newPassword { - return dataprovider.NewValidationError("the new password must be different from the current one") + return utils.NewValidationError("the new password must be different from the current one") } claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { @@ -230,7 +230,7 @@ func doChangeUserPassword(r *http.Request, currentPassword, newPassword, confirm user, err := dataprovider.CheckUserAndPass(claims.Username, currentPassword, utils.GetIPFromRemoteAddress(r.RemoteAddr), common.ProtocolHTTP) if err != nil { - return dataprovider.NewValidationError("current password does not match") + return utils.NewValidationError("current password does not match") } user.Password = newPassword diff --git a/httpd/api_maintenance.go b/httpd/api_maintenance.go index a0cd1ce8..1dd4414f 100644 --- a/httpd/api_maintenance.go +++ b/httpd/api_maintenance.go @@ -16,6 +16,7 @@ import ( "github.com/drakkan/sftpgo/common" "github.com/drakkan/sftpgo/dataprovider" "github.com/drakkan/sftpgo/logger" + "github.com/drakkan/sftpgo/utils" "github.com/drakkan/sftpgo/vfs" ) @@ -104,7 +105,7 @@ func loadDataFromRequest(w http.ResponseWriter, r *http.Request) { content, err := io.ReadAll(r.Body) if err != nil || len(content) == 0 { if len(content) == 0 { - err = dataprovider.NewValidationError("request body is required") + err = utils.NewValidationError("request body is required") } sendAPIResponse(w, r, err, "", getRespStatus(err)) return @@ -150,7 +151,7 @@ func loadData(w http.ResponseWriter, r *http.Request) { func restoreBackup(content []byte, inputFile string, scanQuota, mode int) error { dump, err := dataprovider.ParseDumpData(content) if err != nil { - return dataprovider.NewValidationError(fmt.Sprintf("Unable to parse backup content: %v", err)) + return utils.NewValidationError(fmt.Sprintf("Unable to parse backup content: %v", err)) } if err = RestoreFolders(dump.Folders, inputFile, mode, scanQuota); err != nil { diff --git a/httpd/api_utils.go b/httpd/api_utils.go index 73b69f22..50b7f52c 100644 --- a/httpd/api_utils.go +++ b/httpd/api_utils.go @@ -42,7 +42,7 @@ func sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message } func getRespStatus(err error) int { - if _, ok := err.(*dataprovider.ValidationError); ok { + if _, ok := err.(*utils.ValidationError); ok { return http.StatusBadRequest } if _, ok := err.(*dataprovider.MethodDisabledError); ok { diff --git a/utils/errors.go b/utils/errors.go new file mode 100644 index 00000000..8b73bfc4 --- /dev/null +++ b/utils/errors.go @@ -0,0 +1,20 @@ +package utils + +import "fmt" + +// ValidationError raised if input data is not valid +type ValidationError struct { + err string +} + +// Validation error details +func (e *ValidationError) Error() string { + return fmt.Sprintf("Validation error: %s", e.err) +} + +// NewValidationError returns a validation errors +func NewValidationError(error string) *ValidationError { + return &ValidationError{ + err: error, + } +} diff --git a/vfs/filesystem.go b/vfs/filesystem.go index 0978cb5c..020338ba 100644 --- a/vfs/filesystem.go +++ b/vfs/filesystem.go @@ -1,6 +1,11 @@ package vfs -import "github.com/drakkan/sftpgo/kms" +import ( + "fmt" + + "github.com/drakkan/sftpgo/kms" + "github.com/drakkan/sftpgo/utils" +) // FilesystemProvider defines the supported storage filesystems type FilesystemProvider int @@ -15,6 +20,13 @@ const ( SFTPFilesystemProvider // SFTP ) +// ValidatorHelper implements methods we need for Filesystem.ValidateConfig. +// It is implemented by vfs.Folder and dataprovider.User +type ValidatorHelper interface { + GetGCSCredentialsFilePath() string + GetEncryptionAdditionalData() string +} + // Filesystem defines cloud storage filesystem details type Filesystem struct { RedactedSecret string `json:"-"` @@ -99,6 +111,128 @@ func (f *Filesystem) IsEqual(other *Filesystem) bool { } } +// Validate verifies the FsConfig matching the configured provider and sets all other +// Filesystem.*Config to their zero value if successful +func (f *Filesystem) Validate(helper ValidatorHelper) error { + switch f.Provider { + case S3FilesystemProvider: + if err := f.S3Config.Validate(); err != nil { + return utils.NewValidationError(fmt.Sprintf("could not validate s3config: %v", err)) + } + if err := f.S3Config.EncryptCredentials(helper.GetEncryptionAdditionalData()); err != nil { + return utils.NewValidationError(fmt.Sprintf("could not encrypt s3 access secret: %v", err)) + } + f.GCSConfig = GCSFsConfig{} + f.AzBlobConfig = AzBlobFsConfig{} + f.CryptConfig = CryptFsConfig{} + f.SFTPConfig = SFTPFsConfig{} + return nil + case GCSFilesystemProvider: + if err := f.GCSConfig.Validate(helper.GetGCSCredentialsFilePath()); err != nil { + return utils.NewValidationError(fmt.Sprintf("could not validate GCS config: %v", err)) + } + f.S3Config = S3FsConfig{} + f.AzBlobConfig = AzBlobFsConfig{} + f.CryptConfig = CryptFsConfig{} + f.SFTPConfig = SFTPFsConfig{} + return nil + case AzureBlobFilesystemProvider: + if err := f.AzBlobConfig.Validate(); err != nil { + return utils.NewValidationError(fmt.Sprintf("could not validate Azure Blob config: %v", err)) + } + if err := f.AzBlobConfig.EncryptCredentials(helper.GetEncryptionAdditionalData()); err != nil { + return utils.NewValidationError(fmt.Sprintf("could not encrypt Azure blob account key: %v", err)) + } + f.S3Config = S3FsConfig{} + f.GCSConfig = GCSFsConfig{} + f.CryptConfig = CryptFsConfig{} + f.SFTPConfig = SFTPFsConfig{} + return nil + case CryptedFilesystemProvider: + if err := f.CryptConfig.Validate(); err != nil { + return utils.NewValidationError(fmt.Sprintf("could not validate Crypt fs config: %v", err)) + } + if err := f.CryptConfig.EncryptCredentials(helper.GetEncryptionAdditionalData()); err != nil { + return utils.NewValidationError(fmt.Sprintf("could not encrypt Crypt fs passphrase: %v", err)) + } + f.S3Config = S3FsConfig{} + f.GCSConfig = GCSFsConfig{} + f.AzBlobConfig = AzBlobFsConfig{} + f.SFTPConfig = SFTPFsConfig{} + return nil + case SFTPFilesystemProvider: + if err := f.SFTPConfig.Validate(); err != nil { + return utils.NewValidationError(fmt.Sprintf("could not validate SFTP fs config: %v", err)) + } + if err := f.SFTPConfig.EncryptCredentials(helper.GetEncryptionAdditionalData()); err != nil { + return utils.NewValidationError(fmt.Sprintf("could not encrypt SFTP fs credentials: %v", err)) + } + f.S3Config = S3FsConfig{} + f.GCSConfig = GCSFsConfig{} + f.AzBlobConfig = AzBlobFsConfig{} + f.CryptConfig = CryptFsConfig{} + return nil + default: + f.Provider = LocalFilesystemProvider + f.S3Config = S3FsConfig{} + f.GCSConfig = GCSFsConfig{} + f.AzBlobConfig = AzBlobFsConfig{} + f.CryptConfig = CryptFsConfig{} + f.SFTPConfig = SFTPFsConfig{} + return nil + } +} + +// HasRedactedSecret returns true if configured the filesystem configuration has a redacted secret +func (f *Filesystem) HasRedactedSecret() bool { + // TODO move vfs specific code into each *FsConfig struct + switch f.Provider { + case S3FilesystemProvider: + if f.S3Config.AccessSecret.IsRedacted() { + return true + } + case GCSFilesystemProvider: + if f.GCSConfig.Credentials.IsRedacted() { + return true + } + case AzureBlobFilesystemProvider: + if f.AzBlobConfig.AccountKey.IsRedacted() { + return true + } + case CryptedFilesystemProvider: + if f.CryptConfig.Passphrase.IsRedacted() { + return true + } + case SFTPFilesystemProvider: + if f.SFTPConfig.Password.IsRedacted() { + return true + } + if f.SFTPConfig.PrivateKey.IsRedacted() { + return true + } + } + + return false +} + +// HideConfidentialData hides filesystem confidential data +func (f *Filesystem) HideConfidentialData() { + switch f.Provider { + case S3FilesystemProvider: + f.S3Config.AccessSecret.Hide() + case GCSFilesystemProvider: + f.GCSConfig.Credentials.Hide() + case AzureBlobFilesystemProvider: + f.AzBlobConfig.AccountKey.Hide() + f.AzBlobConfig.SASURL.Hide() + case CryptedFilesystemProvider: + f.CryptConfig.Passphrase.Hide() + case SFTPFilesystemProvider: + f.SFTPConfig.Password.Hide() + f.SFTPConfig.PrivateKey.Hide() + } +} + // GetACopy returns a copy func (f *Filesystem) GetACopy() Filesystem { f.SetEmptySecretsIfNil() diff --git a/vfs/folder.go b/vfs/folder.go index 73090afb..29e7aa33 100644 --- a/vfs/folder.go +++ b/vfs/folder.go @@ -28,8 +28,8 @@ type BaseVirtualFolder struct { FsConfig Filesystem `json:"filesystem"` } -// GetEncrytionAdditionalData returns the additional data to use for AEAD -func (v *BaseVirtualFolder) GetEncrytionAdditionalData() string { +// GetEncryptionAdditionalData returns the additional data to use for AEAD +func (v *BaseVirtualFolder) GetEncryptionAdditionalData() string { return fmt.Sprintf("folder_%v", v.Name) }