Browse Source

eventmanager: add user expiration check

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino 2 years ago
parent
commit
b8496c4d6e

+ 1 - 0
docs/eventmanager.md

@@ -14,6 +14,7 @@ The following actions are supported:
 - `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.
 - `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:
   - `Rename`. You can rename one or more files or directories.
   - `Delete`. You can delete one or more files and directories.

+ 36 - 10
internal/common/eventmanager.go

@@ -1421,7 +1421,6 @@ func executeDeleteFsRuleAction(deletes []string, replacer *strings.Replacer,
 		if err = executeDeleteFsActionForUser(deletes, replacer, user); err != nil {
 			params.AddError(err)
 			failures = append(failures, user.Username)
-			continue
 		}
 	}
 	if len(failures) > 0 {
@@ -1479,7 +1478,6 @@ func executeMkdirFsRuleAction(dirs []string, replacer *strings.Replacer,
 		executed++
 		if err = executeMkDirsFsActionForUser(dirs, replacer, user); err != nil {
 			failures = append(failures, user.Username)
-			continue
 		}
 	}
 	if len(failures) > 0 {
@@ -1593,7 +1591,6 @@ func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *string
 		if err = executeRenameFsActionForUser(renames, replacer, user); err != nil {
 			failures = append(failures, user.Username)
 			params.AddError(err)
-			continue
 		}
 	}
 	if len(failures) > 0 {
@@ -1628,7 +1625,6 @@ func executeCopyFsRuleAction(copy []dataprovider.KeyValue, replacer *strings.Rep
 		if err = executeCopyFsActionForUser(copy, replacer, user); err != nil {
 			failures = append(failures, user.Username)
 			params.AddError(err)
-			continue
 		}
 	}
 	if len(failures) > 0 {
@@ -1779,7 +1775,6 @@ func executeExistFsRuleAction(exist []string, replacer *strings.Replacer, condit
 		if err = executeExistFsActionForUser(exist, replacer, user); err != nil {
 			failures = append(failures, user.Username)
 			params.AddError(err)
-			continue
 		}
 	}
 	if len(failures) > 0 {
@@ -1814,7 +1809,6 @@ func executeCompressFsRuleAction(c dataprovider.EventActionFsCompress, replacer
 		if err = executeCompressFsActionForUser(c, replacer, user); err != nil {
 			failures = append(failures, user.Username)
 			params.AddError(err)
-			continue
 		}
 	}
 	if len(failures) > 0 {
@@ -1896,7 +1890,6 @@ func executeUsersQuotaResetRuleAction(conditions dataprovider.ConditionOptions,
 		if err = executeQuotaResetForUser(&user); err != nil {
 			params.AddError(err)
 			failedResets = append(failedResets, user.Username)
-			continue
 		}
 	}
 	if len(failedResets) > 0 {
@@ -2045,7 +2038,6 @@ func executeDataRetentionCheckRuleAction(config dataprovider.EventActionDataRete
 		if err = executeDataRetentionCheckForUser(user, config.Folders, params, actionName); err != nil {
 			failedChecks = append(failedChecks, user.Username)
 			params.AddError(err)
-			continue
 		}
 	}
 	if len(failedChecks) > 0 {
@@ -2058,6 +2050,40 @@ func executeDataRetentionCheckRuleAction(config dataprovider.EventActionDataRete
 	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 {
 	if err := user.LoadAndApplyGroupSettings(); err != nil {
 		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 {
 			params.AddError(err)
 			failures = append(failures, user.Username)
-			continue
 		}
 	}
 	if len(failures) > 0 {
@@ -2166,7 +2191,6 @@ func executePwdExpirationCheckRuleAction(config dataprovider.EventActionPassword
 		if err = executePwdExpirationCheckForUser(&user, config); err != nil {
 			params.AddError(err)
 			failures = append(failures, user.Username)
-			continue
 		}
 	}
 	if len(failures) > 0 {
@@ -2208,6 +2232,8 @@ func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams,
 		err = executeFsRuleAction(action.Options.FsConfig, conditions, params)
 	case dataprovider.ActionTypePasswordExpirationCheck:
 		err = executePwdExpirationCheckRuleAction(action.Options.PwdExpirationConfig, conditions, params)
+	case dataprovider.ActionTypeUserExpirationCheck:
+		err = executeUserExpirationCheckRuleAction(conditions, params)
 	default:
 		err = fmt.Errorf("unsupported action type: %d", action.Type)
 	}

+ 51 - 0
internal/common/eventmanager_test.go

@@ -436,6 +436,8 @@ func TestEventManagerErrors(t *testing.T) {
 	assert.Error(t, err)
 	err = executeMetadataCheckRuleAction(dataprovider.ConditionOptions{}, &EventParams{})
 	assert.Error(t, err)
+	err = executeUserExpirationCheckRuleAction(dataprovider.ConditionOptions{}, &EventParams{})
+	assert.Error(t, err)
 	err = executeDeleteFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{})
 	assert.Error(t, err)
 	err = executeMkdirFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{})
@@ -844,6 +846,29 @@ func TestEventRuleActions(t *testing.T) {
 	assert.Error(t, err)
 	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{
 		Type: dataprovider.ActionTypeDataRetentionCheck,
 		Options: dataprovider.BaseEventActionOptions{
@@ -1170,6 +1195,32 @@ func TestEventRuleActions(t *testing.T) {
 	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) {
 	username := "test_user_action_group_matching"
 	user := dataprovider.User{

+ 9 - 3
internal/dataprovider/eventrule.go

@@ -46,12 +46,14 @@ const (
 	ActionTypeFilesystem
 	ActionTypeMetadataCheck
 	ActionTypePasswordExpirationCheck
+	ActionTypeUserExpirationCheck
 )
 
 var (
 	supportedEventActions = []int{ActionTypeHTTP, ActionTypeCommand, ActionTypeEmail, ActionTypeFilesystem,
 		ActionTypeBackup, ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
-		ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypePasswordExpirationCheck}
+		ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypePasswordExpirationCheck,
+		ActionTypeUserExpirationCheck}
 )
 
 func isActionTypeValid(action int) bool {
@@ -80,6 +82,8 @@ func getActionTypeAsString(action int) string {
 		return "Filesystem"
 	case ActionTypePasswordExpirationCheck:
 		return "Password expiration check"
+	case ActionTypeUserExpirationCheck:
+		return "User expiration check"
 	default:
 		return "Command"
 	}
@@ -1457,7 +1461,8 @@ func (r *EventRule) validateMandatorySyncActions() error {
 
 func (r *EventRule) checkIPBlockedAndCertificateActions() error {
 	unavailableActions := []int{ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
-		ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypeFilesystem, ActionTypePasswordExpirationCheck}
+		ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypeFilesystem, ActionTypePasswordExpirationCheck,
+		ActionTypeUserExpirationCheck}
 	for _, action := range r.Actions {
 		if util.Contains(unavailableActions, action.Type) {
 			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
 	// affected user. Folder quota reset can be executed only for folders.
 	userSpecificActions := []int{ActionTypeUserQuotaReset, ActionTypeTransferQuotaReset,
-		ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypeFilesystem, ActionTypePasswordExpirationCheck}
+		ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypeFilesystem,
+		ActionTypePasswordExpirationCheck, ActionTypeUserExpirationCheck}
 	for _, action := range r.Actions {
 		if util.Contains(userSpecificActions, action.Type) && providerObjectType != actionObjectUser {
 			return fmt.Errorf("action %q, type %q is only supported for provider user events",

+ 6 - 0
openapi/openapi.yaml

@@ -4871,6 +4871,9 @@ components:
         - 7
         - 8
         - 9
+        - 10
+        - 11
+        - 12
       description: |
         Supported event action types:
           * `1` - HTTP
@@ -4882,6 +4885,9 @@ components:
           * `7` - Transfer quota reset
           * `8` - Data retention check
           * `9` - Filesystem
+          * `10` - Metadata check
+          * `11` - Password expiration check
+          * `12` - User expiration check
     FilesystemActionTypes:
       type: integer
       enum: