mirror of
https://github.com/drakkan/sftpgo.git
synced 2026-10-19 16:15:39 +00:00
dataprovider: refuse the login if a group setting uses %role% without a role
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
@@ -178,6 +178,9 @@ var (
|
||||
ValidProtocols = []string{protocolSSH, protocolFTP, protocolWebDAV, protocolHTTP}
|
||||
// MFAProtocols defines the supported protocols for multi-factor authentication
|
||||
MFAProtocols = []string{protocolHTTP, protocolSSH, protocolFTP}
|
||||
// ErrPlaceholderUnset is returned when a group setting uses a placeholder
|
||||
// the account has no value for. The login is refused
|
||||
ErrPlaceholderUnset = errors.New("a group setting uses a placeholder the account has no value for")
|
||||
// ErrNoInitRequired defines the error returned by InitProvider if no inizialization/update is required
|
||||
ErrNoInitRequired = errors.New("the data provider is up to date")
|
||||
// ErrInvalidCredentials defines the error to return if the supplied credentials are invalid
|
||||
|
||||
@@ -1621,6 +1621,11 @@ func (u *User) applyGroupSettings(groupsMapping map[string]Group) {
|
||||
return
|
||||
}
|
||||
replacer := u.getGroupPlacehodersReplacer()
|
||||
defer func() {
|
||||
if replacer.err != nil {
|
||||
providerLog(logger.LevelError, "unable to apply group settings to user %q: %v", u.Username, replacer.err)
|
||||
}
|
||||
}()
|
||||
for _, g := range u.Groups {
|
||||
if g.Type == sdk.GroupTypePrimary {
|
||||
if group, ok := groupsMapping[g.Name]; ok {
|
||||
@@ -1689,22 +1694,49 @@ func (u *User) loadAndApplyGroupSettings() error {
|
||||
g := groups[idx]
|
||||
u.mergeAdditiveProperties(&g, sdk.GroupTypeSecondary, replacer)
|
||||
}
|
||||
if replacer.err != nil {
|
||||
providerLog(logger.LevelError, "unable to apply group settings to user %q: %v", u.Username, replacer.err)
|
||||
return replacer.err
|
||||
}
|
||||
u.removeDuplicatesAfterGroupMerge()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) getGroupPlacehodersReplacer() *strings.Replacer {
|
||||
return strings.NewReplacer("%username%", u.Username, "%role%", u.Role)
|
||||
type groupRenderer struct {
|
||||
*strings.Replacer
|
||||
user *User
|
||||
err error
|
||||
}
|
||||
|
||||
func (u *User) replacePlaceholder(value string, replacer *strings.Replacer) string {
|
||||
func (r *groupRenderer) render(value string) string {
|
||||
if value == "" {
|
||||
return value
|
||||
}
|
||||
return replacer.Replace(value)
|
||||
if r.user.Role == "" && strings.Contains(value, "%role%") {
|
||||
r.fail(value)
|
||||
return value
|
||||
}
|
||||
return r.Replacer.Replace(value)
|
||||
}
|
||||
|
||||
func (u *User) replaceFsConfigPlaceholders(fsConfig vfs.Filesystem, replacer *strings.Replacer) vfs.Filesystem {
|
||||
func (r *groupRenderer) fail(value string) {
|
||||
if r.err == nil {
|
||||
r.err = fmt.Errorf("%w: %q", ErrPlaceholderUnset, value)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) getGroupPlacehodersReplacer() *groupRenderer {
|
||||
return &groupRenderer{
|
||||
Replacer: strings.NewReplacer("%username%", u.Username, "%role%", u.Role),
|
||||
user: u,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) replacePlaceholder(value string, replacer *groupRenderer) string {
|
||||
return replacer.render(value)
|
||||
}
|
||||
|
||||
func (u *User) replaceFsConfigPlaceholders(fsConfig vfs.Filesystem, replacer *groupRenderer) vfs.Filesystem {
|
||||
switch fsConfig.Provider {
|
||||
case sdk.S3FilesystemProvider:
|
||||
fsConfig.S3Config.KeyPrefix = u.replacePlaceholder(fsConfig.S3Config.KeyPrefix, replacer)
|
||||
@@ -1732,7 +1764,7 @@ func (u *User) mergeCryptFsConfig(group *Group) {
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) mergeWithPrimaryGroup(group *Group, replacer *strings.Replacer) {
|
||||
func (u *User) mergeWithPrimaryGroup(group *Group, replacer *groupRenderer) {
|
||||
if group.UserSettings.HomeDir != "" {
|
||||
u.HomeDir = filepath.Clean(u.replacePlaceholder(group.UserSettings.HomeDir, replacer))
|
||||
}
|
||||
@@ -1774,7 +1806,7 @@ func (u *User) mergeWithPrimaryGroup(group *Group, replacer *strings.Replacer) {
|
||||
u.mergeAdditiveProperties(group, sdk.GroupTypePrimary, replacer)
|
||||
}
|
||||
|
||||
func (u *User) mergePrimaryGroupFilters(filters *sdk.BaseUserFilters, replacer *strings.Replacer) {
|
||||
func (u *User) mergePrimaryGroupFilters(filters *sdk.BaseUserFilters, replacer *groupRenderer) {
|
||||
if u.Filters.MaxUploadFileSize == 0 {
|
||||
u.Filters.MaxUploadFileSize = filters.MaxUploadFileSize
|
||||
}
|
||||
@@ -1822,7 +1854,7 @@ func (u *User) mergePrimaryGroupFilters(filters *sdk.BaseUserFilters, replacer *
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) mergeAdditiveProperties(group *Group, groupType int, replacer *strings.Replacer) {
|
||||
func (u *User) mergeAdditiveProperties(group *Group, groupType int, replacer *groupRenderer) {
|
||||
u.mergeVirtualFolders(group, groupType, replacer)
|
||||
u.mergePermissions(group, groupType, replacer)
|
||||
u.mergeFilePatterns(group, groupType, replacer)
|
||||
@@ -1836,7 +1868,7 @@ func (u *User) mergeAdditiveProperties(group *Group, groupType int, replacer *st
|
||||
u.Filters.AccessTime = append(u.Filters.AccessTime, group.UserSettings.Filters.AccessTime...)
|
||||
}
|
||||
|
||||
func (u *User) mergeVirtualFolders(group *Group, groupType int, replacer *strings.Replacer) {
|
||||
func (u *User) mergeVirtualFolders(group *Group, groupType int, replacer *groupRenderer) {
|
||||
if len(group.VirtualFolders) > 0 {
|
||||
folderPaths := make(map[string]bool)
|
||||
for _, folder := range u.VirtualFolders {
|
||||
@@ -1871,18 +1903,21 @@ func countPathSegments(p string) int {
|
||||
return strings.Count(p, "/")
|
||||
}
|
||||
|
||||
func (u *User) renderSubPathValue(value string, replacer *strings.Replacer) (string, bool) {
|
||||
func (u *User) renderSubPathValue(value string, replacer *groupRenderer) (string, bool) {
|
||||
if value == "" || !strings.Contains(value, "%") {
|
||||
return value, true
|
||||
}
|
||||
rendered := util.CleanPath(replacer.Replace(value))
|
||||
if u.Role == "" && strings.Contains(value, "%role%") {
|
||||
return "", false
|
||||
}
|
||||
rendered := util.CleanPath(replacer.render(value))
|
||||
if countPathSegments(rendered) < countPathSegments(value) {
|
||||
return "", false
|
||||
}
|
||||
return rendered, true
|
||||
}
|
||||
|
||||
func (u *User) mergePermissions(group *Group, groupType int, replacer *strings.Replacer) {
|
||||
func (u *User) mergePermissions(group *Group, groupType int, replacer *groupRenderer) {
|
||||
if u.Permissions == nil {
|
||||
u.Permissions = make(map[string][]string)
|
||||
}
|
||||
@@ -1901,7 +1936,7 @@ func (u *User) mergePermissions(group *Group, groupType int, replacer *strings.R
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) mergeFilePatterns(group *Group, groupType int, replacer *strings.Replacer) {
|
||||
func (u *User) mergeFilePatterns(group *Group, groupType int, replacer *groupRenderer) {
|
||||
if len(group.UserSettings.Filters.FilePatterns) > 0 {
|
||||
patternPaths := make(map[string]bool)
|
||||
for _, pattern := range u.Filters.FilePatterns {
|
||||
|
||||
@@ -1218,3 +1218,36 @@ func TestPassiveHost(t *testing.T) {
|
||||
assert.NoError(t, err, ip)
|
||||
assert.Equal(t, "127.0.0.1", ip)
|
||||
}
|
||||
|
||||
func TestLoginMetricsPlaceholderUnset(t *testing.T) {
|
||||
oldConfig := common.Config
|
||||
cfg := common.Config
|
||||
cfg.DefenderConfig.Enabled = true
|
||||
cfg.DefenderConfig.Driver = common.DefenderDriverMemory
|
||||
cfg.DefenderConfig.Threshold = 100
|
||||
cfg.DefenderConfig.ScoreInvalid = 2
|
||||
cfg.DefenderConfig.ScoreValid = 2
|
||||
err := common.Initialize(cfg, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
err := common.Initialize(oldConfig, 0)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
ip := "172.16.34.7"
|
||||
user := dataprovider.User{}
|
||||
user.Username = "ftp_metrics_user"
|
||||
c := &Connection{BaseConnection: common.NewBaseConnection("", common.ProtocolFTP, "", "", user)}
|
||||
|
||||
updateLoginMetrics(&user, ip, dataprovider.LoginMethodPassword, dataprovider.ErrPlaceholderUnset, c)
|
||||
hosts, err := common.GetDefenderHosts()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, hosts)
|
||||
|
||||
updateLoginMetrics(&user, ip, dataprovider.LoginMethodPassword, dataprovider.ErrInvalidCredentials, c)
|
||||
hosts, err = common.GetDefenderHosts()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, hosts, 1)
|
||||
common.DeleteDefenderHost(ip)
|
||||
}
|
||||
|
||||
@@ -464,7 +464,7 @@ func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err err
|
||||
c.clientContext.HasTLSForControl(), info)
|
||||
plugin.Handler.NotifyLogEvent(notifier.LogEventTypeLoginOK, common.ProtocolFTP, user.Username, ip, "", nil)
|
||||
common.DelayLogin(nil)
|
||||
} else if err != common.ErrInternalFailure {
|
||||
} else if err != common.ErrInternalFailure && !errors.Is(err, dataprovider.ErrPlaceholderUnset) {
|
||||
logger.ConnectionFailedLog(user.Username, ip, loginMethod, common.ProtocolFTP, err.Error())
|
||||
event := common.HostEventLoginFailed
|
||||
logEv := notifier.LogEventTypeLoginFailed
|
||||
|
||||
@@ -750,7 +750,8 @@ func updateLoginMetrics(user *dataprovider.User, loginMethod, ip string, err err
|
||||
logger.LoginLog(user.Username, ip, loginMethod, protocol, "", r.UserAgent(), r.TLS != nil, "")
|
||||
plugin.Handler.NotifyLogEvent(notifier.LogEventTypeLoginOK, protocol, user.Username, ip, "", nil)
|
||||
common.DelayLogin(nil)
|
||||
} else if err != common.ErrInternalFailure && err != common.ErrNoCredentials {
|
||||
} else if err != common.ErrInternalFailure && err != common.ErrNoCredentials &&
|
||||
!errors.Is(err, dataprovider.ErrPlaceholderUnset) {
|
||||
logger.ConnectionFailedLog(user.Username, ip, loginMethod, protocol, err.Error())
|
||||
err = handleDefenderEventLoginFailed(ip, err)
|
||||
logEv := notifier.LogEventTypeLoginFailed
|
||||
|
||||
@@ -1598,6 +1598,73 @@ func TestGroupValidation(t *testing.T) {
|
||||
assert.Contains(t, string(resp), "invalid web client options")
|
||||
}
|
||||
|
||||
func TestGroupPlaceholderWithoutValue(t *testing.T) {
|
||||
base := filepath.Join(os.TempDir(), util.GenerateUniqueID())
|
||||
g := getTestGroup()
|
||||
g.UserSettings.HomeDir = filepath.Join(base, ".%role%.")
|
||||
group, _, err := httpdtest.AddGroup(g, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
u := getTestUser()
|
||||
u.Groups = []sdk.GroupMapping{{Name: group.Name, Type: sdk.GroupTypePrimary}}
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = dataprovider.CheckUserAndPass(defaultUsername, defaultPassword, "", common.ProtocolHTTP)
|
||||
assert.ErrorIs(t, err, dataprovider.ErrPlaceholderUnset)
|
||||
_, err = dataprovider.GetUserWithGroupSettings(defaultUsername, "")
|
||||
assert.ErrorIs(t, err, dataprovider.ErrPlaceholderUnset)
|
||||
|
||||
// with a role the token renders a name
|
||||
r := getTestRole()
|
||||
role, _, err := httpdtest.AddRole(r, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
user.Role = role.Name
|
||||
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
merged, err := dataprovider.CheckUserAndPass(defaultUsername, defaultPassword, "", common.ProtocolHTTP)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, filepath.Join(base, "."+role.Name+"."), merged.HomeDir)
|
||||
|
||||
// a folder sub-path is the exception: the mapping the account cannot render
|
||||
// is left unmounted, the login is unaffected. The mount is not created at
|
||||
// the folder root, which would widen the scope the sub-path declares
|
||||
folder, _, err := httpdtest.AddFolder(vfs.BaseVirtualFolder{
|
||||
Name: "tpl_folder",
|
||||
MappedPath: filepath.Join(base, "folder"),
|
||||
}, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
group.UserSettings.HomeDir = ""
|
||||
group.VirtualFolders = []vfs.VirtualFolder{{
|
||||
BaseVirtualFolder: vfs.BaseVirtualFolder{Name: folder.Name},
|
||||
VirtualPath: "/vdir",
|
||||
Subpath: "/tenants%role%",
|
||||
}}
|
||||
group, _, err = httpdtest.UpdateGroup(group, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
merged, err = dataprovider.GetUserWithGroupSettings(defaultUsername, "")
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, merged.VirtualFolders, 1) {
|
||||
assert.Equal(t, "/tenants"+role.Name, merged.VirtualFolders[0].Subpath)
|
||||
}
|
||||
user.Role = ""
|
||||
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
merged, err = dataprovider.GetUserWithGroupSettings(defaultUsername, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, merged.VirtualFolders, "the mapping is dropped, /tenants is not mounted")
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveGroup(group, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveFolder(folder, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveRole(role, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(base)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGroupSettingsOverride(t *testing.T) {
|
||||
mappedPath1 := filepath.Join(os.TempDir(), util.GenerateUniqueID())
|
||||
folderName1 := filepath.Base(mappedPath1)
|
||||
|
||||
@@ -2309,3 +2309,46 @@ func TestOsFsResolvePathSymlinkLoops(t *testing.T) {
|
||||
t.Logf("loop case %q terminated, err=%v", p, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckAuthErrorAccountConfig(t *testing.T) {
|
||||
oldConfig := common.Config
|
||||
cfg := common.Config
|
||||
cfg.DefenderConfig.Enabled = true
|
||||
cfg.DefenderConfig.Driver = common.DefenderDriverMemory
|
||||
cfg.DefenderConfig.Threshold = 100
|
||||
cfg.DefenderConfig.ScoreInvalid = 2
|
||||
cfg.DefenderConfig.ScoreValid = 2
|
||||
err := common.Initialize(cfg, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
err := common.Initialize(oldConfig, 0)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
newAuthError := func(err error, username string) error {
|
||||
return &authenticationError{
|
||||
err: err,
|
||||
loginMethod: dataprovider.SSHLoginMethodPublicKey,
|
||||
username: username,
|
||||
}
|
||||
}
|
||||
ip := "172.16.44.9"
|
||||
checkAuthError(ip, &ssh.ServerAuthError{
|
||||
Errors: []error{newAuthError(dataprovider.ErrPlaceholderUnset, "misconfigured")},
|
||||
})
|
||||
hosts, err := common.GetDefenderHosts()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, hosts)
|
||||
|
||||
checkAuthError(ip, &ssh.ServerAuthError{
|
||||
Errors: []error{
|
||||
newAuthError(dataprovider.ErrPlaceholderUnset, "misconfigured"),
|
||||
newAuthError(dataprovider.ErrInvalidCredentials, "guessed"),
|
||||
},
|
||||
})
|
||||
hosts, err = common.GetDefenderHosts()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, hosts, 1, "the guess following the misconfiguration is scored")
|
||||
common.DeleteDefenderHost(ip)
|
||||
}
|
||||
|
||||
@@ -813,6 +813,9 @@ func checkAuthError(ip string, err error) {
|
||||
// check public key auth errors here
|
||||
for _, err := range authErrors.Errors {
|
||||
if sftpAuthErr, ok := errors.AsType[*authenticationError](err); ok {
|
||||
if errors.Is(err, dataprovider.ErrPlaceholderUnset) {
|
||||
continue
|
||||
}
|
||||
if sftpAuthErr.getLoginMethod() == dataprovider.SSHLoginMethodPublicKey {
|
||||
event := common.HostEventLoginFailed
|
||||
logEv := notifier.LogEventTypeLoginFailed
|
||||
@@ -1330,7 +1333,7 @@ func updateLoginMetrics(user *dataprovider.User, ip, method string, err error) {
|
||||
common.DelayLogin(nil)
|
||||
} else {
|
||||
logger.ConnectionFailedLog(user.Username, ip, method, common.ProtocolSSH, err.Error())
|
||||
if method != dataprovider.SSHLoginMethodPublicKey {
|
||||
if method != dataprovider.SSHLoginMethodPublicKey && !errors.Is(err, dataprovider.ErrPlaceholderUnset) {
|
||||
// some clients try all available public keys for a user, we
|
||||
// record failed login key auth only once for session if the
|
||||
// authentication fails in checkAuthError
|
||||
|
||||
@@ -1872,3 +1872,37 @@ func TestBindingIsValid(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginMetricsPlaceholderUnset(t *testing.T) {
|
||||
oldConfig := common.Config
|
||||
cfg := common.Config
|
||||
cfg.DefenderConfig.Enabled = true
|
||||
cfg.DefenderConfig.Driver = common.DefenderDriverMemory
|
||||
cfg.DefenderConfig.Threshold = 100
|
||||
cfg.DefenderConfig.ScoreInvalid = 2
|
||||
cfg.DefenderConfig.ScoreValid = 2
|
||||
err := common.Initialize(cfg, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
err := common.Initialize(oldConfig, 0)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
ip := "172.16.34.8"
|
||||
user := dataprovider.User{}
|
||||
user.Username = "dav_metrics_user"
|
||||
req, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
updateLoginMetrics(&user, ip, dataprovider.LoginMethodPassword, dataprovider.ErrPlaceholderUnset, req)
|
||||
hosts, err := common.GetDefenderHosts()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, hosts)
|
||||
|
||||
updateLoginMetrics(&user, ip, dataprovider.LoginMethodPassword, dataprovider.ErrInvalidCredentials, req)
|
||||
hosts, err = common.GetDefenderHosts()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, hosts, 1)
|
||||
common.DeleteDefenderHost(ip)
|
||||
}
|
||||
|
||||
@@ -454,7 +454,8 @@ func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err err
|
||||
logger.LoginLog(user.Username, ip, loginMethod, common.ProtocolWebDAV, "", r.UserAgent(), r.TLS != nil, "")
|
||||
plugin.Handler.NotifyLogEvent(notifier.LogEventTypeLoginOK, common.ProtocolWebDAV, user.Username, ip, "", nil)
|
||||
common.DelayLogin(nil)
|
||||
} else if err != common.ErrInternalFailure && err != common.ErrNoCredentials {
|
||||
} else if err != common.ErrInternalFailure && err != common.ErrNoCredentials &&
|
||||
!errors.Is(err, dataprovider.ErrPlaceholderUnset) {
|
||||
logger.ConnectionFailedLog(user.Username, ip, loginMethod, common.ProtocolWebDAV, err.Error())
|
||||
event := common.HostEventLoginFailed
|
||||
logEv := notifier.LogEventTypeLoginFailed
|
||||
|
||||
Reference in New Issue
Block a user