mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-25 00:50:31 +00:00
EventManager: allow to check for inactive users
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
8b2188fcb6
commit
4d357a6a57
17 changed files with 556 additions and 26 deletions
|
@ -14,6 +14,7 @@ The following actions are supported:
|
|||
- `Data retention check`. You can define per-folder retention policies.
|
||||
- `Password expiration check`. You can send an email notification to users whose password is about to expire.
|
||||
- `User expiration check`. You can receive notifications with expired users.
|
||||
- `User inactivity check`. Allow to disable or delete inactive users.
|
||||
- `Identity Provider account check`. You can create/update accounts for users/admins logging in using an Identity Provider.
|
||||
- `Filesystem`. For these actions, the required permissions are automatically granted. This is the same as executing the actions from an SFTP client and the same restrictions applies. Supported actions:
|
||||
- `Rename`. You can rename one or more files or directories.
|
||||
|
|
12
go.mod
12
go.mod
|
@ -36,7 +36,7 @@ require (
|
|||
github.com/hashicorp/go-hclog v1.6.2
|
||||
github.com/hashicorp/go-plugin v1.6.0
|
||||
github.com/hashicorp/go-retryablehttp v0.7.5
|
||||
github.com/jackc/pgx/v5 v5.5.3
|
||||
github.com/jackc/pgx/v5 v5.5.4
|
||||
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
|
||||
github.com/klauspost/compress v1.17.7
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.20
|
||||
|
@ -71,7 +71,7 @@ require (
|
|||
golang.org/x/crypto v0.19.0
|
||||
golang.org/x/net v0.21.0
|
||||
golang.org/x/oauth2 v0.17.0
|
||||
golang.org/x/sys v0.17.0
|
||||
golang.org/x/sys v0.18.0
|
||||
golang.org/x/term v0.17.0
|
||||
golang.org/x/time v0.5.0
|
||||
google.golang.org/api v0.167.0
|
||||
|
@ -167,15 +167,15 @@ require (
|
|||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
|
||||
golang.org/x/mod v0.15.0 // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.18.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-20240228224816-df926f6c8641 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240228224816-df926f6c8641 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240304161311-37d4d3c04a78 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240304161311-37d4d3c04a78 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240304161311-37d4d3c04a78 // indirect
|
||||
google.golang.org/grpc v1.62.0 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
|
|
23
go.sum
23
go.sum
|
@ -238,8 +238,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
|||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.3 h1:Ces6/M3wbDXYpM8JyyPD57ivTtJACFZJd885pdIaV2s=
|
||||
github.com/jackc/pgx/v5 v5.5.3/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||
github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8=
|
||||
github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
|
||||
|
@ -438,8 +438,8 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
|
|||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -487,8 +487,9 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
|
@ -531,12 +532,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-20240228224816-df926f6c8641 h1:GihpvzHjeZHw+/mzsWpdxwr1LaG6E3ff/gyeZlVHbyc=
|
||||
google.golang.org/genproto v0.0.0-20240228224816-df926f6c8641/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240228224816-df926f6c8641 h1:SO1wX9btGFrwj9EzH3ocqfwiPVOxfv4ggAJajzlHA5s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240228224816-df926f6c8641/go.mod h1:wLupoVsUfYPgOMwjzhYFbaVklw/INms+dqTp0tc1fv8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641 h1:DKU1r6Tj5s1vlU/moGhuGz7E3xRfwjdAfDzbsaQJtEY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs=
|
||||
google.golang.org/genproto v0.0.0-20240304161311-37d4d3c04a78 h1:52X7fVGDxXCpRYt6LsKB08TzuA3jOyPHIW2h8QlvR5U=
|
||||
google.golang.org/genproto v0.0.0-20240304161311-37d4d3c04a78/go.mod h1:yA7a1bW1kwl459Ol0m0lV4hLTfrL/7Bkk4Mj2Ir1mWI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240304161311-37d4d3c04a78 h1:SzXBGiWM1LNVYLCRP3e0/Gsze804l4jGoJ5lYysEO5I=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240304161311-37d4d3c04a78/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240304161311-37d4d3c04a78 h1:Xs9lu+tLXxLIfuci70nG4cpwaRC+mRQPUL7LoIeDJC4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240304161311-37d4d3c04a78/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs=
|
||||
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=
|
||||
|
|
|
@ -2354,6 +2354,65 @@ func executeUserExpirationCheckRuleAction(conditions dataprovider.ConditionOptio
|
|||
return nil
|
||||
}
|
||||
|
||||
func executeInactivityCheckForUser(user *dataprovider.User, config dataprovider.EventActionUserInactivity, when time.Time) error {
|
||||
if config.DeleteThreshold > 0 && (user.Status == 0 || config.DisableThreshold == 0) {
|
||||
if inactivityDays := user.InactivityDays(when); inactivityDays > config.DeleteThreshold {
|
||||
err := dataprovider.DeleteUser(user.Username, dataprovider.ActionExecutorSystem, "", "")
|
||||
eventManagerLog(logger.LevelInfo, "deleting inactive user %q, days of inactivity: %d/%d, err: %v",
|
||||
user.Username, inactivityDays, config.DeleteThreshold, err)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to delete inactive user %q", user.Username)
|
||||
}
|
||||
return fmt.Errorf("inactive user %q deleted. Number of days of inactivity: %d", user.Username, inactivityDays)
|
||||
}
|
||||
}
|
||||
if config.DisableThreshold > 0 && user.Status > 0 {
|
||||
if inactivityDays := user.InactivityDays(when); inactivityDays > config.DisableThreshold {
|
||||
user.Status = 0
|
||||
err := dataprovider.UpdateUser(user, dataprovider.ActionExecutorSystem, "", "")
|
||||
eventManagerLog(logger.LevelInfo, "disabling inactive user %q, days of inactivity: %d/%d, err: %v",
|
||||
user.Username, inactivityDays, config.DisableThreshold, err)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to disable inactive user %q", user.Username)
|
||||
}
|
||||
return fmt.Errorf("inactive user %q disabled. Number of days of inactivity: %d", user.Username, inactivityDays)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeUserInactivityCheckRuleAction(config dataprovider.EventActionUserInactivity,
|
||||
conditions dataprovider.ConditionOptions,
|
||||
params *EventParams,
|
||||
when time.Time,
|
||||
) error {
|
||||
users, err := params.getUsers()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get users: %w", err)
|
||||
}
|
||||
var failures []string
|
||||
for _, user := range users {
|
||||
// if sender is set, the conditions have already been evaluated
|
||||
if params.sender == "" {
|
||||
if !checkUserConditionOptions(&user, &conditions) {
|
||||
eventManagerLog(logger.LevelDebug, "skipping inactivity check for user %q, condition options don't match",
|
||||
user.Username)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err = executeInactivityCheckForUser(&user, config, when); err != nil {
|
||||
params.AddError(err)
|
||||
failures = append(failures, user.Username)
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return fmt.Errorf("executed inactivity check actions for users: %s", strings.Join(failures, ", "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func executePwdExpirationCheckForUser(user *dataprovider.User, config dataprovider.EventActionPasswordExpiration) error {
|
||||
if err := user.LoadAndApplyGroupSettings(); err != nil {
|
||||
eventManagerLog(logger.LevelError, "skipping password expiration check for user %q, cannot apply group settings: %v",
|
||||
|
@ -2523,6 +2582,8 @@ func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams,
|
|||
err = executePwdExpirationCheckRuleAction(action.Options.PwdExpirationConfig, conditions, params)
|
||||
case dataprovider.ActionTypeUserExpirationCheck:
|
||||
err = executeUserExpirationCheckRuleAction(conditions, params)
|
||||
case dataprovider.ActionTypeUserInactivityCheck:
|
||||
err = executeUserInactivityCheckRuleAction(action.Options.UserInactivityConfig, conditions, params, time.Now())
|
||||
default:
|
||||
err = fmt.Errorf("unsupported action type: %d", action.Type)
|
||||
}
|
||||
|
|
|
@ -607,6 +607,9 @@ func TestEventManagerErrors(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
err = executeUserExpirationCheckRuleAction(dataprovider.ConditionOptions{}, &EventParams{})
|
||||
assert.Error(t, err)
|
||||
err = executeUserInactivityCheckRuleAction(dataprovider.EventActionUserInactivity{},
|
||||
dataprovider.ConditionOptions{}, &EventParams{}, time.Time{})
|
||||
assert.Error(t, err)
|
||||
err = executeDeleteFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{})
|
||||
assert.Error(t, err)
|
||||
err = executeMkdirFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{})
|
||||
|
@ -2254,3 +2257,173 @@ func TestMetadataReplacement(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.Equal(t, `{"key":"value"} {\"key\":\"value\"}`, string(data))
|
||||
}
|
||||
|
||||
func TestUserInactivityCheck(t *testing.T) {
|
||||
username1 := "user1"
|
||||
username2 := "user2"
|
||||
user1 := dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
Username: username1,
|
||||
HomeDir: filepath.Join(os.TempDir(), username1),
|
||||
Status: 1,
|
||||
Permissions: map[string][]string{
|
||||
"/": {dataprovider.PermAny},
|
||||
},
|
||||
},
|
||||
}
|
||||
user2 := dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
Username: username2,
|
||||
HomeDir: filepath.Join(os.TempDir(), username2),
|
||||
Status: 1,
|
||||
Permissions: map[string][]string{
|
||||
"/": {dataprovider.PermAny},
|
||||
},
|
||||
},
|
||||
}
|
||||
days := user1.InactivityDays(time.Now().Add(10*24*time.Hour + 5*time.Second))
|
||||
assert.Equal(t, 0, days)
|
||||
|
||||
user2.LastLogin = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
err := executeInactivityCheckForUser(&user2, dataprovider.EventActionUserInactivity{
|
||||
DisableThreshold: 10,
|
||||
}, time.Now().Add(12*24*time.Hour))
|
||||
assert.Error(t, err)
|
||||
user2.LastLogin = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
err = executeInactivityCheckForUser(&user2, dataprovider.EventActionUserInactivity{
|
||||
DeleteThreshold: 10,
|
||||
}, time.Now().Add(12*24*time.Hour))
|
||||
assert.Error(t, err)
|
||||
|
||||
err = dataprovider.AddUser(&user1, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
err = dataprovider.AddUser(&user2, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
user1, err = dataprovider.UserExists(username1, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, user1.Status)
|
||||
days = user1.InactivityDays(time.Now().Add(10*24*time.Hour + 5*time.Second))
|
||||
assert.Equal(t, 10, days)
|
||||
days = user1.InactivityDays(time.Now().Add(-10*24*time.Hour + 5*time.Second))
|
||||
assert.Equal(t, -9, days)
|
||||
|
||||
err = executeUserInactivityCheckRuleAction(dataprovider.EventActionUserInactivity{
|
||||
DisableThreshold: 10,
|
||||
}, dataprovider.ConditionOptions{
|
||||
Names: []dataprovider.ConditionPattern{
|
||||
{
|
||||
Pattern: "not matching",
|
||||
},
|
||||
},
|
||||
}, &EventParams{}, time.Now().Add(12*24*time.Hour))
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = executeUserInactivityCheckRuleAction(dataprovider.EventActionUserInactivity{
|
||||
DisableThreshold: 10,
|
||||
}, dataprovider.ConditionOptions{
|
||||
Names: []dataprovider.ConditionPattern{
|
||||
{
|
||||
Pattern: user1.Username,
|
||||
},
|
||||
},
|
||||
}, &EventParams{}, time.Now())
|
||||
assert.NoError(t, err) // no action
|
||||
|
||||
err = executeUserInactivityCheckRuleAction(dataprovider.EventActionUserInactivity{
|
||||
DisableThreshold: 10,
|
||||
}, dataprovider.ConditionOptions{
|
||||
Names: []dataprovider.ConditionPattern{
|
||||
{
|
||||
Pattern: user1.Username,
|
||||
},
|
||||
},
|
||||
}, &EventParams{}, time.Now().Add(-12*24*time.Hour))
|
||||
assert.NoError(t, err) // no action
|
||||
|
||||
err = executeUserInactivityCheckRuleAction(dataprovider.EventActionUserInactivity{
|
||||
DisableThreshold: 10,
|
||||
DeleteThreshold: 20,
|
||||
}, dataprovider.ConditionOptions{
|
||||
Names: []dataprovider.ConditionPattern{
|
||||
{
|
||||
Pattern: user1.Username,
|
||||
},
|
||||
},
|
||||
}, &EventParams{}, time.Now().Add(30*24*time.Hour))
|
||||
// both thresholds exceeded, the user will be disabled
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "executed inactivity check actions for users")
|
||||
}
|
||||
user1, err = dataprovider.UserExists(username1, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, user1.Status)
|
||||
|
||||
err = executeUserInactivityCheckRuleAction(dataprovider.EventActionUserInactivity{
|
||||
DisableThreshold: 10,
|
||||
}, dataprovider.ConditionOptions{
|
||||
Names: []dataprovider.ConditionPattern{
|
||||
{
|
||||
Pattern: user1.Username,
|
||||
},
|
||||
},
|
||||
}, &EventParams{}, time.Now().Add(30*24*time.Hour))
|
||||
assert.NoError(t, err) // already disabled, no action
|
||||
|
||||
err = executeUserInactivityCheckRuleAction(dataprovider.EventActionUserInactivity{
|
||||
DisableThreshold: 10,
|
||||
DeleteThreshold: 20,
|
||||
}, dataprovider.ConditionOptions{
|
||||
Names: []dataprovider.ConditionPattern{
|
||||
{
|
||||
Pattern: user1.Username,
|
||||
},
|
||||
},
|
||||
}, &EventParams{}, time.Now().Add(-30*24*time.Hour))
|
||||
assert.NoError(t, err)
|
||||
err = executeUserInactivityCheckRuleAction(dataprovider.EventActionUserInactivity{
|
||||
DisableThreshold: 10,
|
||||
DeleteThreshold: 20,
|
||||
}, dataprovider.ConditionOptions{
|
||||
Names: []dataprovider.ConditionPattern{
|
||||
{
|
||||
Pattern: user1.Username,
|
||||
},
|
||||
},
|
||||
}, &EventParams{}, time.Now())
|
||||
assert.NoError(t, err)
|
||||
user1, err = dataprovider.UserExists(username1, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, user1.Status)
|
||||
|
||||
err = executeUserInactivityCheckRuleAction(dataprovider.EventActionUserInactivity{
|
||||
DisableThreshold: 10,
|
||||
DeleteThreshold: 20,
|
||||
}, dataprovider.ConditionOptions{
|
||||
Names: []dataprovider.ConditionPattern{
|
||||
{
|
||||
Pattern: user1.Username,
|
||||
},
|
||||
},
|
||||
}, &EventParams{}, time.Now().Add(30*24*time.Hour)) // the user is disabled, will be now deleted
|
||||
assert.Error(t, err)
|
||||
_, err = dataprovider.UserExists(username1, "")
|
||||
assert.ErrorIs(t, err, util.ErrNotFound)
|
||||
|
||||
err = executeUserInactivityCheckRuleAction(dataprovider.EventActionUserInactivity{
|
||||
DeleteThreshold: 20,
|
||||
}, dataprovider.ConditionOptions{
|
||||
Names: []dataprovider.ConditionPattern{
|
||||
{
|
||||
Pattern: user2.Username,
|
||||
},
|
||||
},
|
||||
}, &EventParams{}, time.Now().Add(30*24*time.Hour)) // no disable threshold, user deleted
|
||||
assert.Error(t, err)
|
||||
_, err = dataprovider.UserExists(username2, "")
|
||||
assert.ErrorIs(t, err, util.ErrNotFound)
|
||||
|
||||
err = dataprovider.DeleteUser(username1, "", "", "")
|
||||
assert.Error(t, err)
|
||||
err = dataprovider.DeleteUser(username2, "", "", "")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
|
|
@ -7171,6 +7171,109 @@ func TestEventRuleIPBlocked(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEventRuleInactivityCheck(t *testing.T) {
|
||||
smtpCfg := smtp.Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: 2525,
|
||||
From: "notification@example.com",
|
||||
TemplatesPath: "templates",
|
||||
}
|
||||
err := smtpCfg.Initialize(configDir, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
a1 := dataprovider.BaseEventAction{
|
||||
Name: "a1",
|
||||
Type: dataprovider.ActionTypeUserInactivityCheck,
|
||||
Options: dataprovider.BaseEventActionOptions{
|
||||
UserInactivityConfig: dataprovider.EventActionUserInactivity{
|
||||
DisableThreshold: 10,
|
||||
DeleteThreshold: 20,
|
||||
},
|
||||
},
|
||||
}
|
||||
action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
a2 := dataprovider.BaseEventAction{
|
||||
Name: "a2",
|
||||
Type: dataprovider.ActionTypeEmail,
|
||||
Options: dataprovider.BaseEventActionOptions{
|
||||
EmailConfig: dataprovider.EventActionEmailConfig{
|
||||
Recipients: []string{"success@example.net"},
|
||||
Subject: `OK`,
|
||||
Body: "OK action",
|
||||
},
|
||||
},
|
||||
}
|
||||
action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
r1 := dataprovider.EventRule{
|
||||
Name: "rule1",
|
||||
Status: 1,
|
||||
Trigger: dataprovider.EventTriggerFsEvent,
|
||||
Conditions: dataprovider.EventConditions{
|
||||
FsEvents: []string{"mkdir"},
|
||||
Options: dataprovider.ConditionOptions{
|
||||
Names: []dataprovider.ConditionPattern{
|
||||
{
|
||||
Pattern: user.Username,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Actions: []dataprovider.EventAction{
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action1.Name,
|
||||
},
|
||||
Order: 1,
|
||||
},
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action2.Name,
|
||||
},
|
||||
Order: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
rule1, resp, err := httpdtest.AddEventRule(r1, http.StatusCreated)
|
||||
assert.NoError(t, err, string(resp))
|
||||
conn, client, err := getSftpClient(user)
|
||||
if assert.NoError(t, err) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
|
||||
lastReceivedEmail.reset()
|
||||
err := client.Mkdir("just a test dir")
|
||||
assert.NoError(t, err)
|
||||
// just check that the action is executed
|
||||
assert.Eventually(t, func() bool {
|
||||
return lastReceivedEmail.get().From != ""
|
||||
}, 1500*time.Millisecond, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.Contains(t, email.To, "success@example.net")
|
||||
}
|
||||
|
||||
_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
||||
smtpCfg = smtp.Config{}
|
||||
err = smtpCfg.Initialize(configDir, true)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEventRulePasswordExpiration(t *testing.T) {
|
||||
smtpCfg := smtp.Config{
|
||||
Host: "127.0.0.1",
|
||||
|
|
|
@ -48,13 +48,14 @@ const (
|
|||
ActionTypePasswordExpirationCheck
|
||||
ActionTypeUserExpirationCheck
|
||||
ActionTypeIDPAccountCheck
|
||||
ActionTypeUserInactivityCheck
|
||||
)
|
||||
|
||||
var (
|
||||
supportedEventActions = []int{ActionTypeHTTP, ActionTypeCommand, ActionTypeEmail, ActionTypeFilesystem,
|
||||
ActionTypeBackup, ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
|
||||
ActionTypeDataRetentionCheck, ActionTypePasswordExpirationCheck,
|
||||
ActionTypeUserExpirationCheck, ActionTypeIDPAccountCheck}
|
||||
ActionTypeUserExpirationCheck, ActionTypeUserInactivityCheck, ActionTypeIDPAccountCheck}
|
||||
)
|
||||
|
||||
func isActionTypeValid(action int) bool {
|
||||
|
@ -83,6 +84,8 @@ func getActionTypeAsString(action int) string {
|
|||
return util.I18nActionTypePwdExpirationCheck
|
||||
case ActionTypeUserExpirationCheck:
|
||||
return util.I18nActionTypeUserExpirationCheck
|
||||
case ActionTypeUserInactivityCheck:
|
||||
return util.I18nActionTypeUserInactivityCheck
|
||||
case ActionTypeIDPAccountCheck:
|
||||
return util.I18nActionTypeIDPCheck
|
||||
default:
|
||||
|
@ -915,6 +918,38 @@ func (c *EventActionPasswordExpiration) validate() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// EventActionUserInactivity defines the configuration for user inactivity checks.
|
||||
type EventActionUserInactivity struct {
|
||||
// DisableThreshold defines inactivity in days, since the last login before disabling the account
|
||||
DisableThreshold int `json:"disable_threshold,omitempty"`
|
||||
// DeleteThreshold defines inactivity in days, since the last login before deleting the account
|
||||
DeleteThreshold int `json:"delete_threshold,omitempty"`
|
||||
}
|
||||
|
||||
func (c *EventActionUserInactivity) validate() error {
|
||||
if c.DeleteThreshold < 0 {
|
||||
c.DeleteThreshold = 0
|
||||
}
|
||||
if c.DisableThreshold < 0 {
|
||||
c.DisableThreshold = 0
|
||||
}
|
||||
if c.DisableThreshold == 0 && c.DeleteThreshold == 0 {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("at least a threshold must be defined"),
|
||||
util.I18nActionThresholdRequired,
|
||||
)
|
||||
}
|
||||
if c.DeleteThreshold > 0 && c.DisableThreshold > 0 {
|
||||
if c.DeleteThreshold <= c.DisableThreshold {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("deletion threshold %d must be greater than deactivation threshold: %d", c.DeleteThreshold, c.DisableThreshold)),
|
||||
util.I18nActionThresholdsInvalid,
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EventActionIDPAccountCheck defines the check to execute after a successful IDP login
|
||||
type EventActionIDPAccountCheck struct {
|
||||
// 0 create/update, 1 create the account if it doesn't exist
|
||||
|
@ -938,13 +973,14 @@ func (c *EventActionIDPAccountCheck) validate() error {
|
|||
|
||||
// BaseEventActionOptions defines the supported configuration options for a base event actions
|
||||
type BaseEventActionOptions struct {
|
||||
HTTPConfig EventActionHTTPConfig `json:"http_config"`
|
||||
CmdConfig EventActionCommandConfig `json:"cmd_config"`
|
||||
EmailConfig EventActionEmailConfig `json:"email_config"`
|
||||
RetentionConfig EventActionDataRetentionConfig `json:"retention_config"`
|
||||
FsConfig EventActionFilesystemConfig `json:"fs_config"`
|
||||
PwdExpirationConfig EventActionPasswordExpiration `json:"pwd_expiration_config"`
|
||||
IDPConfig EventActionIDPAccountCheck `json:"idp_config"`
|
||||
HTTPConfig EventActionHTTPConfig `json:"http_config"`
|
||||
CmdConfig EventActionCommandConfig `json:"cmd_config"`
|
||||
EmailConfig EventActionEmailConfig `json:"email_config"`
|
||||
RetentionConfig EventActionDataRetentionConfig `json:"retention_config"`
|
||||
FsConfig EventActionFilesystemConfig `json:"fs_config"`
|
||||
PwdExpirationConfig EventActionPasswordExpiration `json:"pwd_expiration_config"`
|
||||
UserInactivityConfig EventActionUserInactivity `json:"user_inactivity_config"`
|
||||
IDPConfig EventActionIDPAccountCheck `json:"idp_config"`
|
||||
}
|
||||
|
||||
func (o *BaseEventActionOptions) getACopy() BaseEventActionOptions {
|
||||
|
@ -1009,6 +1045,10 @@ func (o *BaseEventActionOptions) getACopy() BaseEventActionOptions {
|
|||
PwdExpirationConfig: EventActionPasswordExpiration{
|
||||
Threshold: o.PwdExpirationConfig.Threshold,
|
||||
},
|
||||
UserInactivityConfig: EventActionUserInactivity{
|
||||
DisableThreshold: o.UserInactivityConfig.DisableThreshold,
|
||||
DeleteThreshold: o.UserInactivityConfig.DeleteThreshold,
|
||||
},
|
||||
IDPConfig: EventActionIDPAccountCheck{
|
||||
Mode: o.IDPConfig.Mode,
|
||||
TemplateUser: o.IDPConfig.TemplateUser,
|
||||
|
@ -1047,6 +1087,7 @@ func (o *BaseEventActionOptions) validate(action int, name string) error {
|
|||
o.FsConfig = EventActionFilesystemConfig{}
|
||||
o.PwdExpirationConfig = EventActionPasswordExpiration{}
|
||||
o.IDPConfig = EventActionIDPAccountCheck{}
|
||||
o.UserInactivityConfig = EventActionUserInactivity{}
|
||||
return o.HTTPConfig.validate(name)
|
||||
case ActionTypeCommand:
|
||||
o.HTTPConfig = EventActionHTTPConfig{}
|
||||
|
@ -1055,6 +1096,7 @@ func (o *BaseEventActionOptions) validate(action int, name string) error {
|
|||
o.FsConfig = EventActionFilesystemConfig{}
|
||||
o.PwdExpirationConfig = EventActionPasswordExpiration{}
|
||||
o.IDPConfig = EventActionIDPAccountCheck{}
|
||||
o.UserInactivityConfig = EventActionUserInactivity{}
|
||||
return o.CmdConfig.validate()
|
||||
case ActionTypeEmail:
|
||||
o.HTTPConfig = EventActionHTTPConfig{}
|
||||
|
@ -1063,6 +1105,7 @@ func (o *BaseEventActionOptions) validate(action int, name string) error {
|
|||
o.FsConfig = EventActionFilesystemConfig{}
|
||||
o.PwdExpirationConfig = EventActionPasswordExpiration{}
|
||||
o.IDPConfig = EventActionIDPAccountCheck{}
|
||||
o.UserInactivityConfig = EventActionUserInactivity{}
|
||||
return o.EmailConfig.validate()
|
||||
case ActionTypeDataRetentionCheck:
|
||||
o.HTTPConfig = EventActionHTTPConfig{}
|
||||
|
@ -1071,6 +1114,7 @@ func (o *BaseEventActionOptions) validate(action int, name string) error {
|
|||
o.FsConfig = EventActionFilesystemConfig{}
|
||||
o.PwdExpirationConfig = EventActionPasswordExpiration{}
|
||||
o.IDPConfig = EventActionIDPAccountCheck{}
|
||||
o.UserInactivityConfig = EventActionUserInactivity{}
|
||||
return o.RetentionConfig.validate()
|
||||
case ActionTypeFilesystem:
|
||||
o.HTTPConfig = EventActionHTTPConfig{}
|
||||
|
@ -1079,6 +1123,7 @@ func (o *BaseEventActionOptions) validate(action int, name string) error {
|
|||
o.RetentionConfig = EventActionDataRetentionConfig{}
|
||||
o.PwdExpirationConfig = EventActionPasswordExpiration{}
|
||||
o.IDPConfig = EventActionIDPAccountCheck{}
|
||||
o.UserInactivityConfig = EventActionUserInactivity{}
|
||||
return o.FsConfig.validate()
|
||||
case ActionTypePasswordExpirationCheck:
|
||||
o.HTTPConfig = EventActionHTTPConfig{}
|
||||
|
@ -1087,7 +1132,17 @@ func (o *BaseEventActionOptions) validate(action int, name string) error {
|
|||
o.RetentionConfig = EventActionDataRetentionConfig{}
|
||||
o.FsConfig = EventActionFilesystemConfig{}
|
||||
o.IDPConfig = EventActionIDPAccountCheck{}
|
||||
o.UserInactivityConfig = EventActionUserInactivity{}
|
||||
return o.PwdExpirationConfig.validate()
|
||||
case ActionTypeUserInactivityCheck:
|
||||
o.HTTPConfig = EventActionHTTPConfig{}
|
||||
o.CmdConfig = EventActionCommandConfig{}
|
||||
o.EmailConfig = EventActionEmailConfig{}
|
||||
o.RetentionConfig = EventActionDataRetentionConfig{}
|
||||
o.FsConfig = EventActionFilesystemConfig{}
|
||||
o.IDPConfig = EventActionIDPAccountCheck{}
|
||||
o.PwdExpirationConfig = EventActionPasswordExpiration{}
|
||||
return o.UserInactivityConfig.validate()
|
||||
case ActionTypeIDPAccountCheck:
|
||||
o.HTTPConfig = EventActionHTTPConfig{}
|
||||
o.CmdConfig = EventActionCommandConfig{}
|
||||
|
@ -1095,6 +1150,7 @@ func (o *BaseEventActionOptions) validate(action int, name string) error {
|
|||
o.RetentionConfig = EventActionDataRetentionConfig{}
|
||||
o.FsConfig = EventActionFilesystemConfig{}
|
||||
o.PwdExpirationConfig = EventActionPasswordExpiration{}
|
||||
o.UserInactivityConfig = EventActionUserInactivity{}
|
||||
return o.IDPConfig.validate()
|
||||
default:
|
||||
o.HTTPConfig = EventActionHTTPConfig{}
|
||||
|
@ -1104,6 +1160,7 @@ func (o *BaseEventActionOptions) validate(action int, name string) error {
|
|||
o.FsConfig = EventActionFilesystemConfig{}
|
||||
o.PwdExpirationConfig = EventActionPasswordExpiration{}
|
||||
o.IDPConfig = EventActionIDPAccountCheck{}
|
||||
o.UserInactivityConfig = EventActionUserInactivity{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1130,6 +1130,22 @@ func (u *User) CanCopyFromWeb(src, dest string) bool {
|
|||
return u.HasPerm(PermCopy, src) && u.HasPerm(PermCopy, dest)
|
||||
}
|
||||
|
||||
// InactivityDays returns the number of days of inactivity
|
||||
func (u *User) InactivityDays(when time.Time) int {
|
||||
if when.IsZero() {
|
||||
when = time.Now()
|
||||
}
|
||||
lastActivity := u.LastLogin
|
||||
if lastActivity == 0 {
|
||||
lastActivity = u.CreatedAt
|
||||
}
|
||||
if lastActivity == 0 {
|
||||
// unable to determine inactivity
|
||||
return 0
|
||||
}
|
||||
return int(float64(when.Sub(util.GetTimeFromMsecSinceEpoch(lastActivity))) / float64(24*time.Hour))
|
||||
}
|
||||
|
||||
// PasswordExpiresIn returns the number of days before the password expires.
|
||||
// The returned value is negative if the password is expired.
|
||||
// The caller must ensure that a PasswordExpiration is set
|
||||
|
|
|
@ -1827,6 +1827,16 @@ func TestBasicActionRulesHandling(t *testing.T) {
|
|||
_, _, err = httpdtest.UpdateEventAction(a, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
||||
a.Type = dataprovider.ActionTypeUserInactivityCheck
|
||||
a.Options = dataprovider.BaseEventActionOptions{
|
||||
UserInactivityConfig: dataprovider.EventActionUserInactivity{
|
||||
DisableThreshold: 10,
|
||||
DeleteThreshold: 20,
|
||||
},
|
||||
}
|
||||
_, _, err = httpdtest.UpdateEventAction(a, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
||||
a.Type = dataprovider.ActionTypeHTTP
|
||||
a.Options = dataprovider.BaseEventActionOptions{
|
||||
HTTPConfig: dataprovider.EventActionHTTPConfig{
|
||||
|
@ -2501,6 +2511,25 @@ func TestEventActionValidation(t *testing.T) {
|
|||
_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(resp), "invalid account check mode")
|
||||
action.Type = dataprovider.ActionTypeUserInactivityCheck
|
||||
action.Options = dataprovider.BaseEventActionOptions{
|
||||
UserInactivityConfig: dataprovider.EventActionUserInactivity{
|
||||
DisableThreshold: 0,
|
||||
DeleteThreshold: 0,
|
||||
},
|
||||
}
|
||||
_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(resp), "at least a threshold must be defined")
|
||||
action.Options = dataprovider.BaseEventActionOptions{
|
||||
UserInactivityConfig: dataprovider.EventActionUserInactivity{
|
||||
DisableThreshold: 10,
|
||||
DeleteThreshold: 10,
|
||||
},
|
||||
}
|
||||
_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(resp), "must be greater than deactivation threshold")
|
||||
}
|
||||
|
||||
func TestEventRuleValidation(t *testing.T) {
|
||||
|
@ -23137,6 +23166,28 @@ func TestWebEventAction(t *testing.T) {
|
|||
assert.Equal(t, 0, actionGet.Options.CmdConfig.Timeout)
|
||||
assert.Len(t, actionGet.Options.CmdConfig.EnvVars, 0)
|
||||
|
||||
action.Type = dataprovider.ActionTypeUserInactivityCheck
|
||||
action.Options.UserInactivityConfig = dataprovider.EventActionUserInactivity{
|
||||
DisableThreshold: 10,
|
||||
DeleteThreshold: 15,
|
||||
}
|
||||
form.Set("type", fmt.Sprintf("%d", action.Type))
|
||||
form.Set("inactivity_disable_threshold", strconv.Itoa(action.Options.UserInactivityConfig.DisableThreshold))
|
||||
form.Set("inactivity_delete_threshold", strconv.Itoa(action.Options.UserInactivityConfig.DeleteThreshold))
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),
|
||||
bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
setJWTCookieForReq(req, webToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusSeeOther, rr)
|
||||
actionGet, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, action.Type, actionGet.Type)
|
||||
assert.Equal(t, 0, actionGet.Options.PwdExpirationConfig.Threshold)
|
||||
assert.Equal(t, action.Options.UserInactivityConfig.DisableThreshold, actionGet.Options.UserInactivityConfig.DisableThreshold)
|
||||
assert.Equal(t, action.Options.UserInactivityConfig.DeleteThreshold, actionGet.Options.UserInactivityConfig.DeleteThreshold)
|
||||
|
||||
action.Type = dataprovider.ActionTypeIDPAccountCheck
|
||||
form.Set("type", fmt.Sprintf("%d", action.Type))
|
||||
form.Set("idp_mode", "1")
|
||||
|
|
|
@ -2312,6 +2312,13 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
|
|||
if err != nil {
|
||||
return dataprovider.BaseEventActionOptions{}, fmt.Errorf("invalid password expiration threshold: %w", err)
|
||||
}
|
||||
var disableThreshold, deleteThreshold int
|
||||
if val, err := strconv.Atoi(r.Form.Get("inactivity_disable_threshold")); err == nil {
|
||||
disableThreshold = val
|
||||
}
|
||||
if val, err := strconv.Atoi(r.Form.Get("inactivity_delete_threshold")); err == nil {
|
||||
deleteThreshold = val
|
||||
}
|
||||
var emailAttachments []string
|
||||
if r.Form.Get("email_attachments") != "" {
|
||||
emailAttachments = getSliceFromDelimitedValues(r.Form.Get("email_attachments"), ",")
|
||||
|
@ -2373,6 +2380,10 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
|
|||
PwdExpirationConfig: dataprovider.EventActionPasswordExpiration{
|
||||
Threshold: pwdExpirationThreshold,
|
||||
},
|
||||
UserInactivityConfig: dataprovider.EventActionUserInactivity{
|
||||
DisableThreshold: disableThreshold,
|
||||
DeleteThreshold: deleteThreshold,
|
||||
},
|
||||
IDPConfig: dataprovider.EventActionIDPAccountCheck{
|
||||
Mode: idpMode,
|
||||
TemplateUser: strings.TrimSpace(r.Form.Get("idp_user")),
|
||||
|
|
|
@ -1600,6 +1600,12 @@ func checkEventAction(expected, actual dataprovider.BaseEventAction) error {
|
|||
if expected.Options.PwdExpirationConfig.Threshold != actual.Options.PwdExpirationConfig.Threshold {
|
||||
return errors.New("password expiration threshold mismatch")
|
||||
}
|
||||
if expected.Options.UserInactivityConfig.DisableThreshold != actual.Options.UserInactivityConfig.DisableThreshold {
|
||||
return errors.New("user inactivity disable threshold mismatch")
|
||||
}
|
||||
if expected.Options.UserInactivityConfig.DeleteThreshold != actual.Options.UserInactivityConfig.DeleteThreshold {
|
||||
return errors.New("user inactivity delete threshold mismatch")
|
||||
}
|
||||
if err := compareEventActionIDPConfigFields(expected.Options.IDPConfig, actual.Options.IDPConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -276,6 +276,7 @@ const (
|
|||
I18nActionTypeFilesystem = "actions.types.filesystem"
|
||||
I18nActionTypePwdExpirationCheck = "actions.types.password_expiration_check"
|
||||
I18nActionTypeUserExpirationCheck = "actions.types.user_expiration_check"
|
||||
I18nActionTypeUserInactivityCheck = "actions.types.user_inactivity_check"
|
||||
I18nActionTypeIDPCheck = "actions.types.idp_check"
|
||||
I18nActionTypeCommand = "actions.types.command"
|
||||
I18nActionFsTypeRename = "actions.fs_types.rename"
|
||||
|
@ -284,6 +285,8 @@ const (
|
|||
I18nActionFsTypeCompress = "actions.fs_types.compress"
|
||||
I18nActionFsTypeCopy = "actions.fs_types.copy"
|
||||
I18nActionFsTypeCreateDirs = "actions.fs_types.create_dirs"
|
||||
I18nActionThresholdRequired = "actions.inactivity_threshold_required"
|
||||
I18nActionThresholdsInvalid = "actions.inactivity_thresholds_invalid"
|
||||
I18nTriggerFsEvent = "rules.triggers.fs_event"
|
||||
I18nTriggerProviderEvent = "rules.triggers.provider_event"
|
||||
I18nTriggerIPBlockedEvent = "rules.triggers.ip_blocked"
|
||||
|
|
|
@ -5004,10 +5004,10 @@ components:
|
|||
- 7
|
||||
- 8
|
||||
- 9
|
||||
- 10
|
||||
- 11
|
||||
- 12
|
||||
- 13
|
||||
- 14
|
||||
description: |
|
||||
Supported event action types:
|
||||
* `1` - HTTP
|
||||
|
@ -5022,6 +5022,7 @@ components:
|
|||
* `11` - Password expiration check
|
||||
* `12` - User expiration check
|
||||
* `13` - Identity Provider account check
|
||||
* `14` - User inactivity check
|
||||
FilesystemActionTypes:
|
||||
type: integer
|
||||
enum:
|
||||
|
@ -7087,6 +7088,15 @@ components:
|
|||
threshold:
|
||||
type: integer
|
||||
description: 'An email notification will be generated for users whose password expires in a number of days less than or equal to this threshold'
|
||||
EventActionUserInactivity:
|
||||
type: object
|
||||
properties:
|
||||
disable_threshold:
|
||||
type: integer
|
||||
description: 'Inactivity threshold, in days, before disabling the account'
|
||||
delete_threshold:
|
||||
type: integer
|
||||
description: 'Inactivity threshold, in days, before deleting the account'
|
||||
EventActionIDPAccountCheck:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -7120,6 +7130,8 @@ components:
|
|||
$ref: '#/components/schemas/EventActionFilesystemConfig'
|
||||
pwd_expiration_config:
|
||||
$ref: '#/components/schemas/EventActionPasswordExpiration'
|
||||
user_inactivity_config:
|
||||
$ref: '#/components/schemas/EventActionUserInactivity'
|
||||
idp_config:
|
||||
$ref: '#/components/schemas/EventActionIDPAccountCheck'
|
||||
BaseEventAction:
|
||||
|
|
|
@ -934,6 +934,12 @@
|
|||
"idp_template_required": "A user or admin template is required",
|
||||
"threshold": "Threshold",
|
||||
"threshold_help": "An email notification will be generated for users whose password expires in a number of days less than or equal to this threshold",
|
||||
"disable_threshold": "Disable threshold",
|
||||
"disable_threshold_help": "Inactivity in days, since last login or creation before disabling users",
|
||||
"delete_threshold": "Delete threshold",
|
||||
"delete_threshold_help": "Inactivity in days, since last login or creation before deleting users",
|
||||
"inactivity_threshold_required": "At least one inactivity threshold must be defined",
|
||||
"inactivity_thresholds_invalid": "The deletion threshold must be greater than the deactivation threshold",
|
||||
"idp_mode_add_update": "Create or update",
|
||||
"idp_mode_add": "Create if it doesn't exist",
|
||||
"template_user_help": "Template for SFTPGo users in JSON format. Placeholders are supported",
|
||||
|
@ -984,6 +990,7 @@
|
|||
"filesystem": "Filesystem",
|
||||
"password_expiration_check": "Password expiration check",
|
||||
"user_expiration_check": "User expiration check",
|
||||
"user_inactivity_check": "User inactivity check",
|
||||
"idp_check": "Identity Provider account check",
|
||||
"command": "Command"
|
||||
},
|
||||
|
|
|
@ -934,6 +934,12 @@
|
|||
"idp_template_required": "Un modello di utenti o amministratori è obbligatorio",
|
||||
"threshold": "Soglia",
|
||||
"threshold_help": "Verrà generata una notifica email per gli utenti la cui password scade tra un numero di giorni inferiore o uguale a questa soglia",
|
||||
"disable_threshold": "Soglia disabilitazione",
|
||||
"disable_threshold_help": "Inattività in giorni, dall'ultimo login o dalla creazione prima della disabilitazione degli utenti",
|
||||
"delete_threshold": "Soglia eliminazione",
|
||||
"delete_threshold_help": "Inattività in giorni, dall'ultimo login o dalla creazione prima dell'eliminazione degli utenti",
|
||||
"inactivity_threshold_required": "È necessario definire almeno una soglia di inattività",
|
||||
"inactivity_thresholds_invalid": "La soglia di eliminazione deve essere maggiore della soglia di disattivazione",
|
||||
"idp_mode_add_update": "Crea o aggiorna",
|
||||
"idp_mode_add": "Crea se non esiste",
|
||||
"template_user_help": "Modello per gli utenti SFTPGo in formato JSON. I segnaposto sono supportati",
|
||||
|
@ -984,6 +990,7 @@
|
|||
"filesystem": "Filesystem",
|
||||
"password_expiration_check": "Controllo password scadute",
|
||||
"user_expiration_check": "Controllo utenti scaduti",
|
||||
"user_inactivity_check": "Controllo inattività utente",
|
||||
"idp_check": "Controllo account Identity Provider",
|
||||
"command": "Comando"
|
||||
},
|
||||
|
|
|
@ -58,6 +58,22 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row action-type action-user-inactivity mt-10">
|
||||
<label for="idInactivityThresholdDisable" data-i18n="actions.disable_threshold" class="col-md-3 col-form-label">Disable Threshold</label>
|
||||
<div class="col-md-9">
|
||||
<input id="idInactivityThresholdDisable" type="number" min="0" class="form-control" name="inactivity_disable_threshold" value="{{.Action.Options.UserInactivityConfig.DisableThreshold}}" aria-describedby="idInactivityThresholdDisableHelp" />
|
||||
<div id="idInactivityThresholdDisableHelp" class="form-text" data-i18n="actions.disable_threshold_help"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row action-type action-user-inactivity mt-10">
|
||||
<label for="idInactivityThresholdDelete" data-i18n="actions.delete_threshold" class="col-md-3 col-form-label">Delete Threshold</label>
|
||||
<div class="col-md-9">
|
||||
<input id="idInactivityThresholdDelete" type="number" min="0" class="form-control" name="inactivity_delete_threshold" value="{{.Action.Options.UserInactivityConfig.DeleteThreshold}}" aria-describedby="idInactivityThresholdDeleteHelp" />
|
||||
<div id="idInactivityThresholdDeleteHelp" class="form-text" data-i18n="actions.delete_threshold_help"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group action-type action-idp row mt-10">
|
||||
<label for="idIDPMode" data-i18n="general.mode" class="col-md-3 col-form-label">Mode</label>
|
||||
<div class="col-md-9">
|
||||
|
@ -976,6 +992,9 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
case '13':
|
||||
$('.action-idp').show();
|
||||
break;
|
||||
case '14':
|
||||
$('.action-user-inactivity').show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -190,6 +190,8 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
return $.t('actions.types.user_expiration_check');
|
||||
case 13:
|
||||
return $.t('actions.types.idp_check');
|
||||
case 14:
|
||||
return $.t('actions.types.user_inactivity_check');
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue