diff --git a/go.mod b/go.mod index 0ed30305..b5904006 100644 --- a/go.mod +++ b/go.mod @@ -10,10 +10,10 @@ require ( github.com/alexedwards/argon2id v1.0.0 github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 github.com/aws/aws-sdk-go-v2 v1.24.1 - github.com/aws/aws-sdk-go-v2/config v1.26.3 - github.com/aws/aws-sdk-go-v2/credentials v1.16.14 + github.com/aws/aws-sdk-go-v2/config v1.26.4 + github.com/aws/aws-sdk-go-v2/credentials v1.16.15 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.11 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.12 github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.19.6 github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0 github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2 @@ -95,7 +95,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.10 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.18.6 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect github.com/aws/smithy-go v1.19.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/boombuler/barcode v1.0.1 // indirect @@ -171,9 +171,9 @@ require ( golang.org/x/tools v0.17.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect + google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect google.golang.org/grpc v1.60.1 // indirect google.golang.org/protobuf v1.32.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index e3ed69b8..dae60544 100644 --- a/go.sum +++ b/go.sum @@ -37,14 +37,14 @@ github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3 github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo= -github.com/aws/aws-sdk-go-v2/config v1.26.3 h1:dKuc2jdp10y13dEEvPqWxqLoc0vF3Z9FC45MvuQSxOA= -github.com/aws/aws-sdk-go-v2/config v1.26.3/go.mod h1:Bxgi+DeeswYofcYO0XyGClwlrq3DZEXli0kLf4hkGA0= -github.com/aws/aws-sdk-go-v2/credentials v1.16.14 h1:mMDTwwYO9A0/JbOCOG7EOZHtYM+o7OfGWfu0toa23VE= -github.com/aws/aws-sdk-go-v2/credentials v1.16.14/go.mod h1:cniAUh3ErQPHtCQGPT5ouvSAQ0od8caTO9OOuufZOAE= +github.com/aws/aws-sdk-go-v2/config v1.26.4 h1:Juj7LhtxNudNUlfX22K5AnLafO+v4eq9PA3VWSCIQs4= +github.com/aws/aws-sdk-go-v2/config v1.26.4/go.mod h1:tioqQ7wvxMYnTDpoTTLHhV3Zh+z261i/f2oz+ds8eNI= +github.com/aws/aws-sdk-go-v2/credentials v1.16.15 h1:P0/m1LU08MF2kRzx4P//+7lNjiJod1z4xI2WpWhdpTQ= +github.com/aws/aws-sdk-go-v2/credentials v1.16.15/go.mod h1:pgtMCf7Dx4GWw5EpHOTc2Sy17LIP0A0N2C9nQ83pQ/0= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.11 h1:I6lAa3wBWfCz/cKkOpAcumsETRkFAl70sWi8ItcMEsM= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.11/go.mod h1:be1NIO30kJA23ORBLqPo1LttEM6tPNSEcjkd1eKzNW0= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.12 h1:0FMZy36RSYvcvVzEf1xbNdebLHZewW40QWP+P8jCMVk= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.12/go.mod h1:+chyahvarkb3HibkNei9IQEM9P5cWD5w2kgXCa3Hh0I= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw= @@ -69,8 +69,8 @@ github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2 h1:A5sGOT/mukuU+4At1 github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2/go.mod h1:qutL00aW8GSo2D0I6UEOqMvRS3ZyuBrOC1BLe5D2jPc= github.com/aws/aws-sdk-go-v2/service/sso v1.18.6 h1:dGrs+Q/WzhsiUKh82SfTVN66QzyulXuMDTV/G8ZxOac= github.com/aws/aws-sdk-go-v2/service/sso v1.18.6/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6 h1:Yf2MIo9x+0tyv76GljxzqA3WtC5mw7NmazD2chwjxE4= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8= github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0= github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U= github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= @@ -531,12 +531,12 @@ google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 h1:/IWabOtPziuXTEtI1KYCpM6Ss7vaAkeMxk+uXV/xvZs= -google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= -google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 h1:OPXtXn7fNMaXwO3JvOmF1QyTc00jsSFFz1vXXBOdCDo= -google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 h1:gphdwh0npgs8elJ4T6J+DQJHPVF7RsuJHCfwztUb4J4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA= +google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac h1:ZL/Teoy/ZGnzyrqK/Optxxp2pmVh+fmJ97slxSRyzUg= +google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= +google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac h1:OZkkudMUu9LVQMCoRUbI/1p5VCo9BOrlvkqMvWtqa6s= +google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= diff --git a/internal/dataprovider/bolt.go b/internal/dataprovider/bolt.go index c6216e45..775d0ccd 100644 --- a/internal/dataprovider/bolt.go +++ b/internal/dataprovider/bolt.go @@ -387,7 +387,10 @@ func (p *BoltProvider) addAdmin(admin *Admin) error { return err } if a := bucket.Get([]byte(admin.Username)); a != nil { - return fmt.Errorf("admin %q already exists", admin.Username) + return util.NewI18nError( + fmt.Errorf("%w: admin %q already exists", ErrDuplicatedKey, admin.Username), + util.I18nErrorDuplicatedUsername, + ) } id, err := bucket.NextSequence() if err != nil { @@ -649,7 +652,10 @@ func (p *BoltProvider) addUser(user *User) error { return err } if u := bucket.Get([]byte(user.Username)); u != nil { - return fmt.Errorf("username %v already exists", user.Username) + return util.NewI18nError( + fmt.Errorf("%w: username %v already exists", ErrDuplicatedKey, user.Username), + util.I18nErrorDuplicatedUsername, + ) } id, err := bucket.NextSequence() if err != nil { @@ -1121,7 +1127,10 @@ func (p *BoltProvider) addFolder(folder *vfs.BaseVirtualFolder) error { return err } if f := bucket.Get([]byte(folder.Name)); f != nil { - return fmt.Errorf("folder %v already exists", folder.Name) + return util.NewI18nError( + fmt.Errorf("%w: folder %q already exists", ErrDuplicatedKey, folder.Name), + util.I18nErrorDuplicatedUsername, + ) } folder.Users = nil folder.Groups = nil @@ -1435,7 +1444,10 @@ func (p *BoltProvider) addGroup(group *Group) error { return err } if u := bucket.Get([]byte(group.Name)); u != nil { - return fmt.Errorf("group %v already exists", group.Name) + return util.NewI18nError( + fmt.Errorf("%w: group %q already exists", ErrDuplicatedKey, group.Name), + util.I18nErrorDuplicatedUsername, + ) } id, err := bucket.NextSequence() if err != nil { @@ -1808,7 +1820,7 @@ func (p *BoltProvider) addShare(share *Share) error { return err } if a := bucket.Get([]byte(share.ShareID)); a != nil { - return fmt.Errorf("share %v already exists", share.ShareID) + return fmt.Errorf("share %q already exists", share.ShareID) } id, err := bucket.NextSequence() if err != nil { @@ -2188,7 +2200,10 @@ func (p *BoltProvider) addEventAction(action *BaseEventAction) error { return err } if a := bucket.Get([]byte(action.Name)); a != nil { - return fmt.Errorf("event action %s already exists", action.Name) + return util.NewI18nError( + fmt.Errorf("%w: event action %q already exists", ErrDuplicatedKey, action.Name), + util.I18nErrorDuplicatedName, + ) } id, err := bucket.NextSequence() if err != nil { @@ -2449,7 +2464,10 @@ func (p *BoltProvider) addEventRule(rule *EventRule) error { return err } if r := bucket.Get([]byte(rule.Name)); r != nil { - return fmt.Errorf("event rule %q already exists", rule.Name) + return util.NewI18nError( + fmt.Errorf("%w: event rule %q already exists", ErrDuplicatedKey, rule.Name), + util.I18nErrorDuplicatedName, + ) } id, err := bucket.NextSequence() if err != nil { @@ -2618,7 +2636,10 @@ func (p *BoltProvider) addRole(role *Role) error { return err } if r := bucket.Get([]byte(role.Name)); r != nil { - return fmt.Errorf("role %q already exists", role.Name) + return util.NewI18nError( + fmt.Errorf("%w: role %q already exists", ErrDuplicatedKey, role.Name), + util.I18nErrorDuplicatedName, + ) } id, err := bucket.NextSequence() if err != nil { diff --git a/internal/dataprovider/dataprovider.go b/internal/dataprovider/dataprovider.go index 7287b555..a4230d79 100644 --- a/internal/dataprovider/dataprovider.go +++ b/internal/dataprovider/dataprovider.go @@ -148,6 +148,11 @@ const ( DumpScopeConfigs = "configs" ) +const ( + fieldUsername = 1 + fieldName = 2 +) + var ( // SupportedProviders defines the supported data providers SupportedProviders = []string{SQLiteDataProviderName, PGSQLDataProviderName, MySQLDataProviderName, @@ -176,13 +181,17 @@ var ( ErrInvalidCredentials = errors.New("invalid credentials") // ErrLoginNotAllowedFromIP defines the error to return if login is denied from the current IP ErrLoginNotAllowedFromIP = errors.New("login is not allowed from this IP") - isAdminCreated atomic.Bool - validTLSUsernames = []string{string(sdk.TLSUsernameNone), string(sdk.TLSUsernameCN)} - config Config - provider Provider - sqlPlaceholders []string - internalHashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix} - hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, + // ErrDuplicatedKey occurs when there is a unique key constraint violation + ErrDuplicatedKey = errors.New("duplicated key not allowed") + // ErrForeignKeyViolated occurs when there is a foreign key constraint violation + ErrForeignKeyViolated = errors.New("violates foreign key constraint") + isAdminCreated atomic.Bool + validTLSUsernames = []string{string(sdk.TLSUsernameNone), string(sdk.TLSUsernameCN)} + config Config + provider Provider + sqlPlaceholders []string + internalHashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix} + hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix, md5cryptPwdPrefix, md5cryptApr1PwdPrefix, md5DigestPwdPrefix, sha256DigestPwdPrefix, sha512DigestPwdPrefix, sha256cryptPwdPrefix, sha512cryptPwdPrefix, yescryptPwdPrefix} pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix} diff --git a/internal/dataprovider/memory.go b/internal/dataprovider/memory.go index 3c0fcc6c..eb9674d1 100644 --- a/internal/dataprovider/memory.go +++ b/internal/dataprovider/memory.go @@ -332,7 +332,10 @@ func (p *MemoryProvider) addUser(user *User) error { _, err = p.userExistsInternal(user.Username) if err == nil { - return fmt.Errorf("username %q already exists", user.Username) + return util.NewI18nError( + fmt.Errorf("%w: username %v already exists", ErrDuplicatedKey, user.Username), + util.I18nErrorDuplicatedUsername, + ) } user.ID = p.getNextID() user.LastQuotaUpdate = 0 @@ -730,7 +733,10 @@ func (p *MemoryProvider) addAdmin(admin *Admin) error { } _, err = p.adminExistsInternal(admin.Username) if err == nil { - return fmt.Errorf("admin %q already exists", admin.Username) + return util.NewI18nError( + fmt.Errorf("%w: admin %q already exists", ErrDuplicatedKey, admin.Username), + util.I18nErrorDuplicatedUsername, + ) } admin.ID = p.getNextAdminID() admin.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now()) @@ -1041,7 +1047,10 @@ func (p *MemoryProvider) addGroup(group *Group) error { _, err := p.groupExistsInternal(group.Name) if err == nil { - return fmt.Errorf("group %q already exists", group.Name) + return util.NewI18nError( + fmt.Errorf("%w: group %q already exists", ErrDuplicatedKey, group.Name), + util.I18nErrorDuplicatedUsername, + ) } group.ID = p.getNextGroupID() group.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now()) @@ -1512,7 +1521,10 @@ func (p *MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error { _, err = p.folderExistsInternal(folder.Name) if err == nil { - return fmt.Errorf("folder %q already exists", folder.Name) + return util.NewI18nError( + fmt.Errorf("%w: folder %q already exists", ErrDuplicatedKey, folder.Name), + util.I18nErrorDuplicatedUsername, + ) } folder.ID = p.getNextFolderID() folder.Users = nil @@ -2172,7 +2184,10 @@ func (p *MemoryProvider) addEventAction(action *BaseEventAction) error { } _, err = p.actionExistsInternal(action.Name) if err == nil { - return fmt.Errorf("event action %q already exists", action.Name) + return util.NewI18nError( + fmt.Errorf("%w: event action %q already exists", ErrDuplicatedKey, action.Name), + util.I18nErrorDuplicatedName, + ) } action.ID = p.getNextActionID() action.Rules = nil @@ -2348,7 +2363,10 @@ func (p *MemoryProvider) addEventRule(rule *EventRule) error { } _, err := p.ruleExistsInternal(rule.Name) if err == nil { - return fmt.Errorf("event rule %q already exists", rule.Name) + return util.NewI18nError( + fmt.Errorf("%w: event rule %q already exists", ErrDuplicatedKey, rule.Name), + util.I18nErrorDuplicatedName, + ) } rule.ID = p.getNextRuleID() rule.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now()) @@ -2499,7 +2517,10 @@ func (p *MemoryProvider) addRole(role *Role) error { _, err := p.roleExistsInternal(role.Name) if err == nil { - return fmt.Errorf("role %q already exists", role.Name) + return util.NewI18nError( + fmt.Errorf("%w: role %q already exists", ErrDuplicatedKey, role.Name), + util.I18nErrorDuplicatedName, + ) } role.ID = p.getNextRoleID() role.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now()) diff --git a/internal/dataprovider/mysql.go b/internal/dataprovider/mysql.go index 98639092..f7e85cbb 100644 --- a/internal/dataprovider/mysql.go +++ b/internal/dataprovider/mysql.go @@ -32,6 +32,7 @@ import ( "github.com/go-sql-driver/mysql" "github.com/drakkan/sftpgo/v2/internal/logger" + "github.com/drakkan/sftpgo/v2/internal/util" "github.com/drakkan/sftpgo/v2/internal/version" "github.com/drakkan/sftpgo/v2/internal/vfs" ) @@ -341,7 +342,7 @@ func (p *MySQLProvider) userExists(username, role string) (User, error) { } func (p *MySQLProvider) addUser(user *User) error { - return sqlCommonAddUser(user, p.dbHandle) + return p.normalizeError(sqlCommonAddUser(user, p.dbHandle), fieldUsername) } func (p *MySQLProvider) updateUser(user *User) error { @@ -387,7 +388,7 @@ func (p *MySQLProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, err } func (p *MySQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error { - return sqlCommonAddFolder(folder, p.dbHandle) + return p.normalizeError(sqlCommonAddFolder(folder, p.dbHandle), fieldName) } func (p *MySQLProvider) updateFolder(folder *vfs.BaseVirtualFolder) error { @@ -423,7 +424,7 @@ func (p *MySQLProvider) groupExists(name string) (Group, error) { } func (p *MySQLProvider) addGroup(group *Group) error { - return sqlCommonAddGroup(group, p.dbHandle) + return p.normalizeError(sqlCommonAddGroup(group, p.dbHandle), fieldName) } func (p *MySQLProvider) updateGroup(group *Group) error { @@ -443,7 +444,7 @@ func (p *MySQLProvider) adminExists(username string) (Admin, error) { } func (p *MySQLProvider) addAdmin(admin *Admin) error { - return sqlCommonAddAdmin(admin, p.dbHandle) + return p.normalizeError(sqlCommonAddAdmin(admin, p.dbHandle), fieldUsername) } func (p *MySQLProvider) updateAdmin(admin *Admin) error { @@ -603,7 +604,7 @@ func (p *MySQLProvider) eventActionExists(name string) (BaseEventAction, error) } func (p *MySQLProvider) addEventAction(action *BaseEventAction) error { - return sqlCommonAddEventAction(action, p.dbHandle) + return p.normalizeError(sqlCommonAddEventAction(action, p.dbHandle), fieldName) } func (p *MySQLProvider) updateEventAction(action *BaseEventAction) error { @@ -631,7 +632,7 @@ func (p *MySQLProvider) eventRuleExists(name string) (EventRule, error) { } func (p *MySQLProvider) addEventRule(rule *EventRule) error { - return sqlCommonAddEventRule(rule, p.dbHandle) + return p.normalizeError(sqlCommonAddEventRule(rule, p.dbHandle), fieldName) } func (p *MySQLProvider) updateEventRule(rule *EventRule) error { @@ -683,7 +684,7 @@ func (p *MySQLProvider) roleExists(name string) (Role, error) { } func (p *MySQLProvider) addRole(role *Role) error { - return sqlCommonAddRole(role, p.dbHandle) + return p.normalizeError(sqlCommonAddRole(role, p.dbHandle), fieldName) } func (p *MySQLProvider) updateRole(role *Role) error { @@ -824,3 +825,26 @@ func (p *MySQLProvider) resetDatabase() error { sql := sqlReplaceAll(mysqlResetSQL) return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, strings.Split(sql, ";"), 0, false) } + +func (p *MySQLProvider) normalizeError(err error, fieldType int) error { + if err == nil { + return nil + } + var mysqlErr *mysql.MySQLError + if errors.As(err, &mysqlErr) { + switch mysqlErr.Number { + case 1062: + message := util.I18nErrorDuplicatedName + if fieldType == fieldUsername { + message = util.I18nErrorDuplicatedUsername + } + return util.NewI18nError( + fmt.Errorf("%w: %s", ErrDuplicatedKey, err.Error()), + message, + ) + case 1452: + return fmt.Errorf("%w: %s", ErrForeignKeyViolated, err.Error()) + } + } + return err +} diff --git a/internal/dataprovider/pgsql.go b/internal/dataprovider/pgsql.go index 8c163835..83b60558 100644 --- a/internal/dataprovider/pgsql.go +++ b/internal/dataprovider/pgsql.go @@ -29,9 +29,11 @@ import ( "time" "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/stdlib" "github.com/drakkan/sftpgo/v2/internal/logger" + "github.com/drakkan/sftpgo/v2/internal/util" "github.com/drakkan/sftpgo/v2/internal/version" "github.com/drakkan/sftpgo/v2/internal/vfs" ) @@ -353,7 +355,7 @@ func (p *PGSQLProvider) userExists(username, role string) (User, error) { } func (p *PGSQLProvider) addUser(user *User) error { - return sqlCommonAddUser(user, p.dbHandle) + return p.normalizeError(sqlCommonAddUser(user, p.dbHandle), fieldUsername) } func (p *PGSQLProvider) updateUser(user *User) error { @@ -399,7 +401,7 @@ func (p *PGSQLProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, err } func (p *PGSQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error { - return sqlCommonAddFolder(folder, p.dbHandle) + return p.normalizeError(sqlCommonAddFolder(folder, p.dbHandle), fieldName) } func (p *PGSQLProvider) updateFolder(folder *vfs.BaseVirtualFolder) error { @@ -435,7 +437,7 @@ func (p *PGSQLProvider) groupExists(name string) (Group, error) { } func (p *PGSQLProvider) addGroup(group *Group) error { - return sqlCommonAddGroup(group, p.dbHandle) + return p.normalizeError(sqlCommonAddGroup(group, p.dbHandle), fieldName) } func (p *PGSQLProvider) updateGroup(group *Group) error { @@ -455,7 +457,7 @@ func (p *PGSQLProvider) adminExists(username string) (Admin, error) { } func (p *PGSQLProvider) addAdmin(admin *Admin) error { - return sqlCommonAddAdmin(admin, p.dbHandle) + return p.normalizeError(sqlCommonAddAdmin(admin, p.dbHandle), fieldUsername) } func (p *PGSQLProvider) updateAdmin(admin *Admin) error { @@ -615,7 +617,7 @@ func (p *PGSQLProvider) eventActionExists(name string) (BaseEventAction, error) } func (p *PGSQLProvider) addEventAction(action *BaseEventAction) error { - return sqlCommonAddEventAction(action, p.dbHandle) + return p.normalizeError(sqlCommonAddEventAction(action, p.dbHandle), fieldName) } func (p *PGSQLProvider) updateEventAction(action *BaseEventAction) error { @@ -643,7 +645,7 @@ func (p *PGSQLProvider) eventRuleExists(name string) (EventRule, error) { } func (p *PGSQLProvider) addEventRule(rule *EventRule) error { - return sqlCommonAddEventRule(rule, p.dbHandle) + return p.normalizeError(sqlCommonAddEventRule(rule, p.dbHandle), fieldName) } func (p *PGSQLProvider) updateEventRule(rule *EventRule) error { @@ -695,7 +697,7 @@ func (p *PGSQLProvider) roleExists(name string) (Role, error) { } func (p *PGSQLProvider) addRole(role *Role) error { - return sqlCommonAddRole(role, p.dbHandle) + return p.normalizeError(sqlCommonAddRole(role, p.dbHandle), fieldName) } func (p *PGSQLProvider) updateRole(role *Role) error { @@ -842,3 +844,26 @@ func (p *PGSQLProvider) resetDatabase() error { sql := sqlReplaceAll(pgsqlResetSQL) return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 0, false) } + +func (p *PGSQLProvider) normalizeError(err error, fieldType int) error { + if err == nil { + return nil + } + var pgsqlErr *pgconn.PgError + if errors.As(err, &pgsqlErr) { + switch pgsqlErr.Code { + case "23505": + message := util.I18nErrorDuplicatedName + if fieldType == fieldUsername { + message = util.I18nErrorDuplicatedUsername + } + return util.NewI18nError( + fmt.Errorf("%w: %s", ErrDuplicatedKey, err.Error()), + message, + ) + case "23503": + return fmt.Errorf("%w: %s", ErrForeignKeyViolated, err.Error()) + } + } + return err +} diff --git a/internal/dataprovider/sqlite.go b/internal/dataprovider/sqlite.go index 757a77fc..e80c3bc4 100644 --- a/internal/dataprovider/sqlite.go +++ b/internal/dataprovider/sqlite.go @@ -26,8 +26,7 @@ import ( "path/filepath" "time" - // we import go-sqlite3 here to be able to disable SQLite support using a build tag - _ "github.com/mattn/go-sqlite3" + "github.com/mattn/go-sqlite3" "github.com/drakkan/sftpgo/v2/internal/logger" "github.com/drakkan/sftpgo/v2/internal/util" @@ -264,7 +263,7 @@ func (p *SQLiteProvider) userExists(username, role string) (User, error) { } func (p *SQLiteProvider) addUser(user *User) error { - return sqlCommonAddUser(user, p.dbHandle) + return p.normalizeError(sqlCommonAddUser(user, p.dbHandle), fieldUsername) } func (p *SQLiteProvider) updateUser(user *User) error { @@ -310,7 +309,7 @@ func (p *SQLiteProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, er } func (p *SQLiteProvider) addFolder(folder *vfs.BaseVirtualFolder) error { - return sqlCommonAddFolder(folder, p.dbHandle) + return p.normalizeError(sqlCommonAddFolder(folder, p.dbHandle), fieldName) } func (p *SQLiteProvider) updateFolder(folder *vfs.BaseVirtualFolder) error { @@ -346,7 +345,7 @@ func (p *SQLiteProvider) groupExists(name string) (Group, error) { } func (p *SQLiteProvider) addGroup(group *Group) error { - return sqlCommonAddGroup(group, p.dbHandle) + return p.normalizeError(sqlCommonAddGroup(group, p.dbHandle), fieldName) } func (p *SQLiteProvider) updateGroup(group *Group) error { @@ -366,7 +365,7 @@ func (p *SQLiteProvider) adminExists(username string) (Admin, error) { } func (p *SQLiteProvider) addAdmin(admin *Admin) error { - return sqlCommonAddAdmin(admin, p.dbHandle) + return p.normalizeError(sqlCommonAddAdmin(admin, p.dbHandle), fieldUsername) } func (p *SQLiteProvider) updateAdmin(admin *Admin) error { @@ -526,7 +525,7 @@ func (p *SQLiteProvider) eventActionExists(name string) (BaseEventAction, error) } func (p *SQLiteProvider) addEventAction(action *BaseEventAction) error { - return sqlCommonAddEventAction(action, p.dbHandle) + return p.normalizeError(sqlCommonAddEventAction(action, p.dbHandle), fieldName) } func (p *SQLiteProvider) updateEventAction(action *BaseEventAction) error { @@ -554,7 +553,7 @@ func (p *SQLiteProvider) eventRuleExists(name string) (EventRule, error) { } func (p *SQLiteProvider) addEventRule(rule *EventRule) error { - return sqlCommonAddEventRule(rule, p.dbHandle) + return p.normalizeError(sqlCommonAddEventRule(rule, p.dbHandle), fieldName) } func (p *SQLiteProvider) updateEventRule(rule *EventRule) error { @@ -606,7 +605,7 @@ func (p *SQLiteProvider) roleExists(name string) (Role, error) { } func (p *SQLiteProvider) addRole(role *Role) error { - return sqlCommonAddRole(role, p.dbHandle) + return p.normalizeError(sqlCommonAddRole(role, p.dbHandle), fieldName) } func (p *SQLiteProvider) updateRole(role *Role) error { @@ -747,6 +746,28 @@ func (p *SQLiteProvider) resetDatabase() error { return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 0, false) } +func (p *SQLiteProvider) normalizeError(err error, fieldType int) error { + if err == nil { + return nil + } + if e, ok := err.(sqlite3.Error); ok { + switch e.ExtendedCode { + case 1555, 2067: + message := util.I18nErrorDuplicatedName + if fieldType == fieldUsername { + message = util.I18nErrorDuplicatedUsername + } + return util.NewI18nError( + fmt.Errorf("%w: %s", ErrDuplicatedKey, err.Error()), + message, + ) + case 787: + return fmt.Errorf("%w: %s", ErrForeignKeyViolated, err.Error()) + } + } + return err +} + /*func setPragmaFK(dbHandle *sql.DB, value string) error { ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout) defer cancel() diff --git a/internal/httpd/api_utils.go b/internal/httpd/api_utils.go index 792502af..dd31149a 100644 --- a/internal/httpd/api_utils.go +++ b/internal/httpd/api_utils.go @@ -105,6 +105,9 @@ func getRespStatus(err error) int { if errors.Is(err, plugin.ErrNoSearcher) || errors.Is(err, dataprovider.ErrNotImplemented) { return http.StatusNotImplemented } + if errors.Is(err, dataprovider.ErrDuplicatedKey) || errors.Is(err, dataprovider.ErrForeignKeyViolated) { + return http.StatusConflict + } return http.StatusInternalServerError } diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go index 317093be..ae14cd7a 100644 --- a/internal/httpd/httpd_test.go +++ b/internal/httpd/httpd_test.go @@ -600,6 +600,8 @@ func TestBasicUserHandling(t *testing.T) { u.Email = "user@user.com" user, resp, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err, string(resp)) + _, resp, err = httpdtest.AddUser(u, http.StatusConflict) + assert.NoError(t, err, string(resp)) lastPwdChange := user.LastPasswordChange assert.Greater(t, lastPwdChange, int64(0)) user.MaxSessions = 10 diff --git a/internal/util/i18n.go b/internal/util/i18n.go index 885c4505..1098becc 100644 --- a/internal/util/i18n.go +++ b/internal/util/i18n.go @@ -189,6 +189,8 @@ const ( I18nAddFolderTitle = "title.add_folder" I18nUpdateFolderTitle = "title.update_folder" I18nTemplateFolderTitle = "title.template_folder" + I18nErrorDuplicatedUsername = "general.duplicated_username" + I18nErrorDuplicatedName = "general.duplicated_name" ) // NewI18nError returns a I18nError wrappring the provided error diff --git a/static/locales/en/translation.json b/static/locales/en/translation.json index 80cca890..cd869d47 100644 --- a/static/locales/en/translation.json +++ b/static/locales/en/translation.json @@ -211,7 +211,9 @@ "mandatory_encryption": "Mandatory encryption", "name_invalid": "The specified username is not valid, the following characters are allowed: a-zA-Z0-9-_.~", "associations": "Associations", - "template_placeholders": "The following placeholders are supported" + "template_placeholders": "The following placeholders are supported", + "duplicated_username": "The specified username already exists", + "duplicated_name": "The specified name already exists" }, "fs": { "view_file": "View file \"{{- path}}\"", diff --git a/static/locales/it/translation.json b/static/locales/it/translation.json index c334f08b..2e4c9d59 100644 --- a/static/locales/it/translation.json +++ b/static/locales/it/translation.json @@ -211,7 +211,9 @@ "mandatory_encryption": "Crittografia obbligatoria", "name_invalid": "Il nome specificato non è valido, sono consentiti i seguenti caratteri: a-zA-Z0-9-_.~", "associations": "Associazioni", - "template_placeholders": "Sono supportati i seguenti segnaposto" + "template_placeholders": "Sono supportati i seguenti segnaposto", + "duplicated_username": "Il nome utente specificato esiste già", + "duplicated_name": "Il nome specificato esiste già" }, "fs": { "view_file": "Visualizza file \"{{- path}}\"",