Files
Orchard/src/Orchard.Web/Modules/Orchard.Users/Services/InactiveUserSuspensionBackgroundTask.cs
Matteo Piovanelli b042873252 extend users and roles capabilities (#8523)
* Adds the capability to set a user to forcely change its own password at next LogOn

* Force user to not reuse last n passwords

* Moves IPasswordService implementation to Orchard Users
Creates Extensions to share management of Password operations across Services

* Some refactoring

* Password History Policy:
- New User Evente (ChangingPassword)
- Settings to enable the policy
- Security service interfaces to abstract history management
- User service implementations to concretely manage history

* PasswordHistoryPolicy:
- keep in count the password stored within the UserPart as a not reusable password

* WIP automated suspension of inactive users

* Disable users that have been inactive for longer than a specified number of
days, except when they are SiteOwner, or they have a specific flag set to
prevent their suspension.

* Provider to prevent suspension of users based on assigned roles

* cleanup. Refactor of migrations.

* Added action to ask for the challenge email to be resent.
Challenge email is sent again if a user tries to register anew with an email
address they had used to create an account earlier if the email address isn't
validated yet.

* During registration, if a user inserts the information of an existing account
and that account should still validate its email address, the user is presented
a link to request a new challenge email to be sent.

* Added a link to the action to request a new challenge email in the case when
the nonce fails to validate.

* Renamed part and corresponding record.
Added ability to "protect" specific users from having to change password when
it is expired / too old.

Co-authored-by: HermesSbicego-Laser <hermes.sbicego@laser-group.com>
2022-01-14 10:32:07 +01:00

149 lines
6.8 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Orchard.ContentManagement;
using Orchard.Logging;
using Orchard.Security;
using Orchard.Services;
using Orchard.Settings;
using Orchard.Tasks;
using Orchard.Tasks.Locking.Services;
using Orchard.Users.Events;
using Orchard.Users.Models;
namespace Orchard.Users.Services {
public class InactiveUserSuspensionBackgroundTask : Component, IBackgroundTask {
private readonly IDistributedLockService _distributedLockService;
private readonly ISiteService _siteService;
private readonly IClock _clock;
private readonly IContentManager _contentManager;
private readonly IUserEventHandler _userEventHandlers;
private readonly IAuthorizationService _authorizationService;
private readonly IEnumerable<IUserSuspensionConditionProvider> _userSuspensionConditionProviders;
public InactiveUserSuspensionBackgroundTask(
IDistributedLockService distributedLockService,
ISiteService siteService,
IClock clock,
IContentManager contentManager,
IUserEventHandler userEventHandlers,
IAuthorizationService authorizationService,
IEnumerable<IUserSuspensionConditionProvider> userSuspensionConditionProviders) {
_distributedLockService = distributedLockService;
_siteService = siteService;
_clock = clock;
_contentManager = contentManager;
_userEventHandlers = userEventHandlers;
_authorizationService = authorizationService;
_userSuspensionConditionProviders = userSuspensionConditionProviders;
}
public void Sweep() {
Logger.Debug("Beginning sweep to suspend inactive users.");
try {
// Only allow this task to run on one farm node at a time.
IDistributedLock @lock;
if (_distributedLockService.TryAcquireLock(GetType().FullName, TimeSpan.FromHours(1), out @lock)) {
using (@lock) {
// Check whether it's time to do another sweep.
if (!IsItTimeToSweep()) {
return; // too soon
}
Logger.Debug("Checking for inactive users.");
// Get all inactive users (last logon older than the configured timespan)
var thresholdDate = _clock.UtcNow - TimeSpan.FromDays(GetSettings().AllowedInactivityDays);
IContentQuery<UserPart> inactiveUsersQuery = _contentManager
.Query<UserPart>()
.Where<UserPartRecord>(upr =>
// user is enabled
upr.RegistrationStatus == UserStatus.Approved
&& upr.EmailStatus == UserStatus.Approved)
.Where<UserPartRecord>(upr =>
// The last login happened a long time ago
(upr.LastLoginUtc != null && upr.LastLoginUtc <= thresholdDate)
// The user never logged in AND was created a long time ago
|| (upr.LastLoginUtc == null && upr.CreatedUtc <= thresholdDate)
);
// If providers could alter the query we'd be able to immediately limit the number
// of ContentItems we'll fetch, and as a consequence the number of operations later.
// However, such conditions would make users immune from being suspended.
foreach (var provider in _userSuspensionConditionProviders) {
inactiveUsersQuery = provider.AlterQuery(inactiveUsersQuery);
}
var inactiveUsers = inactiveUsersQuery.List();
// By default, all inactive users should be suspended, except SiteOwner.
foreach (var userUnderTest in inactiveUsers.Where(up => !IsSiteOwner(up))) {
// Ask providers whether users should be processed/disabled
var saveTheUser = _userSuspensionConditionProviders
.Aggregate(false, (prev, scp) => prev || scp.UserIsProtected(userUnderTest));
// Suspend the users that have gotten this far.
if (!saveTheUser) {
DisableUser(userUnderTest);
}
}
// Done!
// Mark the time that we have done this check.
GetSettings().LastSweepUtc = _clock.UtcNow;
Logger.Debug("Done checking for inactive users.");
}
} else {
Logger.Debug("Distributed lock could not be acquired; going back to sleep.");
}
} catch (Exception ex) {
Logger.Error(ex, "Error during sweep to suspend inactive users.");
} finally {
Logger.Debug("Ending sweep to suspend inactive users.");
}
}
private bool IsSiteOwner(UserPart userPart) {
return _authorizationService
.TryCheckAccess(StandardPermissions.SiteOwner,
userPart, null);
}
private void DisableUser(UserPart userPart) {
userPart.RegistrationStatus = UserStatus.Pending;
Logger.Information(T("User {0} disabled by automatic moderation", userPart.UserName).Text);
_userEventHandlers.Moderate(userPart);
}
private bool IsItTimeToSweep() {
var settings = GetSettings();
if (settings.SuspendInactiveUsers) {
if (settings.MinimumSweepInterval <= 0) {
return true;
}
var lastSweep = settings.LastSweepUtc ?? DateTime.MinValue;
var now = _clock.UtcNow;
var interval = TimeSpan.FromHours(settings.MinimumSweepInterval);
return now - lastSweep >= interval;
}
return false;
}
#region Memorize settings
private ISite _siteSettings;
private ISite GetSiteSettings() {
if (_siteSettings == null) {
_siteSettings = _siteService.GetSiteSettings();
}
return _siteSettings;
}
private UserSuspensionSettingsPart _settingsPart;
private UserSuspensionSettingsPart GetSettings() {
if (_settingsPart == null) {
_settingsPart = GetSiteSettings().As<UserSuspensionSettingsPart>();
}
return _settingsPart;
}
#endregion
}
}