eventmanager: add user expiration check
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
b0cfaf189c
commit
b8496c4d6e
5 changed files with 103 additions and 13 deletions
|
@ -14,6 +14,7 @@ The following actions are supported:
|
||||||
- `Data retention check`. You can define per-folder retention policies.
|
- `Data retention check`. You can define per-folder retention policies.
|
||||||
- `Metadata check`. A metadata check requires a metadata plugin such as [this one](https://github.com/sftpgo/sftpgo-plugin-metadata) and removes the metadata associated to missing items (for example objects deleted outside SFTPGo). A metadata check does nothing is no metadata plugin is installed or external metadata are not supported for a filesystem.
|
- `Metadata check`. A metadata check requires a metadata plugin such as [this one](https://github.com/sftpgo/sftpgo-plugin-metadata) and removes the metadata associated to missing items (for example objects deleted outside SFTPGo). A metadata check does nothing is no metadata plugin is installed or external metadata are not supported for a filesystem.
|
||||||
- `Password expiration check`. You can send an email notification to users whose password is about to expire.
|
- `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.
|
||||||
- `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:
|
- `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.
|
- `Rename`. You can rename one or more files or directories.
|
||||||
- `Delete`. You can delete one or more files and directories.
|
- `Delete`. You can delete one or more files and directories.
|
||||||
|
|
|
@ -1421,7 +1421,6 @@ func executeDeleteFsRuleAction(deletes []string, replacer *strings.Replacer,
|
||||||
if err = executeDeleteFsActionForUser(deletes, replacer, user); err != nil {
|
if err = executeDeleteFsActionForUser(deletes, replacer, user); err != nil {
|
||||||
params.AddError(err)
|
params.AddError(err)
|
||||||
failures = append(failures, user.Username)
|
failures = append(failures, user.Username)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(failures) > 0 {
|
if len(failures) > 0 {
|
||||||
|
@ -1479,7 +1478,6 @@ func executeMkdirFsRuleAction(dirs []string, replacer *strings.Replacer,
|
||||||
executed++
|
executed++
|
||||||
if err = executeMkDirsFsActionForUser(dirs, replacer, user); err != nil {
|
if err = executeMkDirsFsActionForUser(dirs, replacer, user); err != nil {
|
||||||
failures = append(failures, user.Username)
|
failures = append(failures, user.Username)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(failures) > 0 {
|
if len(failures) > 0 {
|
||||||
|
@ -1593,7 +1591,6 @@ func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *string
|
||||||
if err = executeRenameFsActionForUser(renames, replacer, user); err != nil {
|
if err = executeRenameFsActionForUser(renames, replacer, user); err != nil {
|
||||||
failures = append(failures, user.Username)
|
failures = append(failures, user.Username)
|
||||||
params.AddError(err)
|
params.AddError(err)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(failures) > 0 {
|
if len(failures) > 0 {
|
||||||
|
@ -1628,7 +1625,6 @@ func executeCopyFsRuleAction(copy []dataprovider.KeyValue, replacer *strings.Rep
|
||||||
if err = executeCopyFsActionForUser(copy, replacer, user); err != nil {
|
if err = executeCopyFsActionForUser(copy, replacer, user); err != nil {
|
||||||
failures = append(failures, user.Username)
|
failures = append(failures, user.Username)
|
||||||
params.AddError(err)
|
params.AddError(err)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(failures) > 0 {
|
if len(failures) > 0 {
|
||||||
|
@ -1779,7 +1775,6 @@ func executeExistFsRuleAction(exist []string, replacer *strings.Replacer, condit
|
||||||
if err = executeExistFsActionForUser(exist, replacer, user); err != nil {
|
if err = executeExistFsActionForUser(exist, replacer, user); err != nil {
|
||||||
failures = append(failures, user.Username)
|
failures = append(failures, user.Username)
|
||||||
params.AddError(err)
|
params.AddError(err)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(failures) > 0 {
|
if len(failures) > 0 {
|
||||||
|
@ -1814,7 +1809,6 @@ func executeCompressFsRuleAction(c dataprovider.EventActionFsCompress, replacer
|
||||||
if err = executeCompressFsActionForUser(c, replacer, user); err != nil {
|
if err = executeCompressFsActionForUser(c, replacer, user); err != nil {
|
||||||
failures = append(failures, user.Username)
|
failures = append(failures, user.Username)
|
||||||
params.AddError(err)
|
params.AddError(err)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(failures) > 0 {
|
if len(failures) > 0 {
|
||||||
|
@ -1896,7 +1890,6 @@ func executeUsersQuotaResetRuleAction(conditions dataprovider.ConditionOptions,
|
||||||
if err = executeQuotaResetForUser(&user); err != nil {
|
if err = executeQuotaResetForUser(&user); err != nil {
|
||||||
params.AddError(err)
|
params.AddError(err)
|
||||||
failedResets = append(failedResets, user.Username)
|
failedResets = append(failedResets, user.Username)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(failedResets) > 0 {
|
if len(failedResets) > 0 {
|
||||||
|
@ -2045,7 +2038,6 @@ func executeDataRetentionCheckRuleAction(config dataprovider.EventActionDataRete
|
||||||
if err = executeDataRetentionCheckForUser(user, config.Folders, params, actionName); err != nil {
|
if err = executeDataRetentionCheckForUser(user, config.Folders, params, actionName); err != nil {
|
||||||
failedChecks = append(failedChecks, user.Username)
|
failedChecks = append(failedChecks, user.Username)
|
||||||
params.AddError(err)
|
params.AddError(err)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(failedChecks) > 0 {
|
if len(failedChecks) > 0 {
|
||||||
|
@ -2058,6 +2050,40 @@ func executeDataRetentionCheckRuleAction(config dataprovider.EventActionDataRete
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func executeUserExpirationCheckRuleAction(conditions dataprovider.ConditionOptions, params *EventParams) error {
|
||||||
|
users, err := params.getUsers()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get users: %w", err)
|
||||||
|
}
|
||||||
|
var failures []string
|
||||||
|
var executed int
|
||||||
|
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 expiration check for user %q, condition options don't match",
|
||||||
|
user.Username)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
executed++
|
||||||
|
if user.ExpirationDate > 0 {
|
||||||
|
expDate := util.GetTimeFromMsecSinceEpoch(user.ExpirationDate)
|
||||||
|
if expDate.Before(time.Now()) {
|
||||||
|
failures = append(failures, user.Username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(failures) > 0 {
|
||||||
|
return fmt.Errorf("expired users: %+v", failures)
|
||||||
|
}
|
||||||
|
if executed == 0 {
|
||||||
|
eventManagerLog(logger.LevelError, "no user expiration check executed")
|
||||||
|
return errors.New("no user expiration check executed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func executeMetadataCheckForUser(user *dataprovider.User) error {
|
func executeMetadataCheckForUser(user *dataprovider.User) error {
|
||||||
if err := user.LoadAndApplyGroupSettings(); err != nil {
|
if err := user.LoadAndApplyGroupSettings(); err != nil {
|
||||||
eventManagerLog(logger.LevelError, "skipping scheduled quota reset for user %s, cannot apply group settings: %v",
|
eventManagerLog(logger.LevelError, "skipping scheduled quota reset for user %s, cannot apply group settings: %v",
|
||||||
|
@ -2097,7 +2123,6 @@ func executeMetadataCheckRuleAction(conditions dataprovider.ConditionOptions, pa
|
||||||
if err = executeMetadataCheckForUser(&user); err != nil {
|
if err = executeMetadataCheckForUser(&user); err != nil {
|
||||||
params.AddError(err)
|
params.AddError(err)
|
||||||
failures = append(failures, user.Username)
|
failures = append(failures, user.Username)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(failures) > 0 {
|
if len(failures) > 0 {
|
||||||
|
@ -2166,7 +2191,6 @@ func executePwdExpirationCheckRuleAction(config dataprovider.EventActionPassword
|
||||||
if err = executePwdExpirationCheckForUser(&user, config); err != nil {
|
if err = executePwdExpirationCheckForUser(&user, config); err != nil {
|
||||||
params.AddError(err)
|
params.AddError(err)
|
||||||
failures = append(failures, user.Username)
|
failures = append(failures, user.Username)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(failures) > 0 {
|
if len(failures) > 0 {
|
||||||
|
@ -2208,6 +2232,8 @@ func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams,
|
||||||
err = executeFsRuleAction(action.Options.FsConfig, conditions, params)
|
err = executeFsRuleAction(action.Options.FsConfig, conditions, params)
|
||||||
case dataprovider.ActionTypePasswordExpirationCheck:
|
case dataprovider.ActionTypePasswordExpirationCheck:
|
||||||
err = executePwdExpirationCheckRuleAction(action.Options.PwdExpirationConfig, conditions, params)
|
err = executePwdExpirationCheckRuleAction(action.Options.PwdExpirationConfig, conditions, params)
|
||||||
|
case dataprovider.ActionTypeUserExpirationCheck:
|
||||||
|
err = executeUserExpirationCheckRuleAction(conditions, params)
|
||||||
default:
|
default:
|
||||||
err = fmt.Errorf("unsupported action type: %d", action.Type)
|
err = fmt.Errorf("unsupported action type: %d", action.Type)
|
||||||
}
|
}
|
||||||
|
|
|
@ -436,6 +436,8 @@ func TestEventManagerErrors(t *testing.T) {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
err = executeMetadataCheckRuleAction(dataprovider.ConditionOptions{}, &EventParams{})
|
err = executeMetadataCheckRuleAction(dataprovider.ConditionOptions{}, &EventParams{})
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
err = executeUserExpirationCheckRuleAction(dataprovider.ConditionOptions{}, &EventParams{})
|
||||||
|
assert.Error(t, err)
|
||||||
err = executeDeleteFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{})
|
err = executeDeleteFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{})
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
err = executeMkdirFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{})
|
err = executeMkdirFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{})
|
||||||
|
@ -844,6 +846,29 @@ func TestEventRuleActions(t *testing.T) {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.True(t, ActiveMetadataChecks.Remove(username1))
|
assert.True(t, ActiveMetadataChecks.Remove(username1))
|
||||||
|
|
||||||
|
action = dataprovider.BaseEventAction{
|
||||||
|
Type: dataprovider.ActionTypeUserExpirationCheck,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{
|
||||||
|
Names: []dataprovider.ConditionPattern{
|
||||||
|
{
|
||||||
|
Pattern: "don't match",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, getErrorString(err), "no user expiration check executed")
|
||||||
|
|
||||||
|
err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{
|
||||||
|
Names: []dataprovider.ConditionPattern{
|
||||||
|
{
|
||||||
|
Pattern: username1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
dataRetentionAction := dataprovider.BaseEventAction{
|
dataRetentionAction := dataprovider.BaseEventAction{
|
||||||
Type: dataprovider.ActionTypeDataRetentionCheck,
|
Type: dataprovider.ActionTypeDataRetentionCheck,
|
||||||
Options: dataprovider.BaseEventActionOptions{
|
Options: dataprovider.BaseEventActionOptions{
|
||||||
|
@ -1170,6 +1195,32 @@ func TestEventRuleActions(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUserExpirationCheck(t *testing.T) {
|
||||||
|
username := "test_user_expiration_check"
|
||||||
|
user := dataprovider.User{
|
||||||
|
BaseUser: sdk.BaseUser{
|
||||||
|
Username: username,
|
||||||
|
Permissions: map[string][]string{
|
||||||
|
"/": {dataprovider.PermAny},
|
||||||
|
},
|
||||||
|
HomeDir: filepath.Join(os.TempDir(), username),
|
||||||
|
ExpirationDate: util.GetTimeAsMsSinceEpoch(time.Now().Add(-24 * time.Hour)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := dataprovider.AddUser(&user, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = executeUserExpirationCheckRuleAction(dataprovider.ConditionOptions{}, &EventParams{})
|
||||||
|
if assert.Error(t, err) {
|
||||||
|
assert.Contains(t, err.Error(), "expired users")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dataprovider.DeleteUser(username, "", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestEventRuleActionsNoGroupMatching(t *testing.T) {
|
func TestEventRuleActionsNoGroupMatching(t *testing.T) {
|
||||||
username := "test_user_action_group_matching"
|
username := "test_user_action_group_matching"
|
||||||
user := dataprovider.User{
|
user := dataprovider.User{
|
||||||
|
|
|
@ -46,12 +46,14 @@ const (
|
||||||
ActionTypeFilesystem
|
ActionTypeFilesystem
|
||||||
ActionTypeMetadataCheck
|
ActionTypeMetadataCheck
|
||||||
ActionTypePasswordExpirationCheck
|
ActionTypePasswordExpirationCheck
|
||||||
|
ActionTypeUserExpirationCheck
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
supportedEventActions = []int{ActionTypeHTTP, ActionTypeCommand, ActionTypeEmail, ActionTypeFilesystem,
|
supportedEventActions = []int{ActionTypeHTTP, ActionTypeCommand, ActionTypeEmail, ActionTypeFilesystem,
|
||||||
ActionTypeBackup, ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
|
ActionTypeBackup, ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
|
||||||
ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypePasswordExpirationCheck}
|
ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypePasswordExpirationCheck,
|
||||||
|
ActionTypeUserExpirationCheck}
|
||||||
)
|
)
|
||||||
|
|
||||||
func isActionTypeValid(action int) bool {
|
func isActionTypeValid(action int) bool {
|
||||||
|
@ -80,6 +82,8 @@ func getActionTypeAsString(action int) string {
|
||||||
return "Filesystem"
|
return "Filesystem"
|
||||||
case ActionTypePasswordExpirationCheck:
|
case ActionTypePasswordExpirationCheck:
|
||||||
return "Password expiration check"
|
return "Password expiration check"
|
||||||
|
case ActionTypeUserExpirationCheck:
|
||||||
|
return "User expiration check"
|
||||||
default:
|
default:
|
||||||
return "Command"
|
return "Command"
|
||||||
}
|
}
|
||||||
|
@ -1457,7 +1461,8 @@ func (r *EventRule) validateMandatorySyncActions() error {
|
||||||
|
|
||||||
func (r *EventRule) checkIPBlockedAndCertificateActions() error {
|
func (r *EventRule) checkIPBlockedAndCertificateActions() error {
|
||||||
unavailableActions := []int{ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
|
unavailableActions := []int{ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
|
||||||
ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypeFilesystem, ActionTypePasswordExpirationCheck}
|
ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypeFilesystem, ActionTypePasswordExpirationCheck,
|
||||||
|
ActionTypeUserExpirationCheck}
|
||||||
for _, action := range r.Actions {
|
for _, action := range r.Actions {
|
||||||
if util.Contains(unavailableActions, action.Type) {
|
if util.Contains(unavailableActions, action.Type) {
|
||||||
return fmt.Errorf("action %q, type %q is not supported for event trigger %q",
|
return fmt.Errorf("action %q, type %q is not supported for event trigger %q",
|
||||||
|
@ -1472,7 +1477,8 @@ func (r *EventRule) checkProviderEventActions(providerObjectType string) error {
|
||||||
// can be executed only if we modify a user. They will be executed for the
|
// can be executed only if we modify a user. They will be executed for the
|
||||||
// affected user. Folder quota reset can be executed only for folders.
|
// affected user. Folder quota reset can be executed only for folders.
|
||||||
userSpecificActions := []int{ActionTypeUserQuotaReset, ActionTypeTransferQuotaReset,
|
userSpecificActions := []int{ActionTypeUserQuotaReset, ActionTypeTransferQuotaReset,
|
||||||
ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypeFilesystem, ActionTypePasswordExpirationCheck}
|
ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypeFilesystem,
|
||||||
|
ActionTypePasswordExpirationCheck, ActionTypeUserExpirationCheck}
|
||||||
for _, action := range r.Actions {
|
for _, action := range r.Actions {
|
||||||
if util.Contains(userSpecificActions, action.Type) && providerObjectType != actionObjectUser {
|
if util.Contains(userSpecificActions, action.Type) && providerObjectType != actionObjectUser {
|
||||||
return fmt.Errorf("action %q, type %q is only supported for provider user events",
|
return fmt.Errorf("action %q, type %q is only supported for provider user events",
|
||||||
|
|
|
@ -4871,6 +4871,9 @@ components:
|
||||||
- 7
|
- 7
|
||||||
- 8
|
- 8
|
||||||
- 9
|
- 9
|
||||||
|
- 10
|
||||||
|
- 11
|
||||||
|
- 12
|
||||||
description: |
|
description: |
|
||||||
Supported event action types:
|
Supported event action types:
|
||||||
* `1` - HTTP
|
* `1` - HTTP
|
||||||
|
@ -4882,6 +4885,9 @@ components:
|
||||||
* `7` - Transfer quota reset
|
* `7` - Transfer quota reset
|
||||||
* `8` - Data retention check
|
* `8` - Data retention check
|
||||||
* `9` - Filesystem
|
* `9` - Filesystem
|
||||||
|
* `10` - Metadata check
|
||||||
|
* `11` - Password expiration check
|
||||||
|
* `12` - User expiration check
|
||||||
FilesystemActionTypes:
|
FilesystemActionTypes:
|
||||||
type: integer
|
type: integer
|
||||||
enum:
|
enum:
|
||||||
|
|
Loading…
Reference in a new issue