mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-10-26 12:03:16 +08:00
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>
This commit is contained in:
committed by
GitHub
parent
1e1668fdc2
commit
b042873252
@@ -40,7 +40,7 @@ namespace Orchard.Specs.Bindings {
|
||||
var memberShipService = environment.Resolve<IMembershipService>();
|
||||
var roleService = environment.Resolve<IRoleService>();
|
||||
var userRoleRepository = environment.Resolve<IRepository<UserRolesPartRecord>>();
|
||||
var user = memberShipService.CreateUser(new CreateUserParams(username, "qwerty123!", username + "@foo.com", "", "", true));
|
||||
var user = memberShipService.CreateUser(new CreateUserParams(username, "qwerty123!", username + "@foo.com", "", "", true, false));
|
||||
|
||||
foreach (var roleName in roles.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)) {
|
||||
var role = roleService.GetRoleByName(roleName);
|
||||
|
||||
@@ -116,14 +116,14 @@ namespace Orchard.Tests.Modules.Users.Services
|
||||
|
||||
[Test]
|
||||
public void CreateUserShouldAllocateModelAndCreateRecords() {
|
||||
var user = _membershipService.CreateUser(new CreateUserParams("a", "b", "c", null, null, true));
|
||||
var user = _membershipService.CreateUser(new CreateUserParams("a", "b", "c", null, null, true, false));
|
||||
Assert.That(user.UserName, Is.EqualTo("a"));
|
||||
Assert.That(user.Email, Is.EqualTo("c"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DefaultPasswordFormatShouldBeHashedAndHaveSalt() {
|
||||
var user = _membershipService.CreateUser(new CreateUserParams("a", "b", "c", null, null, true));
|
||||
var user = _membershipService.CreateUser(new CreateUserParams("a", "b", "c", null, null, true, false));
|
||||
|
||||
var userRepository = _container.Resolve<IRepository<UserPartRecord>>();
|
||||
var userRecord = userRepository.Get(user.Id);
|
||||
@@ -135,11 +135,11 @@ namespace Orchard.Tests.Modules.Users.Services
|
||||
|
||||
[Test]
|
||||
public void SaltAndPasswordShouldBeDifferentEvenWithSameSourcePassword() {
|
||||
var user1 = _membershipService.CreateUser(new CreateUserParams("a", "b", "c", null, null, true));
|
||||
var user1 = _membershipService.CreateUser(new CreateUserParams("a", "b", "c", null, null, true, false));
|
||||
_session.Flush();
|
||||
_session.Clear();
|
||||
|
||||
var user2 = _membershipService.CreateUser(new CreateUserParams("d", "b", "e", null, null, true));
|
||||
var user2 = _membershipService.CreateUser(new CreateUserParams("d", "b", "e", null, null, true, false));
|
||||
_session.Flush();
|
||||
_session.Clear();
|
||||
|
||||
@@ -156,7 +156,7 @@ namespace Orchard.Tests.Modules.Users.Services
|
||||
|
||||
[Test]
|
||||
public void ValidateUserShouldReturnNullIfUserOrPasswordIsIncorrect() {
|
||||
_membershipService.CreateUser(new CreateUserParams("test-user", "test-password", "c", null, null, true));
|
||||
_membershipService.CreateUser(new CreateUserParams("test-user", "test-password", "c", null, null, true, false));
|
||||
_session.Flush();
|
||||
_session.Clear();
|
||||
|
||||
@@ -172,14 +172,14 @@ namespace Orchard.Tests.Modules.Users.Services
|
||||
|
||||
[Test]
|
||||
public void UsersWhoHaveNeverLoggedInCanBeAuthenticated() {
|
||||
var user = (UserPart)_membershipService.CreateUser(new CreateUserParams("a", "b", "c", null, null, true));
|
||||
var user = (UserPart)_membershipService.CreateUser(new CreateUserParams("a", "b", "c", null, null, true, false));
|
||||
|
||||
Assert.That(_membershipValidationService.CanAuthenticateWithCookie(user), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UsersWhoHaveNeverLoggedOutCanBeAuthenticated() {
|
||||
var user = (UserPart)_membershipService.CreateUser(new CreateUserParams("a", "b", "c", null, null, true));
|
||||
var user = (UserPart)_membershipService.CreateUser(new CreateUserParams("a", "b", "c", null, null, true, false));
|
||||
|
||||
user.LastLoginUtc = _clock.UtcNow;
|
||||
_clock.Advance(TimeSpan.FromMinutes(1));
|
||||
@@ -189,7 +189,7 @@ namespace Orchard.Tests.Modules.Users.Services
|
||||
|
||||
[Test]
|
||||
public void UsersWhoHaveLoggedOutCantBeAuthenticated() {
|
||||
var user = (UserPart)_membershipService.CreateUser(new CreateUserParams("a", "b", "c", null, null, true));
|
||||
var user = (UserPart)_membershipService.CreateUser(new CreateUserParams("a", "b", "c", null, null, true, false));
|
||||
|
||||
user.LastLoginUtc = _clock.UtcNow;
|
||||
_clock.Advance(TimeSpan.FromMinutes(1));
|
||||
@@ -201,7 +201,7 @@ namespace Orchard.Tests.Modules.Users.Services
|
||||
|
||||
[Test]
|
||||
public void UsersWhoHaveLoggedInCanBeAuthenticated() {
|
||||
var user = (UserPart)_membershipService.CreateUser(new CreateUserParams("a", "b", "c", null, null, true));
|
||||
var user = (UserPart)_membershipService.CreateUser(new CreateUserParams("a", "b", "c", null, null, true, false));
|
||||
|
||||
user.LastLogoutUtc = _clock.UtcNow;
|
||||
_clock.Advance(TimeSpan.FromMinutes(1));
|
||||
@@ -213,7 +213,7 @@ namespace Orchard.Tests.Modules.Users.Services
|
||||
|
||||
[Test]
|
||||
public void PendingUsersCantBeAuthenticated() {
|
||||
var user = (UserPart)_membershipService.CreateUser(new CreateUserParams("a", "b", "c", null, null, true));
|
||||
var user = (UserPart)_membershipService.CreateUser(new CreateUserParams("a", "b", "c", null, null, true, false));
|
||||
|
||||
user.RegistrationStatus = UserStatus.Pending;
|
||||
|
||||
@@ -222,7 +222,7 @@ namespace Orchard.Tests.Modules.Users.Services
|
||||
|
||||
[Test]
|
||||
public void ApprovedUsersCanBeAuthenticated() {
|
||||
var user = (UserPart)_membershipService.CreateUser(new CreateUserParams("a", "b", "c", null, null, true));
|
||||
var user = (UserPart)_membershipService.CreateUser(new CreateUserParams("a", "b", "c", null, null, true, false));
|
||||
|
||||
user.RegistrationStatus = UserStatus.Approved;
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ namespace Orchard.Tests.Modules.Users.Services
|
||||
|
||||
[Test]
|
||||
public void NonceShouldBeDecryptable() {
|
||||
var user = _membershipService.CreateUser(new CreateUserParams("foo", "66554321", "foo@bar.com", "", "", true));
|
||||
var user = _membershipService.CreateUser(new CreateUserParams("foo", "66554321", "foo@bar.com", "", "", true, false));
|
||||
var nonce = _userService.CreateNonce(user, new TimeSpan(1, 0, 0));
|
||||
|
||||
Assert.That(nonce, Is.Not.Empty);
|
||||
@@ -145,7 +145,7 @@ namespace Orchard.Tests.Modules.Users.Services
|
||||
Thread.CurrentThread.CurrentCulture = turkishCulture;
|
||||
|
||||
// Create user lower case
|
||||
_membershipService.CreateUser(new CreateUserParams("admin", "66554321", "foo@bar.com", "", "", true));
|
||||
_membershipService.CreateUser(new CreateUserParams("admin", "66554321", "foo@bar.com", "", "", true, false));
|
||||
|
||||
// Verify unicity with upper case which with turkish coallition would yeld admin with an i without the dot and therefore generate a different user name
|
||||
Assert.That(_userService.VerifyUserUnicity("ADMIN", "differentfoo@bar.com"), Is.False);
|
||||
|
||||
@@ -49,6 +49,9 @@ namespace Orchard.AuditTrail.Providers.Users {
|
||||
_auditTrailManager.CreateRecord<UserAuditTrailEventProvider>(eventName, _wca.GetContext().CurrentUser, properties, eventData, eventFilterKey: "user", eventFilterData: user.UserName);
|
||||
}
|
||||
|
||||
public void ChangingPassword(IUser user, string password) {
|
||||
}
|
||||
|
||||
public void Creating(UserContext context) {
|
||||
}
|
||||
|
||||
@@ -72,5 +75,6 @@ namespace Orchard.AuditTrail.Providers.Users {
|
||||
|
||||
public void Moderate(IUser user) {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -100,9 +100,7 @@
|
||||
<Compile Include="Activities\EmailActivity.cs" />
|
||||
<Compile Include="Controllers\EmailAdminController.cs" />
|
||||
<Compile Include="Forms\MailForms.cs" />
|
||||
<Compile Include="Forms\EmailForm.cs">
|
||||
<SubType>Component</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Forms\EmailForm.cs" />
|
||||
<Compile Include="Migrations.cs" />
|
||||
<Compile Include="Drivers\SmtpSettingsPartDriver.cs" />
|
||||
<Compile Include="Handlers\SmtpSettingsPartHandler.cs" />
|
||||
|
||||
@@ -97,7 +97,7 @@ namespace Orchard.OpenId.Services {
|
||||
var localUser =
|
||||
_membershipService.GetUser(userName) ??
|
||||
_membershipService.CreateUser(new CreateUserParams(
|
||||
userName, Membership.GeneratePassword(16, 1), userName, string.Empty, string.Empty, true
|
||||
userName, Membership.GeneratePassword(16, 1), userName, string.Empty, string.Empty, true, false
|
||||
));
|
||||
|
||||
return _localAuthenticationUser = localUser;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.ContentManagement.Drivers;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Roles.Constants;
|
||||
using Orchard.Roles.Models;
|
||||
using Orchard.Roles.Services;
|
||||
using Orchard.Roles.ViewModels;
|
||||
|
||||
namespace Orchard.Roles.Drivers {
|
||||
public class RolesUserSuspensionSettingsPartDriver : ContentPartDriver<RolesUserSuspensionSettingsPart> {
|
||||
private readonly IRoleService _roleService;
|
||||
|
||||
public RolesUserSuspensionSettingsPartDriver(
|
||||
IRoleService roleService) {
|
||||
|
||||
_roleService = roleService;
|
||||
|
||||
T = NullLocalizer.Instance;
|
||||
}
|
||||
|
||||
public Localizer T { get; set; }
|
||||
|
||||
protected override DriverResult Editor(RolesUserSuspensionSettingsPart part, dynamic shapeHelper) {
|
||||
|
||||
return ContentShape("Parts_Roles_UserSuspensionSettings_Edit",
|
||||
() => {
|
||||
var vm = BuildVM(part);
|
||||
// check from the part what's configured.
|
||||
return shapeHelper.EditorTemplate(
|
||||
TemplateName: "Parts/Roles.UserSuspensionSettings",
|
||||
Model: vm,
|
||||
Prefix: Prefix
|
||||
);
|
||||
}).OnGroup(T("Users").Text);
|
||||
}
|
||||
|
||||
protected override DriverResult Editor(RolesUserSuspensionSettingsPart part, IUpdateModel updater, dynamic shapeHelper) {
|
||||
|
||||
var vm = BuildVM(part);
|
||||
if (updater.TryUpdateModel(vm, Prefix, null, null)) {
|
||||
part.Configuration = vm.Configuration;
|
||||
}
|
||||
return Editor(part, shapeHelper);
|
||||
}
|
||||
|
||||
private RolesUserSuspensionSettingsViewModel BuildVM(RolesUserSuspensionSettingsPart part) {
|
||||
var systemRoles = SystemRoles.GetSystemRoles();
|
||||
var allRoles = _roleService
|
||||
.GetRoles()
|
||||
// exclude system roles (Anonymous and Authenticated)
|
||||
.Where(r => !systemRoles
|
||||
.Any(sr => sr.Equals(r.Name, StringComparison.InvariantCultureIgnoreCase)))
|
||||
;
|
||||
var vm = new RolesUserSuspensionSettingsViewModel();
|
||||
// Add the "default" element for the configuration for authenticated users
|
||||
// who have no other roles.
|
||||
vm.Configuration.Add(
|
||||
new RoleSuspensionConfiguration {
|
||||
RoleId = 0,
|
||||
RoleName = T("No Role").Text,
|
||||
RoleLabel = T("Protect users with no configured role.").Text,
|
||||
IsSafeFromSuspension = GetConfigurationStatus(part, 0)
|
||||
});
|
||||
// Add configuration elements for all existing roles.
|
||||
vm.Configuration.AddRange(allRoles
|
||||
.Select(rr => new RoleSuspensionConfiguration {
|
||||
RoleId = rr.Id,
|
||||
RoleName = rr.Name, // use the "current" role name from the records
|
||||
RoleLabel = T("Protect users with the \"{0}\" user role.", rr.Name).Text,
|
||||
IsSafeFromSuspension = GetConfigurationStatus(part, rr.Id)
|
||||
}));
|
||||
return vm;
|
||||
}
|
||||
|
||||
private bool GetConfigurationStatus(RolesUserSuspensionSettingsPart part, int rId) {
|
||||
var config = part.Configuration?.FirstOrDefault(rsc => rsc.RoleId == rId);
|
||||
return config == null ? false : config.IsSafeFromSuspension;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Orchard.ContentManagement.Handlers;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Roles.Models;
|
||||
|
||||
namespace Orchard.Roles.Handlers {
|
||||
public class RolesUserSuspensionSettingsPartHandler : ContentHandler {
|
||||
|
||||
public RolesUserSuspensionSettingsPartHandler() {
|
||||
|
||||
T = NullLocalizer.Instance;
|
||||
Filters.Add(new ActivatingFilter<RolesUserSuspensionSettingsPart>("Site"));
|
||||
}
|
||||
|
||||
public Localizer T { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Roles.ViewModels;
|
||||
|
||||
namespace Orchard.Roles.Models {
|
||||
public class RolesUserSuspensionSettingsPart : ContentPart {
|
||||
public List<RoleSuspensionConfiguration> Configuration {
|
||||
get {
|
||||
return JsonConvert
|
||||
.DeserializeObject<List<RoleSuspensionConfiguration>>(SerializedConfiguration);
|
||||
}
|
||||
set { SerializedConfiguration = JsonConvert.SerializeObject(value); }
|
||||
}
|
||||
|
||||
public string SerializedConfiguration {
|
||||
get { return this.Retrieve(x => x.SerializedConfiguration, defaultValue: string.Empty); }
|
||||
set { this.Store(x => x.SerializedConfiguration, value ?? string.Empty); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,9 @@
|
||||
<Reference Include="Microsoft.Web.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.ComponentModel.DataAnnotations" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
@@ -100,6 +103,7 @@
|
||||
<Compile Include="Commands\RoleCommands.cs" />
|
||||
<Compile Include="Constants\SystemRoles.cs" />
|
||||
<Compile Include="Controllers\AdminController.cs" />
|
||||
<Compile Include="Drivers\RolesUserSuspensionSettingsPartDriver.cs" />
|
||||
<Compile Include="Drivers\UserTaskDriver.cs" />
|
||||
<Compile Include="Events\IRoleEventHandler.cs" />
|
||||
<Compile Include="Events\PermissionAddedContext.cs" />
|
||||
@@ -115,6 +119,8 @@
|
||||
<Compile Include="Extensions\UserRolesExtensions.cs" />
|
||||
<Compile Include="Forms\SelectRolesForms.cs" />
|
||||
<Compile Include="Forms\UserTaskForms.cs" />
|
||||
<Compile Include="Handlers\RolesUserSuspensionSettingsPartHandler.cs" />
|
||||
<Compile Include="Models\RolesUserSuspensionSettingsPart.cs" />
|
||||
<Compile Include="Recipes\Builders\RolesStep.cs" />
|
||||
<Compile Include="Recipes\Executors\RolesStep.cs" />
|
||||
<Compile Include="Migrations.cs" />
|
||||
@@ -134,9 +140,11 @@
|
||||
<Compile Include="Services\IRoleService.cs" />
|
||||
<Compile Include="Services\RolesBasedAuthorizationService.cs" />
|
||||
<Compile Include="Services\RoleService.cs" />
|
||||
<Compile Include="Services\RolesUserSuspensionConditionProvider.cs" />
|
||||
<Compile Include="ViewModels\RoleCreateViewModel.cs" />
|
||||
<Compile Include="ViewModels\RoleEditViewModel.cs" />
|
||||
<Compile Include="ViewModels\RolesIndexViewModel.cs" />
|
||||
<Compile Include="ViewModels\RolesUserSuspensionSettingsViewModel.cs" />
|
||||
<Compile Include="ViewModels\UserRolesViewModel.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -167,6 +175,10 @@
|
||||
<Project>{642a49d7-8752-4177-80d6-bfbbcfad3de0}</Project>
|
||||
<Name>Orchard.Forms</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Orchard.Users\Orchard.Users.csproj">
|
||||
<Project>{79aed36e-abd0-4747-93d3-8722b042454b}</Project>
|
||||
<Name>Orchard.Users</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Orchard.Workflows\Orchard.Workflows.csproj">
|
||||
<Project>{7059493c-8251-4764-9c1e-2368b8b485bc}</Project>
|
||||
<Name>Orchard.Workflows</Name>
|
||||
@@ -203,6 +215,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
<Content Include="Views\EditorTemplates\Parts\Roles.UserSuspensionSettings.cshtml" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<Placement>
|
||||
<Place Parts_Roles_UserRoles_Edit="Content:10"/>
|
||||
<Place Parts_Roles_UserSuspensionSettings_Edit="Content:6.1"/>
|
||||
</Placement>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Linq;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Data;
|
||||
using Orchard.Roles.Models;
|
||||
using Orchard.Settings;
|
||||
using Orchard.Users.Models;
|
||||
using Orchard.Users.Services;
|
||||
|
||||
namespace Orchard.Roles.Services {
|
||||
public class RolesUserSuspensionConditionProvider : IUserSuspensionConditionProvider {
|
||||
private readonly ISiteService _siteService;
|
||||
private readonly IRepository<UserRolesPartRecord> _userRolesRepository;
|
||||
|
||||
public RolesUserSuspensionConditionProvider(
|
||||
ISiteService siteService,
|
||||
IRepository<UserRolesPartRecord> userRolesRepository) {
|
||||
|
||||
_siteService = siteService;
|
||||
_userRolesRepository = userRolesRepository;
|
||||
}
|
||||
|
||||
public IContentQuery<UserPart> AlterQuery(IContentQuery<UserPart> query) {
|
||||
return query;
|
||||
}
|
||||
|
||||
public bool UserIsProtected(UserPart userPart) {
|
||||
// Get the user roles: we fetch them directly from the repository rather
|
||||
// than through a part, because we are going to need the roles Ids and
|
||||
// not just their names.
|
||||
var roleIds = _userRolesRepository
|
||||
.Fetch(urpr => urpr.UserId == userPart.Id)
|
||||
.Select(urpr => urpr.Role.Id)
|
||||
.ToList();
|
||||
// get settings
|
||||
var safeRoleIds = Settings.Configuration
|
||||
.Where(rsc => rsc.IsSafeFromSuspension)
|
||||
.Select(rsc => rsc.RoleId);
|
||||
// Case where we are "saving" users with no specific assigned role (i.e.
|
||||
// these users are just Authenticated)
|
||||
if (safeRoleIds.Contains(0) && !roleIds.Any()) {
|
||||
return true;
|
||||
}
|
||||
// If the user has assigned roles we need to check whether any of those
|
||||
// makes them "safe".
|
||||
return roleIds.Any(i => safeRoleIds.Contains(i));
|
||||
}
|
||||
|
||||
private RolesUserSuspensionSettingsPart _settingsPart;
|
||||
private RolesUserSuspensionSettingsPart Settings {
|
||||
get {
|
||||
if (_settingsPart == null) {
|
||||
_settingsPart = _siteService.GetSiteSettings().As<RolesUserSuspensionSettingsPart>();
|
||||
}
|
||||
return _settingsPart;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Orchard.Roles.ViewModels {
|
||||
public class RolesUserSuspensionSettingsViewModel {
|
||||
|
||||
public RolesUserSuspensionSettingsViewModel() {
|
||||
Configuration = new List<RoleSuspensionConfiguration>();
|
||||
}
|
||||
|
||||
public List<RoleSuspensionConfiguration> Configuration { get; set; }
|
||||
}
|
||||
|
||||
public class RoleSuspensionConfiguration {
|
||||
public int RoleId { get; set; }
|
||||
[JsonIgnore]
|
||||
public string RoleName { get; set; }
|
||||
[JsonIgnore]
|
||||
public string RoleLabel { get; set; }
|
||||
public bool IsSafeFromSuspension { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
@model Orchard.Roles.ViewModels.RolesUserSuspensionSettingsViewModel
|
||||
|
||||
<fieldset>
|
||||
<legend>
|
||||
@T("Automated User Moderation: rules on roles")
|
||||
</legend>
|
||||
<div>
|
||||
<p>@T("Select the user roles that will be safe from automated user suspension (this only applies if automated suspension is enabled).")</p>
|
||||
<div style="margin-left: 30px;">
|
||||
@if (Model.Configuration.Any()) {
|
||||
var index = 0;
|
||||
foreach (var entry in Model.Configuration) {
|
||||
@Html.Hidden("Configuration["+index+"].RoleId", entry.RoleId)
|
||||
<div>
|
||||
@Html.CheckBox("Configuration["+index+"].IsSafeFromSuspension", entry.IsSafeFromSuspension)
|
||||
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.Configuration[index].IsSafeFromSuspension)">@entry.RoleLabel</label>
|
||||
</div>
|
||||
|
||||
index++;
|
||||
}
|
||||
} else {
|
||||
<p>@T("There are no roles.")</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -5,4 +5,5 @@
|
||||
<package id="Microsoft.AspNet.WebPages" version="3.2.7" targetFramework="net48" />
|
||||
<package id="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" version="2.0.1" targetFramework="net48" />
|
||||
<package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net48" />
|
||||
<package id="Newtonsoft.Json" version="12.0.3" targetFramework="net48" />
|
||||
</packages>
|
||||
@@ -194,7 +194,7 @@ namespace Orchard.Setup.Services {
|
||||
var membershipService = environment.Resolve<IMembershipService>();
|
||||
var user = membershipService.CreateUser(
|
||||
new CreateUserParams(context.AdminUsername, context.AdminPassword,
|
||||
String.Empty, String.Empty, String.Empty, true));
|
||||
String.Empty, String.Empty, String.Empty, true, false));
|
||||
|
||||
// Set site owner as current user for request (it will be set as the owner of all content items).
|
||||
var authenticationService = environment.Resolve<IAuthenticationService>();
|
||||
|
||||
@@ -76,7 +76,8 @@ namespace Orchard.Users.Activities {
|
||||
email,
|
||||
isApproved: approved,
|
||||
passwordQuestion: null,
|
||||
passwordAnswer: null));
|
||||
passwordAnswer: null,
|
||||
forcePasswordChange: false));
|
||||
|
||||
workflowContext.Content = user;
|
||||
|
||||
|
||||
@@ -43,14 +43,14 @@ namespace Orchard.Users.Commands {
|
||||
}
|
||||
|
||||
IDictionary<string, LocalizedString> validationErrors;
|
||||
if (!_userService.PasswordMeetsPolicies(Password, out validationErrors)) {
|
||||
if (!_userService.PasswordMeetsPolicies(Password, null, out validationErrors)) {
|
||||
foreach (var error in validationErrors) {
|
||||
Context.Output.WriteLine(error.Value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var user = _membershipService.CreateUser(new CreateUserParams(UserName, Password, Email, null, null, true));
|
||||
var user = _membershipService.CreateUser(new CreateUserParams(UserName, Password, Email, null, null, true, false));
|
||||
if (user == null) {
|
||||
Context.Output.WriteLine(T("Could not create user {0}. The authentication provider returned an error", UserName));
|
||||
return;
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
public const string PasswordDoesNotContainUppercase = "PasswordDoesNotContainUppercase";
|
||||
public const string PasswordDoesNotContainLowercase = "PasswordDoesNotContainLowercase";
|
||||
public const string PasswordDoesNotContainSpecialCharacters = "PasswordDoesNotContainSpecialCharacters";
|
||||
public const string PasswordDoesNotMeetHistoryPolicy = "PasswordDoesNotMeetHistoryPolicy";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Web.Mvc;
|
||||
using System.Web.Security;
|
||||
@@ -91,7 +92,6 @@ namespace Orchard.Users.Controllers {
|
||||
var user = ValidateLogOn(userNameOrEmail, password);
|
||||
if (!ModelState.IsValid) {
|
||||
var shape = _orchardServices.New.LogOn().Title(T("Log On").Text);
|
||||
|
||||
return new ShapeResult(this, shape);
|
||||
}
|
||||
|
||||
@@ -102,6 +102,9 @@ namespace Orchard.Users.Controllers {
|
||||
_membershipService.PasswordIsExpired(user, membershipSettings.PasswordExpirationTimeInDays)) {
|
||||
return RedirectToAction("ChangeExpiredPassword", new { username = user.UserName });
|
||||
}
|
||||
if (user != null && user.As<UserPart>().ForcePasswordChange) {
|
||||
return RedirectToAction("ChangeExpiredPassword", new { username = user.UserName });
|
||||
}
|
||||
|
||||
_authenticationService.SignIn(user, rememberMe);
|
||||
_userEventHandler.LoggedIn(user);
|
||||
@@ -135,7 +138,6 @@ namespace Orchard.Users.Controllers {
|
||||
ViewData["NumberRequirement"] = membershipSettings.GetPasswordNumberRequirement();
|
||||
|
||||
var shape = _orchardServices.New.Register();
|
||||
|
||||
return new ShapeResult(this, shape);
|
||||
}
|
||||
|
||||
@@ -158,7 +160,7 @@ namespace Orchard.Users.Controllers {
|
||||
if (ValidateRegistration(userName, email, password, confirmPassword)) {
|
||||
// Attempt to register the user
|
||||
// No need to report this to IUserEventHandler because _membershipService does that for us
|
||||
var user = _membershipService.CreateUser(new CreateUserParams(userName, password, email, null, null, false));
|
||||
var user = _membershipService.CreateUser(new CreateUserParams(userName, password, email, null, null, false, false));
|
||||
|
||||
if (user != null) {
|
||||
if (user.As<UserPart>().EmailStatus == UserStatus.Pending) {
|
||||
@@ -230,6 +232,46 @@ namespace Orchard.Users.Controllers {
|
||||
return RedirectToAction("LogOn");
|
||||
}
|
||||
|
||||
[AlwaysAccessible]
|
||||
public ActionResult RequestChallengeEmail(string email = null) {
|
||||
// ensure users can request lost password
|
||||
var membershipSettings = _membershipService.GetSettings();
|
||||
if (!membershipSettings.UsersMustValidateEmail) {
|
||||
return HttpNotFound();
|
||||
}
|
||||
|
||||
return View(model: email);
|
||||
}
|
||||
|
||||
[HttpPost, ActionName("RequestChallengeEmail")]
|
||||
[AlwaysAccessible]
|
||||
public ActionResult RequestChallengeEmailPOST(string username) {
|
||||
// ensure users can request lost password
|
||||
var membershipSettings = _membershipService.GetSettings();
|
||||
if (!membershipSettings.UsersMustValidateEmail) {
|
||||
return HttpNotFound();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username)) {
|
||||
ModelState.AddModelError("username", T("You must specify a username or e-mail."));
|
||||
return View();
|
||||
}
|
||||
// Get the user
|
||||
var user = _userService.GetUserByNameOrEmail(username);
|
||||
if (user != null && user.EmailStatus == UserStatus.Pending) {
|
||||
var siteUrl = _orchardServices.WorkContext.CurrentSite.BaseUrl;
|
||||
if (string.IsNullOrWhiteSpace(siteUrl)) {
|
||||
siteUrl = HttpContext.Request.ToRootUrlString();
|
||||
}
|
||||
|
||||
_userService.SendChallengeEmail(user.As<UserPart>(), nonce => Url.MakeAbsolute(Url.Action("ChallengeEmail", "Account", new { Area = "Orchard.Users", nonce = nonce }), siteUrl));
|
||||
|
||||
_userEventHandler.SentChallengeEmail(user);
|
||||
}
|
||||
|
||||
return RedirectToAction("ChallengeEmailSent");
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[AlwaysAccessible]
|
||||
public ActionResult ChangePassword() {
|
||||
@@ -265,7 +307,7 @@ namespace Orchard.Users.Controllers {
|
||||
.ShouldInvalidateAuthOnPasswordChanged;
|
||||
ViewData["InvalidateOnPasswordChange"] = shouldSignout;
|
||||
|
||||
if (!ValidateChangePassword(currentPassword, newPassword, confirmPassword)) {
|
||||
if (!ValidateChangePassword(currentPassword, newPassword, confirmPassword, _orchardServices.WorkContext.CurrentUser)) {
|
||||
return View();
|
||||
}
|
||||
|
||||
@@ -288,9 +330,10 @@ namespace Orchard.Users.Controllers {
|
||||
[AlwaysAccessible]
|
||||
public ActionResult ChangeExpiredPassword(string username) {
|
||||
var membershipSettings = _membershipService.GetSettings();
|
||||
var lastPasswordChangeUtc = _membershipService.GetUser(username).As<UserPart>().LastPasswordChangeUtc;
|
||||
|
||||
if (lastPasswordChangeUtc.Value.AddDays(membershipSettings.PasswordExpirationTimeInDays) > _clock.UtcNow) {
|
||||
var userPart = _membershipService.GetUser(username).As<UserPart>();
|
||||
var lastPasswordChangeUtc = userPart.LastPasswordChangeUtc;
|
||||
if (lastPasswordChangeUtc.Value.AddDays(membershipSettings.PasswordExpirationTimeInDays) > _clock.UtcNow &&
|
||||
!userPart.ForcePasswordChange) {
|
||||
return RedirectToAction("LogOn");
|
||||
}
|
||||
|
||||
@@ -316,11 +359,12 @@ namespace Orchard.Users.Controllers {
|
||||
SpecialCharacterRequirement: membershipSettings.GetPasswordSpecialRequirement(),
|
||||
NumberRequirement: membershipSettings.GetPasswordNumberRequirement());
|
||||
|
||||
if (!ValidateChangePassword(currentPassword, newPassword, confirmPassword)) {
|
||||
if (!ValidateChangePassword(currentPassword, newPassword, confirmPassword, _membershipService.GetUser(username))) {
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
if (PasswordChangeIsSuccess(currentPassword, newPassword, username)) {
|
||||
|
||||
return RedirectToAction("ChangePasswordSuccess");
|
||||
} else {
|
||||
return View(viewModel);
|
||||
@@ -332,14 +376,15 @@ namespace Orchard.Users.Controllers {
|
||||
var validated = _membershipService.ValidateUser(username, currentPassword, out List<LocalizedString> validationErrors);
|
||||
|
||||
if (validated != null) {
|
||||
_userEventHandler.ChangingPassword(validated, newPassword);
|
||||
_membershipService.SetPassword(validated, newPassword);
|
||||
_userEventHandler.ChangedPassword(validated, newPassword);
|
||||
|
||||
// if security settings tell to invalidate on password change fire the LoggedOut event
|
||||
if (_orchardServices.WorkContext.CurrentSite.As<SecuritySettingsPart>().ShouldInvalidateAuthOnPasswordChanged) {
|
||||
if (_orchardServices.WorkContext
|
||||
.CurrentSite.As<SecuritySettingsPart>()
|
||||
.ShouldInvalidateAuthOnPasswordChanged) {
|
||||
_userEventHandler.LoggedOut(validated);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -348,7 +393,8 @@ namespace Orchard.Users.Controllers {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// unknown error
|
||||
ModelState.AddModelError("_FORM", T("The current password is incorrect or the new password is invalid."));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -383,10 +429,12 @@ namespace Orchard.Users.Controllers {
|
||||
ViewData["SpecialCharacterRequirement"] = membershipSettings.GetPasswordSpecialRequirement();
|
||||
ViewData["NumberRequirement"] = membershipSettings.GetPasswordNumberRequirement();
|
||||
|
||||
if (!ValidatePassword(newPassword, confirmPassword)) {
|
||||
if (!ValidatePassword(newPassword, confirmPassword, user)) {
|
||||
return View();
|
||||
}
|
||||
|
||||
_userEventHandler.ChangingPassword(user, newPassword);
|
||||
|
||||
_membershipService.SetPassword(user, newPassword);
|
||||
|
||||
_userEventHandler.ChangedPassword(user, newPassword);
|
||||
@@ -399,7 +447,6 @@ namespace Orchard.Users.Controllers {
|
||||
ViewData["InvalidateOnPasswordChange"] = _orchardServices.WorkContext
|
||||
.CurrentSite.As<SecuritySettingsPart>()
|
||||
.ShouldInvalidateAuthOnPasswordChanged;
|
||||
|
||||
return View();
|
||||
}
|
||||
|
||||
@@ -437,7 +484,7 @@ namespace Orchard.Users.Controllers {
|
||||
}
|
||||
|
||||
#region Validation Methods
|
||||
private bool ValidateChangePassword(string currentPassword, string newPassword, string confirmPassword) {
|
||||
private bool ValidateChangePassword(string currentPassword, string newPassword, string confirmPassword, IUser user) {
|
||||
if (string.IsNullOrEmpty(currentPassword)) {
|
||||
ModelState.AddModelError("currentPassword", T("You must specify a current password."));
|
||||
}
|
||||
@@ -450,7 +497,7 @@ namespace Orchard.Users.Controllers {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ValidatePassword(newPassword, confirmPassword);
|
||||
return ValidatePassword(newPassword, confirmPassword, user);
|
||||
}
|
||||
|
||||
private IUser ValidateLogOn(string userNameOrEmail, string password) {
|
||||
@@ -496,11 +543,34 @@ namespace Orchard.Users.Controllers {
|
||||
if (!context.ValidationSuccessful) {
|
||||
foreach (var error in context.ValidationErrors) {
|
||||
ModelState.AddModelError(error.Key, error.Value);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_userService.VerifyUserUnicity(userName, email)) {
|
||||
// Not a new registration, but perhaps we already have that user and they
|
||||
// haven't validated their email address. This doesn't care whether there
|
||||
// were other issues with the registration attempt that caused its validation
|
||||
// to fail: if the user exists and still has to confirm their email, we show
|
||||
// a link to the action from which the challenge email is sent again.
|
||||
var membershipSettings = _membershipService.GetSettings();
|
||||
if (membershipSettings.UsersMustValidateEmail) {
|
||||
var user = _userService.GetUserByNameOrEmail(email);
|
||||
if (user == null) {
|
||||
user = _userService.GetUserByNameOrEmail(userName);
|
||||
}
|
||||
if (user != null && user.EmailStatus == UserStatus.Pending) {
|
||||
// We can't have links in the "text" of a ModelState Error. We are using a notifier
|
||||
// to provide the user with an option to ask for a new challenge email.
|
||||
_orchardServices.Notifier.Warning(
|
||||
T("User with that username and/or email already exists. Follow <a href=\"{0}\">this link</a> if you want to receive a new email to validate your address.",
|
||||
Url.Action(actionName: "RequestChallengeEmail", routeValues: new { email = email })));
|
||||
// In creating the link above we use the email that was written in the form
|
||||
// rather than the actual user's email address to prevent exploiting this
|
||||
// for information discovery.
|
||||
}
|
||||
}
|
||||
// We should add the error to the ModelState anyway.
|
||||
context.ValidationErrors.Add("userExists", T("User with that username and/or email already exists."));
|
||||
}
|
||||
|
||||
@@ -519,9 +589,10 @@ namespace Orchard.Users.Controllers {
|
||||
return ModelState.IsValid;
|
||||
}
|
||||
|
||||
private bool ValidatePassword(string password) {
|
||||
private bool ValidatePassword(string password, IUser user) {
|
||||
var context = new AccountValidationContext {
|
||||
Password = password
|
||||
Password = password,
|
||||
User = user
|
||||
};
|
||||
var result = _accountValidationService.ValidatePassword(context);
|
||||
if (!result) {
|
||||
@@ -532,12 +603,12 @@ namespace Orchard.Users.Controllers {
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool ValidatePassword(string password, string confirmPassword) {
|
||||
private bool ValidatePassword(string password, string confirmPassword, IUser user) {
|
||||
if (!string.Equals(password, confirmPassword, StringComparison.Ordinal)) {
|
||||
ModelState.AddModelError("_FORM", T("The new password and confirmation password do not match."));
|
||||
return false;
|
||||
}
|
||||
return ValidatePassword(password);
|
||||
return ValidatePassword(password, user);
|
||||
}
|
||||
|
||||
private static string ErrorCodeToString(MembershipCreateStatus createStatus) {
|
||||
|
||||
@@ -192,7 +192,7 @@ namespace Orchard.Users.Controllers {
|
||||
|
||||
IDictionary<string, LocalizedString> validationErrors;
|
||||
|
||||
if (!_userService.PasswordMeetsPolicies(createModel.Password, out validationErrors)) {
|
||||
if (!_userService.PasswordMeetsPolicies(createModel.Password, null, out validationErrors)) {
|
||||
ModelState.AddModelErrors(validationErrors);
|
||||
}
|
||||
|
||||
@@ -202,7 +202,8 @@ namespace Orchard.Users.Controllers {
|
||||
createModel.UserName,
|
||||
createModel.Password,
|
||||
createModel.Email,
|
||||
null, null, true));
|
||||
null, null, true,
|
||||
createModel.ForcePasswordChange));
|
||||
}
|
||||
|
||||
var model = Services.ContentManager.UpdateEditor(user, this);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Orchard.ContentManagement;
|
||||
using System.Collections.Generic;
|
||||
using System.Web.Mvc;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.ContentManagement.Drivers;
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.Localization;
|
||||
@@ -6,10 +8,8 @@ using Orchard.Security;
|
||||
using Orchard.Users.Models;
|
||||
using Orchard.Users.Services;
|
||||
using Orchard.Users.ViewModels;
|
||||
using System.Collections.Generic;
|
||||
using System.Web.Mvc;
|
||||
|
||||
namespace Orchard.Users.Drivers{
|
||||
namespace Orchard.Users.Drivers {
|
||||
|
||||
[OrchardFeature("Orchard.Users.PasswordEditor")]
|
||||
public class UserPartPasswordDriver : ContentPartDriver<UserPart> {
|
||||
@@ -38,22 +38,21 @@ namespace Orchard.Users.Drivers{
|
||||
protected override DriverResult Editor(UserPart part, IUpdateModel updater, dynamic shapeHelper) {
|
||||
var editModel = new UserEditPasswordViewModel { User = part };
|
||||
if (updater != null) {
|
||||
if (updater.TryUpdateModel(editModel,Prefix,null,null)) {
|
||||
if (updater.TryUpdateModel(editModel, Prefix, null, null)) {
|
||||
if (!(string.IsNullOrEmpty(editModel.Password) && string.IsNullOrEmpty(editModel.ConfirmPassword))) {
|
||||
if (string.IsNullOrEmpty(editModel.Password) || string.IsNullOrEmpty(editModel.ConfirmPassword)) {
|
||||
updater.AddModelError("MissingPassword", T("Password or Confirm Password field is empty."));
|
||||
}
|
||||
else {
|
||||
if (editModel.Password != editModel.ConfirmPassword){
|
||||
} else {
|
||||
if (editModel.Password != editModel.ConfirmPassword) {
|
||||
updater.AddModelError("ConfirmPassword", T("Password confirmation must match."));
|
||||
}
|
||||
var actUser = _membershipService.GetUser(part.UserName);
|
||||
_membershipService.SetPassword(actUser, editModel.Password);
|
||||
}
|
||||
IDictionary<string, LocalizedString> validationErrors;
|
||||
if (!_userService.PasswordMeetsPolicies(editModel.Password, out validationErrors)) {
|
||||
updater.AddModelErrors(validationErrors);
|
||||
}
|
||||
IDictionary<string, LocalizedString> validationErrors;
|
||||
if (!_userService.PasswordMeetsPolicies(editModel.Password, part, out validationErrors)) {
|
||||
updater.AddModelErrors(validationErrors);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.ContentManagement.Drivers;
|
||||
using Orchard.Users.Models;
|
||||
|
||||
namespace Orchard.Users.Drivers {
|
||||
public class UserSecurityConfigurationPartDriver : ContentPartDriver<UserSecurityConfigurationPart> {
|
||||
|
||||
public UserSecurityConfigurationPartDriver() { }
|
||||
|
||||
protected override DriverResult Editor(
|
||||
UserSecurityConfigurationPart part, dynamic shapeHelper) {
|
||||
|
||||
return ContentShape("Parts_User_UserSecurityConfiguration_Edit",
|
||||
() => shapeHelper.EditorTemplate(
|
||||
TemplateName: "Parts/User.UserSecurityConfiguration",
|
||||
Model: part,
|
||||
Prefix: Prefix
|
||||
));
|
||||
}
|
||||
protected override DriverResult Editor(
|
||||
UserSecurityConfigurationPart part, IUpdateModel updater, dynamic shapeHelper) {
|
||||
updater.TryUpdateModel(part, Prefix, null, null);
|
||||
return Editor(part, shapeHelper);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,11 @@ namespace Orchard.Users.Events {
|
||||
/// </summary>
|
||||
void AccessDenied(IUser user);
|
||||
|
||||
/// <summary>
|
||||
/// Called before a user has changed password
|
||||
/// </summary>
|
||||
void ChangingPassword(IUser user, string password);
|
||||
|
||||
/// <summary>
|
||||
/// Called after a user has changed password
|
||||
/// </summary>
|
||||
|
||||
@@ -6,13 +6,17 @@ using Orchard.ContentManagement;
|
||||
using Orchard.Security;
|
||||
using Orchard.Services;
|
||||
using Orchard.Users.Models;
|
||||
using Orchard.Users.Services;
|
||||
|
||||
namespace Orchard.Users.Events {
|
||||
public class LoginUserEventHandler : IUserEventHandler {
|
||||
private readonly IClock _clock;
|
||||
private readonly IPasswordHistoryService _passwordHistoryService;
|
||||
private PasswordHistoryEntry _freezedPasswordEntry;
|
||||
|
||||
public LoginUserEventHandler(IClock clock) {
|
||||
public LoginUserEventHandler(IClock clock, IPasswordHistoryService passwordHistoryService) {
|
||||
_clock = clock;
|
||||
_passwordHistoryService = passwordHistoryService;
|
||||
}
|
||||
|
||||
public void Creating(UserContext context) { }
|
||||
@@ -29,7 +33,26 @@ namespace Orchard.Users.Events {
|
||||
|
||||
public void AccessDenied(IUser user) { }
|
||||
|
||||
public void ChangedPassword(IUser user, string password) { }
|
||||
public void ChangingPassword(IUser user, string password) {
|
||||
var userPart = user.As<UserPart>();
|
||||
_freezedPasswordEntry = new PasswordHistoryEntry {
|
||||
User = user,
|
||||
Password = userPart.Password,
|
||||
PasswordSalt = userPart.PasswordSalt,
|
||||
HashAlgorithm = userPart.HashAlgorithm,
|
||||
PasswordFormat = userPart.PasswordFormat,
|
||||
LastPasswordChangeUtc = userPart.LastPasswordChangeUtc
|
||||
};
|
||||
}
|
||||
|
||||
public void ChangedPassword(IUser user, string password) {
|
||||
// If password has changed set to false the Force Password Change flag
|
||||
if (user.As<UserPart>().ForcePasswordChange)
|
||||
user.As<UserPart>().ForcePasswordChange = false;
|
||||
|
||||
// Store in the password history the previous password
|
||||
_passwordHistoryService.CreateEntry(_freezedPasswordEntry);
|
||||
}
|
||||
|
||||
public void SentChallengeEmail(IUser user) { }
|
||||
|
||||
|
||||
@@ -15,5 +15,11 @@
|
||||
public static bool GetPasswordSpecialRequirement(this IMembershipSettings membershipSettings) {
|
||||
return membershipSettings.EnableCustomPasswordPolicy ? membershipSettings.EnablePasswordSpecialRequirement : false;
|
||||
}
|
||||
public static bool GetPasswordHistoryRequirement(this IMembershipSettings membershipSettings) {
|
||||
return membershipSettings.EnablePasswordHistoryPolicy ? membershipSettings.EnablePasswordHistoryPolicy : false;
|
||||
}
|
||||
public static int GetPasswordReuseLimit(this IMembershipSettings membershipSettings) {
|
||||
return membershipSettings.EnablePasswordHistoryPolicy ? membershipSettings.PasswordReuseLimit : 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Orchard.ContentManagement.Handlers;
|
||||
using Orchard.Data;
|
||||
using Orchard.Users.Models;
|
||||
|
||||
namespace Orchard.Users.Handlers {
|
||||
public class UserSecurityConfigurationPartHandler : ContentHandler {
|
||||
public UserSecurityConfigurationPartHandler(
|
||||
IRepository<UserSecurityConfigurationPartRecord> repository) {
|
||||
|
||||
Filters.Add(new ActivatingFilter<UserSecurityConfigurationPart>("User"));
|
||||
Filters.Add(StorageFilter.For(repository));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.ContentManagement.Handlers;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Users.Models;
|
||||
|
||||
namespace Orchard.Users.Handlers {
|
||||
public class UserSuspensionSettingsPartHandler : ContentHandler {
|
||||
|
||||
public UserSuspensionSettingsPartHandler() {
|
||||
T = NullLocalizer.Instance;
|
||||
Filters.Add(new ActivatingFilter<UserSuspensionSettingsPart>("Site"));
|
||||
Filters.Add(
|
||||
new TemplateFilterForPart<UserSuspensionSettingsPart>("SuspensionSettings", "Parts/Users.SuspensionSettings", "users")
|
||||
.Position("6"));
|
||||
}
|
||||
|
||||
public Localizer T { get; set; }
|
||||
|
||||
protected override void GetItemMetadata(GetContentItemMetadataContext context) {
|
||||
if (context.ContentItem.ContentType != "Site")
|
||||
return;
|
||||
base.GetItemMetadata(context);
|
||||
context.Metadata.EditorGroupInfo.Add(new GroupInfo(T("Users")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,5 +89,9 @@ namespace Orchard.Users.Handlers {
|
||||
user,
|
||||
() => new Dictionary<string, object> { { "User", user } });
|
||||
}
|
||||
|
||||
//TODO evaluate if we need a workflow event for this
|
||||
public void ChangingPassword(Security.IUser user, string password) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,37 @@ namespace Orchard.Users {
|
||||
.Column<DateTime>("CreatedUtc")
|
||||
.Column<DateTime>("LastLoginUtc")
|
||||
.Column<DateTime>("LastLogoutUtc")
|
||||
.Column<DateTime>("LastPasswordChangeUtc", c => c.WithDefault(new DateTime(1990, 1, 1))))
|
||||
.Column<DateTime>("LastPasswordChangeUtc", c => c.WithDefault(new DateTime(1990, 1, 1)))
|
||||
.Column<bool>("ForcePasswordChange"))
|
||||
.AlterTable("UserPartRecord", table => table
|
||||
.CreateIndex("IDX_UserPartRecord_NormalizedUserName", "NormalizedUserName"));
|
||||
.CreateIndex("IDX_UserPartRecord_NormalizedUserName", "NormalizedUserName"))
|
||||
// users are most commonly searched by NormalizedUserName and or Email
|
||||
.AlterTable("UserPartRecord", table => table
|
||||
.CreateIndex($"IDX_UserPartRecord_NameAndEmail", "NormalizedUserName", "Email"));
|
||||
|
||||
//Password History Table
|
||||
SchemaBuilder
|
||||
.CreateTable("PasswordHistoryRecord", table => table
|
||||
.Column<int>("Id", col => col.PrimaryKey().Identity())
|
||||
.Column<int>("UserPartRecord_Id")
|
||||
.Column<string>("Password")
|
||||
.Column<string>("PasswordFormat")
|
||||
.Column<string>("HashAlgorithm")
|
||||
.Column<string>("PasswordSalt")
|
||||
.Column<DateTime>("LastPasswordChangeUtc", c => c.WithDefault(new DateTime(1990, 1, 1))))
|
||||
.AlterTable("PasswordHistoryRecord", table => table
|
||||
.CreateIndex($"IDX_UserPartRecord_Id", "UserPartRecord_Id"));
|
||||
|
||||
// Queryable bool to tell which users should not be suspended automatically
|
||||
SchemaBuilder
|
||||
.CreateTable("UserSecurityConfigurationPartRecord", table => table
|
||||
.ContentPartRecord()
|
||||
.Column<bool>("SaveFromSuspension")
|
||||
.Column<bool>("PreventPasswordExpiration"));
|
||||
|
||||
ContentDefinitionManager.AlterTypeDefinition("User", cfg => cfg.Creatable(false));
|
||||
|
||||
return 6;
|
||||
return 9;
|
||||
}
|
||||
|
||||
public int UpdateFrom1() {
|
||||
@@ -79,5 +103,32 @@ namespace Orchard.Users {
|
||||
});
|
||||
return 7;
|
||||
}
|
||||
public int UpdateFrom7() {
|
||||
SchemaBuilder.AlterTable("UserPartRecord", table => {
|
||||
table.AddColumn<bool>("ForcePasswordChange");
|
||||
});
|
||||
SchemaBuilder
|
||||
.CreateTable("PasswordHistoryRecord", table => table
|
||||
.Column<int>("Id", col => col.PrimaryKey().Identity())
|
||||
.Column<int>("UserPartRecord_Id")
|
||||
.Column<string>("Password")
|
||||
.Column<string>("PasswordFormat")
|
||||
.Column<string>("HashAlgorithm")
|
||||
.Column<string>("PasswordSalt")
|
||||
.Column<DateTime>("LastPasswordChangeUtc"))
|
||||
.AlterTable("PasswordHistoryRecord", table => table
|
||||
.CreateIndex($"IDX_UserPartRecord_Id", "UserPartRecord_Id"));
|
||||
return 8;
|
||||
}
|
||||
|
||||
public int UpdateFrom8() {
|
||||
SchemaBuilder
|
||||
.CreateTable("UserSecurityConfigurationPartRecord", table => table
|
||||
.ContentPartRecord()
|
||||
.Column<bool>("SaveFromSuspension")
|
||||
.Column<bool>("PreventPasswordExpiration"));
|
||||
|
||||
return 9;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Web;
|
||||
using System.Web.Security;
|
||||
|
||||
namespace Orchard.Users.Models {
|
||||
public class PasswordHistoryRecord {
|
||||
public virtual int Id { get; set; }
|
||||
public virtual UserPartRecord UserPartRecord { get; set; }
|
||||
public virtual string Password { get; set; }
|
||||
public virtual MembershipPasswordFormat PasswordFormat { get; set; }
|
||||
public virtual string HashAlgorithm { get; set; }
|
||||
public virtual string PasswordSalt { get; set; }
|
||||
public virtual DateTime? LastPasswordChangeUtc { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -91,5 +91,15 @@ namespace Orchard.Users.Models {
|
||||
get { return this.Retrieve(x => x.PasswordFormat, MembershipPasswordFormat.Hashed); }
|
||||
set { this.Store(x => x.PasswordFormat, value); }
|
||||
}
|
||||
|
||||
public bool EnablePasswordHistoryPolicy {
|
||||
get { return this.Retrieve(x => x.EnablePasswordHistoryPolicy); }
|
||||
set { this.Store(x => x.EnablePasswordHistoryPolicy, value); }
|
||||
}
|
||||
[Range(1, int.MaxValue, ErrorMessage = "The minimum password reuse limit must be at least 1.")]
|
||||
public int PasswordReuseLimit {
|
||||
get { return this.Retrieve(x => x.PasswordReuseLimit, 5); }
|
||||
set { this.Store(x => x.PasswordReuseLimit, value); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Security;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Web.Security;
|
||||
|
||||
namespace Orchard.Users.Models {
|
||||
@@ -81,5 +82,12 @@ namespace Orchard.Users.Models {
|
||||
get { return Retrieve(x => x.LastPasswordChangeUtc); }
|
||||
set { Store(x => x.LastPasswordChangeUtc, value); }
|
||||
}
|
||||
/// <summary>
|
||||
/// Set to true means that the user must change the password at next logon
|
||||
/// </summary>
|
||||
public bool ForcePasswordChange {
|
||||
get { return Retrieve(x => x.ForcePasswordChange); }
|
||||
set { Store(x => x.ForcePasswordChange, value); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Orchard.ContentManagement.Records;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Web.Security;
|
||||
|
||||
namespace Orchard.Users.Models {
|
||||
@@ -20,5 +22,6 @@ namespace Orchard.Users.Models {
|
||||
public virtual DateTime? LastLoginUtc { get; set; }
|
||||
public virtual DateTime? LastLogoutUtc { get; set; }
|
||||
public virtual DateTime? LastPasswordChangeUtc { get; set; }
|
||||
public virtual bool ForcePasswordChange { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Orchard.ContentManagement;
|
||||
|
||||
namespace Orchard.Users.Models {
|
||||
public class UserSecurityConfigurationPart : ContentPart<UserSecurityConfigurationPartRecord> {
|
||||
public bool SaveFromSuspension {
|
||||
get { return Retrieve(x => x.SaveFromSuspension); }
|
||||
set { Store(x => x.SaveFromSuspension, value); }
|
||||
}
|
||||
|
||||
public bool PreventPasswordExpiration {
|
||||
get { return Retrieve(x => x.PreventPasswordExpiration); }
|
||||
set { Store(x => x.PreventPasswordExpiration, value); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Orchard.ContentManagement.Records;
|
||||
|
||||
namespace Orchard.Users.Models {
|
||||
public class UserSecurityConfigurationPartRecord : ContentPartRecord {
|
||||
// We are creating a record for this rather than making do with the infoset
|
||||
// because this will allow us to explicitly query for those users that must
|
||||
// (or not) be saved from suspension.
|
||||
public virtual bool SaveFromSuspension { get; set; }
|
||||
public virtual bool PreventPasswordExpiration { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using Orchard.ContentManagement;
|
||||
|
||||
namespace Orchard.Users.Models {
|
||||
public class UserSuspensionSettingsPart : ContentPart {
|
||||
public bool SuspendInactiveUsers {
|
||||
get { return this.Retrieve(x => x.SuspendInactiveUsers); }
|
||||
set { this.Store(x => x.SuspendInactiveUsers, value); }
|
||||
}
|
||||
|
||||
public int AllowedInactivityDays {
|
||||
get { return this.Retrieve(x => x.AllowedInactivityDays, 90); }
|
||||
set { this.Store(x => x.AllowedInactivityDays, value); }
|
||||
}
|
||||
|
||||
public int MinimumSweepInterval {
|
||||
get { return this.Retrieve(x => x.MinimumSweepInterval, 12); }
|
||||
set { this.Store(x => x.MinimumSweepInterval, value); }
|
||||
}
|
||||
|
||||
public DateTime? LastSweepUtc {
|
||||
get { return this.Retrieve(x => x.LastSweepUtc); }
|
||||
set { this.Store(x => x.LastSweepUtc, value); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,6 +104,7 @@
|
||||
<Compile Include="Constants\UserPasswordValidationResults.cs" />
|
||||
<Compile Include="Controllers\AccountController.cs" />
|
||||
<Compile Include="Controllers\AdminController.cs" />
|
||||
<Compile Include="Drivers\UserSecurityConfigurationPartDriver.cs" />
|
||||
<Compile Include="Drivers\UserApprovePartDriver.cs" />
|
||||
<Compile Include="Drivers\UserPartDriver.cs" />
|
||||
<Compile Include="Drivers\UserPartPasswordDriver.cs" />
|
||||
@@ -114,7 +115,9 @@
|
||||
<Compile Include="Forms\VerifyUserUnicityForm.cs" />
|
||||
<Compile Include="Forms\CreateUserForm.cs" />
|
||||
<Compile Include="Handlers\ApproveUserHandler.cs" />
|
||||
<Compile Include="Handlers\UserSecurityConfigurationPartHandler.cs" />
|
||||
<Compile Include="Handlers\SecuritySettingsPartHandler.cs" />
|
||||
<Compile Include="Handlers\UserSuspensionSettingsPartHandler.cs" />
|
||||
<Compile Include="Handlers\WorkflowUserEventHandler.cs" />
|
||||
<Compile Include="Extensions\ModelStateDictionaryExtensions.cs" />
|
||||
<Compile Include="Migrations.cs" />
|
||||
@@ -122,20 +125,29 @@
|
||||
<Compile Include="Handlers\RegistrationSettingsPartHandler.cs" />
|
||||
<Compile Include="Events\IUserEventHandler.cs" />
|
||||
<Compile Include="Models\MessageTypes.cs" />
|
||||
<Compile Include="Models\PasswordHistoryRecord.cs" />
|
||||
<Compile Include="Models\UserSecurityConfigurationPart.cs" />
|
||||
<Compile Include="Models\UserSecurityConfigurationPartRecord.cs" />
|
||||
<Compile Include="Models\RegistrationSettingsPart.cs" />
|
||||
<Compile Include="Models\SecuritySettingsPart.cs" />
|
||||
<Compile Include="Models\UserPart.cs" />
|
||||
<Compile Include="Handlers\UserPartHandler.cs" />
|
||||
<Compile Include="Models\UserPartRecord.cs" />
|
||||
<Compile Include="Models\UserStatus.cs" />
|
||||
<Compile Include="Models\UserSuspensionSettingsPart.cs" />
|
||||
<Compile Include="Permissions.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Services\ApproveUserService.cs" />
|
||||
<Compile Include="Services\AccountValidationService.cs" />
|
||||
<Compile Include="Services\AuthenticationRedirectionFilter.cs" />
|
||||
<Compile Include="Services\InactiveUserSuspensionBackgroundTask.cs" />
|
||||
<Compile Include="Services\IUserService.cs" />
|
||||
<Compile Include="Services\IUserSuspensionConditionProvider.cs" />
|
||||
<Compile Include="Services\MembershipValidationService.cs" />
|
||||
<Compile Include="Services\PasswordChangedDateUserDataProvider.cs" />
|
||||
<Compile Include="Services\PasswordHistoryService.cs" />
|
||||
<Compile Include="Services\PasswordService.cs" />
|
||||
<Compile Include="Services\ProtectSpecificUserConditionProvider.cs" />
|
||||
<Compile Include="Services\UserResolverSelector.cs" />
|
||||
<Compile Include="Services\MembershipService.cs" />
|
||||
<Compile Include="AdminMenu.cs" />
|
||||
@@ -230,10 +242,15 @@
|
||||
<Content Include="Views\Template.User.Wrapper.cshtml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Placement.info" />
|
||||
<Content Include="Placement.info">
|
||||
<SubType>Designer</SubType>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
<Content Include="Views\EditorTemplates\Parts\Users.SuspensionSettings.cshtml" />
|
||||
<Content Include="Views\EditorTemplates\Parts\User.UserSecurityConfiguration.cshtml" />
|
||||
<Content Include="Views\Account\RequestChallengeEmail.cshtml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Views\Account\ChangeExpiredPassword.cshtml" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<Placement>
|
||||
<Match Path="~/Admin/Users/Edit/*">
|
||||
<Place Parts_User_EditPassword_Edit="Content:1"/>
|
||||
<Place Parts_User_UserSecurityConfiguration_Edit="Content:1.1"/>
|
||||
<Place Parts_UserApprove_Edit="Sidebar:25"/> <!-- immediately following the contents module's Approve Now button -->
|
||||
</Match>
|
||||
</Placement>
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Orchard.Users.Services {
|
||||
|
||||
public bool ValidatePassword(AccountValidationContext context) {
|
||||
IDictionary<string, LocalizedString> validationErrors;
|
||||
_userService.PasswordMeetsPolicies(context.Password, out validationErrors);
|
||||
_userService.PasswordMeetsPolicies(context.Password, context.User, out validationErrors);
|
||||
if (validationErrors != null && validationErrors.Any()) {
|
||||
foreach (var err in validationErrors) {
|
||||
if (!context.ValidationErrors.ContainsKey(err.Key)) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Orchard.Localization;
|
||||
using Orchard.Security;
|
||||
using Orchard.Users.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
@@ -7,6 +8,7 @@ namespace Orchard.Users.Services {
|
||||
public interface IUserService : IDependency {
|
||||
bool VerifyUserUnicity(string userName, string email);
|
||||
bool VerifyUserUnicity(int id, string userName, string email);
|
||||
UserPart GetUserByNameOrEmail(string usernameOrEmail);
|
||||
|
||||
void SendChallengeEmail(IUser user, Func<string, string> createUrl);
|
||||
IUser ValidateChallenge(string challengeToken);
|
||||
@@ -17,6 +19,6 @@ namespace Orchard.Users.Services {
|
||||
string CreateNonce(IUser user, TimeSpan delay);
|
||||
bool DecryptNonce(string challengeToken, out string username, out DateTime validateByUtc);
|
||||
|
||||
bool PasswordMeetsPolicies(string password, out IDictionary<string, LocalizedString> validationErrors);
|
||||
bool PasswordMeetsPolicies(string password, IUser user, out IDictionary<string, LocalizedString> validationErrors);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Users.Models;
|
||||
|
||||
namespace Orchard.Users.Services {
|
||||
public interface IUserSuspensionConditionProvider : IDependency {
|
||||
IContentQuery<UserPart> AlterQuery(
|
||||
IContentQuery<UserPart> query);
|
||||
|
||||
bool UserIsProtected(UserPart userPart);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ namespace Orchard.Users.Services {
|
||||
private readonly IShapeFactory _shapeFactory;
|
||||
private readonly IShapeDisplay _shapeDisplay;
|
||||
private readonly IAppConfigurationAccessor _appConfigurationAccessor;
|
||||
private readonly IPasswordService _passwordService;
|
||||
private readonly IClock _clock;
|
||||
|
||||
public MembershipService(
|
||||
@@ -40,7 +41,8 @@ namespace Orchard.Users.Services {
|
||||
IEncryptionService encryptionService,
|
||||
IShapeFactory shapeFactory,
|
||||
IShapeDisplay shapeDisplay,
|
||||
IAppConfigurationAccessor appConfigurationAccessor) {
|
||||
IAppConfigurationAccessor appConfigurationAccessor,
|
||||
IPasswordService passwordService) {
|
||||
_orchardServices = orchardServices;
|
||||
_messageService = messageService;
|
||||
_userEventHandlers = userEventHandlers;
|
||||
@@ -48,6 +50,7 @@ namespace Orchard.Users.Services {
|
||||
_shapeFactory = shapeFactory;
|
||||
_shapeDisplay = shapeDisplay;
|
||||
_appConfigurationAccessor = appConfigurationAccessor;
|
||||
_passwordService = passwordService;
|
||||
_clock = clock;
|
||||
Logger = NullLogger.Instance;
|
||||
T = NullLocalizer.Instance;
|
||||
@@ -72,14 +75,15 @@ namespace Orchard.Users.Services {
|
||||
user.NormalizedUserName = createUserParams.Username.ToLowerInvariant();
|
||||
user.HashAlgorithm = PBKDF2;
|
||||
user.CreatedUtc = _clock.UtcNow;
|
||||
user.ForcePasswordChange = createUserParams.ForcePasswordChange;
|
||||
SetPassword(user, createUserParams.Password);
|
||||
|
||||
if ( registrationSettings != null) {
|
||||
if (registrationSettings != null) {
|
||||
user.RegistrationStatus = registrationSettings.UsersAreModerated ? UserStatus.Pending : UserStatus.Approved;
|
||||
user.EmailStatus = registrationSettings.UsersMustValidateEmail ? UserStatus.Pending : UserStatus.Approved;
|
||||
}
|
||||
|
||||
if(createUserParams.IsApproved) {
|
||||
if (createUserParams.IsApproved) {
|
||||
user.RegistrationStatus = UserStatus.Approved;
|
||||
user.EmailStatus = UserStatus.Approved;
|
||||
}
|
||||
@@ -87,7 +91,7 @@ namespace Orchard.Users.Services {
|
||||
var userContext = new UserContext { User = user, Cancel = false, UserParameters = createUserParams };
|
||||
_userEventHandlers.Creating(userContext);
|
||||
|
||||
if(userContext.Cancel) {
|
||||
if (userContext.Cancel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -98,15 +102,15 @@ namespace Orchard.Users.Services {
|
||||
_userEventHandlers.Approved(user);
|
||||
}
|
||||
|
||||
if ( registrationSettings != null
|
||||
if (registrationSettings != null
|
||||
&& registrationSettings.UsersAreModerated
|
||||
&& registrationSettings.NotifyModeration
|
||||
&& !createUserParams.IsApproved) {
|
||||
var usernames = String.IsNullOrWhiteSpace(registrationSettings.NotificationsRecipients)
|
||||
? new string[0]
|
||||
: registrationSettings.NotificationsRecipients.Split(new[] {',', ' '}, StringSplitOptions.RemoveEmptyEntries);
|
||||
: registrationSettings.NotificationsRecipients.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach ( var userName in usernames ) {
|
||||
foreach (var userName in usernames) {
|
||||
if (String.IsNullOrWhiteSpace(userName)) {
|
||||
continue;
|
||||
}
|
||||
@@ -142,7 +146,12 @@ namespace Orchard.Users.Services {
|
||||
if (user == null)
|
||||
user = _orchardServices.ContentManager.Query<UserPart, UserPartRecord>().Where(u => u.Email == lowerName).List().FirstOrDefault();
|
||||
|
||||
if (user == null || ValidatePassword(user.As<UserPart>(), password) == false) {
|
||||
if (user == null || !_passwordService.IsMatch(new PasswordContext {
|
||||
Password = user.Password,
|
||||
HashAlgorithm = user.HashAlgorithm,
|
||||
PasswordFormat = user.PasswordFormat,
|
||||
PasswordSalt = user.PasswordSalt
|
||||
}, password)) {
|
||||
validationErrors.Add(T("The username or e-mail or password provided is incorrect."));
|
||||
return null;
|
||||
}
|
||||
@@ -157,7 +166,12 @@ namespace Orchard.Users.Services {
|
||||
}
|
||||
|
||||
public bool PasswordIsExpired(IUser user, int days) {
|
||||
return user.As<UserPart>().LastPasswordChangeUtc.Value.AddDays(days) < _clock.UtcNow;
|
||||
// TODO: add providers to extend this
|
||||
var passwordIsExpired = user.As<UserPart>().LastPasswordChangeUtc.Value.AddDays(days) < _clock.UtcNow;
|
||||
var securityPart = user.As<UserSecurityConfigurationPart>();
|
||||
var preventExpiration = securityPart != null && securityPart.PreventPasswordExpiration;
|
||||
return passwordIsExpired
|
||||
&& !preventExpiration;
|
||||
}
|
||||
|
||||
public void SetPassword(IUser user, string password) {
|
||||
@@ -182,118 +196,28 @@ namespace Orchard.Users.Services {
|
||||
userPart.LastPasswordChangeUtc = _clock.UtcNow;
|
||||
}
|
||||
|
||||
private bool ValidatePassword(UserPart userPart, string password) {
|
||||
// Note - the password format stored with the record is used
|
||||
// otherwise changing the password format on the site would invalidate
|
||||
// all logins
|
||||
switch (userPart.PasswordFormat) {
|
||||
case MembershipPasswordFormat.Clear:
|
||||
return ValidatePasswordClear(userPart, password);
|
||||
case MembershipPasswordFormat.Hashed:
|
||||
return ValidatePasswordHashed(userPart, password);
|
||||
case MembershipPasswordFormat.Encrypted:
|
||||
return ValidatePasswordEncrypted(userPart, password);
|
||||
default:
|
||||
throw new ApplicationException("Unexpected password format value");
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetPasswordClear(UserPart userPart, string password) {
|
||||
userPart.PasswordFormat = MembershipPasswordFormat.Clear;
|
||||
userPart.Password = password;
|
||||
userPart.PasswordSalt = null;
|
||||
}
|
||||
|
||||
private static bool ValidatePasswordClear(UserPart userPart, string password) {
|
||||
return userPart.Password == password;
|
||||
}
|
||||
|
||||
private static void SetPasswordHashed(UserPart userPart, string password) {
|
||||
private void SetPasswordHashed(UserPart userPart, string password) {
|
||||
var saltBytes = new byte[0x10];
|
||||
using (var random = new RNGCryptoServiceProvider()) {
|
||||
random.GetBytes(saltBytes);
|
||||
}
|
||||
|
||||
userPart.PasswordFormat = MembershipPasswordFormat.Hashed;
|
||||
userPart.Password = ComputeHashBase64(userPart.HashAlgorithm, saltBytes, password);
|
||||
userPart.Password = PasswordExtensions.ComputeHashBase64(userPart.HashAlgorithm, saltBytes, password);
|
||||
userPart.PasswordSalt = Convert.ToBase64String(saltBytes);
|
||||
}
|
||||
|
||||
private bool ValidatePasswordHashed(UserPart userPart, string password) {
|
||||
var saltBytes = Convert.FromBase64String(userPart.PasswordSalt);
|
||||
|
||||
bool isValid;
|
||||
if (userPart.HashAlgorithm == PBKDF2) {
|
||||
// We can't reuse ComputeHashBase64 as the internally generated salt repeated calls to Crypto.HashPassword() return different results.
|
||||
isValid = Crypto.VerifyHashedPassword(userPart.Password, Encoding.Unicode.GetString(CombineSaltAndPassword(saltBytes, password)));
|
||||
}
|
||||
else {
|
||||
isValid = SecureStringEquality(userPart.Password, ComputeHashBase64(userPart.HashAlgorithm, saltBytes, password));
|
||||
}
|
||||
|
||||
// Migrating older password hashes to Default algorithm if necessary and enabled.
|
||||
if (isValid && userPart.HashAlgorithm != DefaultHashAlgorithm) {
|
||||
var keepOldConfiguration = _appConfigurationAccessor.GetConfiguration("Orchard.Users.KeepOldPasswordHash");
|
||||
if (String.IsNullOrEmpty(keepOldConfiguration) || keepOldConfiguration.Equals("false", StringComparison.OrdinalIgnoreCase)) {
|
||||
userPart.HashAlgorithm = DefaultHashAlgorithm;
|
||||
userPart.Password = ComputeHashBase64(userPart.HashAlgorithm, saltBytes, password);
|
||||
}
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
private static string ComputeHashBase64(string hashAlgorithmName, byte[] saltBytes, string password) {
|
||||
var combinedBytes = CombineSaltAndPassword(saltBytes, password);
|
||||
|
||||
// Extending HashAlgorithm would be too complicated: http://stackoverflow.com/questions/6460711/adding-a-custom-hashalgorithmtype-in-c-sharp-asp-net?lq=1
|
||||
if (hashAlgorithmName == PBKDF2) {
|
||||
// HashPassword() already returns a base64 string.
|
||||
return Crypto.HashPassword(Encoding.Unicode.GetString(combinedBytes));
|
||||
}
|
||||
else {
|
||||
using (var hashAlgorithm = HashAlgorithm.Create(hashAlgorithmName)) {
|
||||
return Convert.ToBase64String(hashAlgorithm.ComputeHash(combinedBytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares two strings without giving hint about the time it takes to do so.
|
||||
/// </summary>
|
||||
/// <param name="a">The first string to compare.</param>
|
||||
/// <param name="b">The second string to compare.</param>
|
||||
/// <returns><c>true</c> if both strings are equal, <c>false</c>.</returns>
|
||||
private bool SecureStringEquality(string a, string b) {
|
||||
if (a == null || b == null || (a.Length != b.Length)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var aBytes = Encoding.Unicode.GetBytes(a);
|
||||
var bBytes = Encoding.Unicode.GetBytes(b);
|
||||
|
||||
var bytesAreEqual = true;
|
||||
for (int i = 0; i < a.Length; i++) {
|
||||
bytesAreEqual &= (aBytes[i] == bBytes[i]);
|
||||
}
|
||||
|
||||
return bytesAreEqual;
|
||||
}
|
||||
|
||||
private static byte[] CombineSaltAndPassword(byte[] saltBytes, string password) {
|
||||
var passwordBytes = Encoding.Unicode.GetBytes(password);
|
||||
return saltBytes.Concat(passwordBytes).ToArray();
|
||||
}
|
||||
|
||||
private void SetPasswordEncrypted(UserPart userPart, string password) {
|
||||
userPart.Password = Convert.ToBase64String(_encryptionService.Encode(Encoding.UTF8.GetBytes(password)));
|
||||
userPart.PasswordSalt = null;
|
||||
userPart.PasswordFormat = MembershipPasswordFormat.Encrypted;
|
||||
}
|
||||
|
||||
private bool ValidatePasswordEncrypted(UserPart userPart, string password) {
|
||||
return String.Equals(password, Encoding.UTF8.GetString(_encryptionService.Decode(Convert.FromBase64String(userPart.Password))), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Web;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Data;
|
||||
using Orchard.Security;
|
||||
using Orchard.Users.Models;
|
||||
|
||||
namespace Orchard.Users.Services {
|
||||
public class PasswordHistoryService : IPasswordHistoryService {
|
||||
private readonly IEnumerable<PasswordHistoryEntry> emptyPasswordHistoryEntries = new List<PasswordHistoryEntry>();
|
||||
private readonly IRepository<PasswordHistoryRecord> _historyRepository;
|
||||
private readonly IPasswordService _passwordService;
|
||||
|
||||
public PasswordHistoryService(IRepository<PasswordHistoryRecord> historyRepository, IPasswordService passwordService) {
|
||||
_historyRepository = historyRepository;
|
||||
_passwordService = passwordService;
|
||||
}
|
||||
|
||||
public void CreateEntry(PasswordHistoryEntry context) {
|
||||
_historyRepository.Create(new PasswordHistoryRecord {
|
||||
UserPartRecord = context.User?.As<UserPart>()?.Record,
|
||||
HashAlgorithm = context.HashAlgorithm,
|
||||
Password = context.Password,
|
||||
PasswordFormat = context.PasswordFormat,
|
||||
PasswordSalt = context.PasswordSalt,
|
||||
LastPasswordChangeUtc = context.LastPasswordChangeUtc,
|
||||
});
|
||||
}
|
||||
|
||||
public IEnumerable<PasswordHistoryEntry> GetLastPasswords(IUser user, int count) {
|
||||
if (user == null)
|
||||
return emptyPasswordHistoryEntries;
|
||||
var lastPasswords = _historyRepository
|
||||
.Fetch(x => x.UserPartRecord.Id == user.Id)
|
||||
.OrderByDescending(x => x.LastPasswordChangeUtc)
|
||||
//because we append the last password (stored within the UserPart) we take 1 less password from history
|
||||
.Take(count - 1)
|
||||
.Select(x => new PasswordHistoryEntry {
|
||||
Password = x.Password,
|
||||
PasswordSalt = x.PasswordSalt,
|
||||
HashAlgorithm = x.HashAlgorithm,
|
||||
PasswordFormat = x.PasswordFormat,
|
||||
LastPasswordChangeUtc = x.LastPasswordChangeUtc,
|
||||
User = user
|
||||
});
|
||||
return lastPasswords
|
||||
//we append the last used password stored within the UserPart
|
||||
.Append(new PasswordHistoryEntry {
|
||||
Password = user.As<UserPart>().Password,
|
||||
PasswordSalt = user.As<UserPart>().PasswordSalt,
|
||||
HashAlgorithm = user.As<UserPart>().HashAlgorithm,
|
||||
PasswordFormat = user.As<UserPart>().PasswordFormat,
|
||||
LastPasswordChangeUtc = user.As<UserPart>().LastPasswordChangeUtc,
|
||||
User = user });
|
||||
}
|
||||
|
||||
public bool PasswordMatchLastOnes(string password, IUser user, int count) {
|
||||
if (user == null)
|
||||
return false;
|
||||
return GetLastPasswords(user, count)
|
||||
.Any(x => _passwordService.IsMatch(x, password));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using System.Web.Helpers;
|
||||
using System.Web.Security;
|
||||
using Orchard.Environment.Configuration;
|
||||
using Orchard.Security;
|
||||
|
||||
namespace Orchard.Users.Services {
|
||||
public class PasswordService : IPasswordService {
|
||||
private const string DefaultHashAlgorithm = PasswordExtensions.PBKDF2;
|
||||
private readonly IEncryptionService _encryptionService;
|
||||
private readonly IAppConfigurationAccessor _appConfigurationAccessor;
|
||||
|
||||
public PasswordService(
|
||||
IEncryptionService encryptionService,
|
||||
IAppConfigurationAccessor appConfigurationAccessor) {
|
||||
_encryptionService = encryptionService;
|
||||
_appConfigurationAccessor = appConfigurationAccessor;
|
||||
}
|
||||
|
||||
public bool IsMatch(PasswordContext context, string plaintextPassword) {
|
||||
switch (context.PasswordFormat) {
|
||||
case MembershipPasswordFormat.Clear:
|
||||
return EqualsClear(context, plaintextPassword);
|
||||
case MembershipPasswordFormat.Hashed:
|
||||
return EqualsHashed(context, plaintextPassword);
|
||||
case MembershipPasswordFormat.Encrypted:
|
||||
return EqualsEncrypted(context, plaintextPassword);
|
||||
default:
|
||||
throw new ApplicationException("Unexpected password format value");
|
||||
}
|
||||
}
|
||||
|
||||
private bool EqualsClear(PasswordContext context, string plaintextPassword) {
|
||||
return context.Password == plaintextPassword;
|
||||
}
|
||||
private bool EqualsHashed(PasswordContext context, string plaintextPassword) {
|
||||
var saltBytes = Convert.FromBase64String(context.PasswordSalt);
|
||||
|
||||
bool isValid;
|
||||
if (context.HashAlgorithm == PasswordExtensions.PBKDF2) {
|
||||
// We can't reuse ComputeHashBase64 as the internally generated salt repeated calls to Crypto.HashPassword() return different results.
|
||||
isValid = Crypto.VerifyHashedPassword(context.Password, Encoding.Unicode.GetString(PasswordExtensions.CombineSaltAndPassword(saltBytes, plaintextPassword)));
|
||||
}
|
||||
else {
|
||||
isValid = SecureStringEquality(context.Password, PasswordExtensions.ComputeHashBase64(context.HashAlgorithm, saltBytes, plaintextPassword));
|
||||
}
|
||||
|
||||
// Migrating older password hashes to Default algorithm if necessary and enabled.
|
||||
if (isValid && context.HashAlgorithm != DefaultHashAlgorithm) {
|
||||
var keepOldConfiguration = _appConfigurationAccessor.GetConfiguration("Orchard.Users.KeepOldPasswordHash");
|
||||
if (String.IsNullOrEmpty(keepOldConfiguration) || keepOldConfiguration.Equals("false", StringComparison.OrdinalIgnoreCase)) {
|
||||
context.HashAlgorithm = DefaultHashAlgorithm;
|
||||
context.Password = PasswordExtensions.ComputeHashBase64(context.HashAlgorithm, saltBytes, plaintextPassword);
|
||||
}
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
private bool EqualsEncrypted(PasswordContext context, string plaintextPassword) {
|
||||
return String.Equals(plaintextPassword, Encoding.UTF8.GetString(_encryptionService.Decode(Convert.FromBase64String(context.Password))), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Compares two strings without giving hint about the time it takes to do so.
|
||||
/// </summary>
|
||||
/// <param name="a">The first string to compare.</param>
|
||||
/// <param name="b">The second string to compare.</param>
|
||||
/// <returns><c>true</c> if both strings are equal, <c>false</c>.</returns>
|
||||
private bool SecureStringEquality(string a, string b) {
|
||||
if (a == null || b == null || (a.Length != b.Length)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var aBytes = Encoding.Unicode.GetBytes(a);
|
||||
var bBytes = Encoding.Unicode.GetBytes(b);
|
||||
|
||||
var bytesAreEqual = true;
|
||||
for (int i = 0; i < a.Length; i++) {
|
||||
bytesAreEqual &= (aBytes[i] == bBytes[i]);
|
||||
}
|
||||
|
||||
return bytesAreEqual;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Users.Models;
|
||||
|
||||
namespace Orchard.Users.Services {
|
||||
public class ProtectSpecificUserConditionProvider : IUserSuspensionConditionProvider {
|
||||
|
||||
// Method to add conditions to the query that fetches the users that we may
|
||||
// try to suspend
|
||||
public IContentQuery<UserPart> AlterQuery(
|
||||
IContentQuery<UserPart> query) {
|
||||
|
||||
// Don't fetch the users that are protected from suspension
|
||||
query = query
|
||||
.Where<UserSecurityConfigurationPartRecord>(pr => !pr.SaveFromSuspension);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
// Method to tell whether a specific user should be "saved" from suspension
|
||||
public bool UserIsProtected(UserPart userPart) {
|
||||
|
||||
return userPart
|
||||
.As<UserSecurityConfigurationPart>()
|
||||
.SaveFromSuspension;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using Orchard.Environment.Configuration;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Logging;
|
||||
using Orchard.Messaging.Services;
|
||||
using Orchard.Mvc.Html;
|
||||
using Orchard.Security;
|
||||
using Orchard.Services;
|
||||
using Orchard.Settings;
|
||||
@@ -30,6 +31,7 @@ namespace Orchard.Users.Services {
|
||||
private readonly IShapeFactory _shapeFactory;
|
||||
private readonly IShapeDisplay _shapeDisplay;
|
||||
private readonly ISiteService _siteService;
|
||||
private readonly IPasswordHistoryService _passwordHistoryService;
|
||||
|
||||
public UserService(
|
||||
IContentManager contentManager,
|
||||
@@ -40,7 +42,8 @@ namespace Orchard.Users.Services {
|
||||
IEncryptionService encryptionService,
|
||||
IShapeFactory shapeFactory,
|
||||
IShapeDisplay shapeDisplay,
|
||||
ISiteService siteService) {
|
||||
ISiteService siteService,
|
||||
IPasswordHistoryService passwordHistoryService) {
|
||||
|
||||
_contentManager = contentManager;
|
||||
_membershipService = membershipService;
|
||||
@@ -50,7 +53,7 @@ namespace Orchard.Users.Services {
|
||||
_shapeFactory = shapeFactory;
|
||||
_shapeDisplay = shapeDisplay;
|
||||
_siteService = siteService;
|
||||
|
||||
_passwordHistoryService = passwordHistoryService;
|
||||
Logger = NullLogger.Instance;
|
||||
T = NullLocalizer.Instance;
|
||||
}
|
||||
@@ -155,8 +158,7 @@ namespace Orchard.Users.Services {
|
||||
}
|
||||
|
||||
public bool SendLostPasswordEmail(string usernameOrEmail, Func<string, string> createUrl) {
|
||||
var lowerName = usernameOrEmail.ToLowerInvariant();
|
||||
var user = _contentManager.Query<UserPart, UserPartRecord>().Where(u => u.NormalizedUserName == lowerName || u.Email == lowerName).List().FirstOrDefault();
|
||||
var user = GetUserByNameOrEmail(usernameOrEmail);
|
||||
|
||||
if (user != null) {
|
||||
string nonce = CreateNonce(user, DelayToResetPassword);
|
||||
@@ -199,7 +201,7 @@ namespace Orchard.Users.Services {
|
||||
return user;
|
||||
}
|
||||
|
||||
public bool PasswordMeetsPolicies(string password, out IDictionary<string, LocalizedString> validationErrors) {
|
||||
public bool PasswordMeetsPolicies(string password, IUser user, out IDictionary<string, LocalizedString> validationErrors) {
|
||||
validationErrors = new Dictionary<string, LocalizedString>();
|
||||
var settings = _siteService.GetSiteSettings().As<RegistrationSettingsPart>();
|
||||
|
||||
@@ -231,9 +233,25 @@ namespace Orchard.Users.Services {
|
||||
validationErrors.Add(UserPasswordValidationResults.PasswordDoesNotContainSpecialCharacters,
|
||||
T("The password must contain at least one special character."));
|
||||
}
|
||||
if (settings.EnablePasswordHistoryPolicy) {
|
||||
var enforcePasswordHistory = settings.GetPasswordReuseLimit();
|
||||
if (_passwordHistoryService.PasswordMatchLastOnes(password, user, enforcePasswordHistory)) {
|
||||
validationErrors.Add(UserPasswordValidationResults.PasswordDoesNotMeetHistoryPolicy,
|
||||
T.Plural("You cannot reuse the last password.", "You cannot reuse none of last {0} passwords.", enforcePasswordHistory));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return validationErrors.Count == 0;
|
||||
}
|
||||
|
||||
public UserPart GetUserByNameOrEmail(string usernameOrEmail) {
|
||||
var lowerName = usernameOrEmail.ToLowerInvariant();
|
||||
return _contentManager
|
||||
.Query<UserPart, UserPartRecord>()
|
||||
.Where(u => u.NormalizedUserName == lowerName || u.Email == lowerName)
|
||||
.Slice(0, 1)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using Orchard.ContentManagement;
|
||||
using Orchard.Users.Models;
|
||||
|
||||
namespace Orchard.Users.ViewModels {
|
||||
public class UserCreateViewModel {
|
||||
public class UserCreateViewModel {
|
||||
[Required]
|
||||
public string UserName { get; set; }
|
||||
|
||||
@@ -15,5 +15,8 @@ namespace Orchard.Users.ViewModels {
|
||||
|
||||
[Required, DataType(DataType.Password)]
|
||||
public string ConfirmPassword { get; set; }
|
||||
|
||||
public bool ForcePasswordChange { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,11 @@ namespace Orchard.Users.ViewModels {
|
||||
set { User.As<UserPart>().Email = value; }
|
||||
}
|
||||
|
||||
public bool ForcePasswordChange {
|
||||
get { return User.As<UserPart>().ForcePasswordChange; }
|
||||
set { User.As<UserPart>().ForcePasswordChange = value; }
|
||||
}
|
||||
|
||||
public IContent User { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
@model dynamic
|
||||
<h1>@Html.TitleForPage(T("Challenge Email").ToString()) </h1>
|
||||
<p>@T("Your email address could not be validated.") </p>
|
||||
<p>
|
||||
@T("Follow <a href=\"{0}\">this link</a> to request a new challenge email.",
|
||||
Url.Action("RequestChallengeEmail"))
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
@model string
|
||||
@{
|
||||
var prefill = (string)Model;
|
||||
}
|
||||
|
||||
<h1>@Html.TitleForPage(T("Confirm Email Address").ToString())</h1>
|
||||
<p>@T("Please enter your username or email address. You will receive a link via email to validate your address.")</p>
|
||||
@using (Html.BeginFormAntiForgeryPost()) {
|
||||
<fieldset>
|
||||
<legend>@T("Account Information")</legend>
|
||||
<div>
|
||||
<label for="username">@T("Username or E-mail:")</label>
|
||||
@if (string.IsNullOrWhiteSpace(prefill)) {
|
||||
@Html.TextBox("username")
|
||||
} else {
|
||||
@Html.TextBox("username", value: prefill)
|
||||
}
|
||||
@Html.ValidationMessage("username")
|
||||
</div>
|
||||
<div>
|
||||
<button class="primaryAction" type="submit">@T("Send Request")</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
}
|
||||
@@ -19,3 +19,7 @@
|
||||
@Html.PasswordFor(m=>m.ConfirmPassword, new { @class = "text medium" })
|
||||
@Html.ValidationMessageFor(m=>m.ConfirmPassword, "*")
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
@Html.CheckBoxFor(m => m.ForcePasswordChange)
|
||||
@Html.LabelFor(m => m.ForcePasswordChange, T("User must change password at next logon").Text, new { @class = "forcheckbox" })
|
||||
</fieldset>
|
||||
@@ -9,3 +9,7 @@
|
||||
@Html.TextBoxFor(m=>m.Email, new { @class = "text medium" })
|
||||
@Html.ValidationMessageFor(m=>m.Email, "*")
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
@Html.CheckBoxFor(m => m.ForcePasswordChange)
|
||||
@Html.LabelFor(m => m.ForcePasswordChange, T("User must change password at next logon").Text, new { @class = "forcheckbox" })
|
||||
</fieldset>
|
||||
@@ -0,0 +1,13 @@
|
||||
@model Orchard.Users.Models.UserSecurityConfigurationPart
|
||||
<fieldset>
|
||||
@Html.CheckBoxFor(m => m.SaveFromSuspension)
|
||||
@Html.LabelFor(m => m.SaveFromSuspension,
|
||||
T("User will not be automatically suspended by periodic sweeps.").Text,
|
||||
new { @class = "forcheckbox" })
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
@Html.CheckBoxFor(m => m.PreventPasswordExpiration)
|
||||
@Html.LabelFor(m => m.PreventPasswordExpiration,
|
||||
T("User will not be required to renew their password when it's expired.").Text,
|
||||
new { @class = "forcheckbox" })
|
||||
</fieldset>
|
||||
@@ -66,56 +66,64 @@
|
||||
new SelectListItem { Text = T("Encrypted").Text, Value = MembershipPasswordFormat.Encrypted.ToString() }
|
||||
}, "Value", "Text"))
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@Html.EditorFor(m => m.EnablePasswordHistoryPolicy)
|
||||
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.EnablePasswordHistoryPolicy)">@T("Enable password history policy")</label>
|
||||
<div data-controllerid="@Html.FieldIdFor(m => m.EnablePasswordHistoryPolicy)" style="margin-left: 30px;">
|
||||
<label for="@Html.FieldIdFor(m => m.PasswordReuseLimit)">@T("Block reuse of last n passwords")</label>
|
||||
@Html.TextBoxFor(m => m.PasswordReuseLimit, new { @class = "text medium", @Value = Model.PasswordReuseLimit })
|
||||
@Html.ValidationMessage("PasswordReuseLimit", "*")
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" value="true" class="check-box" id="@Html.FieldIdFor(m => m.EnableLostPassword)" name="@Html.FieldNameFor(m => m.EnableLostPassword)" @(Model.EnableLostPassword ? "checked=\"checked\"" : "") @(emailEnabled ? "" : "disabled=\"disabled\"") />
|
||||
<input name="@Html.FieldNameFor(m => m.EnableLostPassword)" type="hidden" value="false">
|
||||
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.EnableLostPassword)">@T("Display a link to enable users to reset their password")</label>
|
||||
|
||||
<div>
|
||||
<input type="checkbox" value="true" class="check-box" id="@Html.FieldIdFor(m => m.EnableLostPassword)" name="@Html.FieldNameFor(m => m.EnableLostPassword)" @(Model.EnableLostPassword ? "checked=\"checked\"" : "") @(emailEnabled ? "" : "disabled=\"disabled\"")/>
|
||||
<input name="@Html.FieldNameFor(m => m.EnableLostPassword)" type="hidden" value="false">
|
||||
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.EnableLostPassword)">@T("Display a link to enable users to reset their password")</label>
|
||||
@if (!emailEnabled) {
|
||||
<div class="message message-Warning">@T("This option is available when an email module is activated.")</div>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" value="true" class="check-box" id="@Html.FieldIdFor(m => m.UsersMustValidateEmail)" name="@Html.FieldNameFor(m => m.UsersMustValidateEmail)" @(Model.UsersMustValidateEmail ? "checked=\"checked\"" : "") @(emailEnabled ? "" : "disabled=\"disabled\"") />
|
||||
<input name="@Html.FieldNameFor(m => m.UsersMustValidateEmail)" type="hidden" value="false">
|
||||
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.UsersMustValidateEmail)">@T("Users must verify their email address")</label>
|
||||
|
||||
@if(!emailEnabled) {
|
||||
<div class="message message-Warning">@T("This option is available when an email module is activated.")</div>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" value="true" class="check-box" id="@Html.FieldIdFor(m => m.UsersMustValidateEmail)" name="@Html.FieldNameFor(m => m.UsersMustValidateEmail)" @(Model.UsersMustValidateEmail ? "checked=\"checked\"" : "") @(emailEnabled ? "" : "disabled=\"disabled\"")/>
|
||||
<input name="@Html.FieldNameFor(m => m.UsersMustValidateEmail)" type="hidden" value="false">
|
||||
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.UsersMustValidateEmail)">@T("Users must verify their email address")</label>
|
||||
@if (!emailEnabled) {
|
||||
<div class="message message-Warning">@T("This option is available when an email module is activated.")</div>
|
||||
}
|
||||
</div>
|
||||
<div data-controllerid="@Html.FieldIdFor(m => m.UsersMustValidateEmail)">
|
||||
<label for="@Html.FieldIdFor(m => m.ValidateEmailRegisteredWebsite)">@T("Website public name")</label>
|
||||
@Html.TextBoxFor(m => m.ValidateEmailRegisteredWebsite, new { @class = "text medium" })
|
||||
@Html.ValidationMessage("ValidateEmailRegisteredWebsite", "*")
|
||||
<span class="hint">@T("The name of your website as it will appear in the verification e-mail.")</span>
|
||||
|
||||
@if(!emailEnabled) {
|
||||
<div class="message message-Warning">@T("This option is available when an email module is activated.")</div>
|
||||
}
|
||||
</div>
|
||||
<div data-controllerid="@Html.FieldIdFor(m => m.UsersMustValidateEmail)">
|
||||
<label for="@Html.FieldIdFor(m => m.ValidateEmailRegisteredWebsite)">@T("Website public name")</label>
|
||||
@Html.TextBoxFor(m => m.ValidateEmailRegisteredWebsite, new { @class = "text medium" } )
|
||||
@Html.ValidationMessage("ValidateEmailRegisteredWebsite", "*")
|
||||
<span class="hint">@T("The name of your website as it will appear in the verification e-mail.")</span>
|
||||
<label for="@Html.FieldIdFor(m => m.ValidateEmailContactEMail)">@T("Contact Us E-Mail address")</label>
|
||||
@Html.TextBoxFor(m => m.ValidateEmailContactEMail, new { @class = "text medium" })
|
||||
@Html.ValidationMessage("ValidateEmailContactEMail", "*")
|
||||
<span class="hint">@T("The e-mail address displayed in the verification e-mail for a Contact Us link. Leave empty for no link.")</span>
|
||||
</div>
|
||||
<div>
|
||||
@Html.EditorFor(m => m.UsersAreModerated)
|
||||
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.UsersAreModerated)">@T("Users must be approved before they can log in")</label>
|
||||
</div>
|
||||
<div data-controllerid="@Html.FieldIdFor(m => m.UsersAreModerated)">
|
||||
<input type="checkbox" value="true" class="check-box" id="@Html.FieldIdFor(m => m.NotifyModeration)" name="@Html.FieldNameFor(m => m.NotifyModeration)" @(Model.NotifyModeration ? "checked=\"checked\"" : "") @(emailEnabled ? "" : "disabled=\"disabled\"") />
|
||||
<input name="@Html.FieldNameFor(m => m.NotifyModeration)" type="hidden" value="false">
|
||||
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.NotifyModeration)">@T("Send a notification when a user needs moderation")</label>
|
||||
|
||||
<label for="@Html.FieldIdFor(m => m.ValidateEmailContactEMail)">@T("Contact Us E-Mail address")</label>
|
||||
@Html.TextBoxFor(m => m.ValidateEmailContactEMail, new { @class = "text medium" } )
|
||||
@Html.ValidationMessage("ValidateEmailContactEMail", "*")
|
||||
<span class="hint">@T("The e-mail address displayed in the verification e-mail for a Contact Us link. Leave empty for no link.")</span>
|
||||
</div>
|
||||
<div>
|
||||
@Html.EditorFor(m => m.UsersAreModerated)
|
||||
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.UsersAreModerated)">@T("Users must be approved before they can log in")</label>
|
||||
</div>
|
||||
<div data-controllerid="@Html.FieldIdFor(m => m.UsersAreModerated)">
|
||||
<input type="checkbox" value="true" class="check-box" id="@Html.FieldIdFor(m => m.NotifyModeration)" name="@Html.FieldNameFor(m => m.NotifyModeration)" @(Model.NotifyModeration ? "checked=\"checked\"" : "") @(emailEnabled ? "" : "disabled=\"disabled\"")/>
|
||||
<input name="@Html.FieldNameFor(m => m.NotifyModeration)" type="hidden" value="false">
|
||||
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.NotifyModeration)">@T("Send a notification when a user needs moderation")</label>
|
||||
@if (!emailEnabled) {
|
||||
<div class="message message-Warning">@T("This option is available when an email module is activated.")</div>
|
||||
}
|
||||
|
||||
@if(!emailEnabled) {
|
||||
<div class="message message-Warning">@T("This option is available when an email module is activated.")</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
<div data-controllerid="@Html.FieldIdFor(m => m.NotifyModeration)">
|
||||
<label for="@Html.FieldIdFor(m => m.NotificationsRecipients)">@T("Moderators")</label>
|
||||
@Html.TextBoxFor(m => m.NotificationsRecipients, new { @class = "text medium" } )
|
||||
@Html.ValidationMessage("NotificationsRecipients", "*")
|
||||
<span class="hint">@T("The usernames to send the notifications to (e.g., \"admin, user1, ...\").")</span>
|
||||
</div>
|
||||
</div>
|
||||
<div data-controllerid="@Html.FieldIdFor(m => m.NotifyModeration)">
|
||||
<label for="@Html.FieldIdFor(m => m.NotificationsRecipients)">@T("Moderators")</label>
|
||||
@Html.TextBoxFor(m => m.NotificationsRecipients, new { @class = "text medium" })
|
||||
@Html.ValidationMessage("NotificationsRecipients", "*")
|
||||
<span class="hint">@T("The usernames to send the notifications to (e.g., \"admin, user1, ...\").")</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -0,0 +1,33 @@
|
||||
@using Orchard.Localization.Models;
|
||||
@using Orchard.Localization.Services;
|
||||
@model Orchard.Users.Models.UserSuspensionSettingsPart
|
||||
@{
|
||||
var _dateLocalizationServices = WorkContext.Resolve<IDateLocalizationServices>();
|
||||
var _dateTimeLocalization = WorkContext.Resolve<IDateTimeFormatProvider>();
|
||||
var lastSweepDateString = _dateLocalizationServices
|
||||
.ConvertToLocalizedString(
|
||||
Model.LastSweepUtc,
|
||||
_dateTimeLocalization.ShortDateTimeFormat,
|
||||
new DateLocalizationOptions() { NullText = T("Never").Text });
|
||||
}
|
||||
<fieldset>
|
||||
<legend>@T("Automated User Moderation")</legend>
|
||||
<div>
|
||||
@Html.EditorFor(m => m.SuspendInactiveUsers)
|
||||
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.SuspendInactiveUsers)">
|
||||
@T("Automatically suspend users after a period of inactivity.")
|
||||
</label>
|
||||
<div data-controllerid="@Html.FieldIdFor(m => m.SuspendInactiveUsers)" style="margin-left: 30px;">
|
||||
<div>
|
||||
<label for="@Html.FieldIdFor(m => m.AllowedInactivityDays)">@T("Allowed inactivity days. Users will be suspended only if this period is more than 0 days.")</label>
|
||||
@Html.TextBoxFor(m => m.AllowedInactivityDays, new { @class = "text medium", @Value = Model.AllowedInactivityDays })
|
||||
@Html.ValidationMessage("AllowedInactivityDays", "*")
|
||||
</div>
|
||||
<div>
|
||||
<label for="@Html.FieldIdFor(m => m.MinimumSweepInterval)">@T("Minimum time in hours between runs checking for users to suspend. (Last time checked: {0})", lastSweepDateString)</label>
|
||||
@Html.TextBoxFor(m => m.MinimumSweepInterval, new { @class = "text medium", @Value = Model.MinimumSweepInterval })
|
||||
@Html.ValidationMessage("MinimumSweepInterval", "*")
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -210,11 +210,16 @@
|
||||
<Compile Include="Localization\Services\ILocalizationStreamParser.cs" />
|
||||
<Compile Include="Localization\Services\LocalizationStreamParser.cs" />
|
||||
<Compile Include="Mvc\Html\LinkExtensions.cs" />
|
||||
<Compile Include="Security\IPasswordHistoryService.cs" />
|
||||
<Compile Include="Security\IPasswordService.cs" />
|
||||
<Compile Include="Security\ISecurityService.cs" />
|
||||
<Compile Include="Security\ISslSettingsProvider.cs" />
|
||||
<Compile Include="Security\IUserDataProvider.cs" />
|
||||
<Compile Include="Security\NullAccountValidationService.cs" />
|
||||
<Compile Include="Security\NullMembershipService.cs" />
|
||||
<Compile Include="Security\PasswordContext.cs" />
|
||||
<Compile Include="Security\PasswordExtensions.cs" />
|
||||
<Compile Include="Security\PasswordHistoryEntry.cs" />
|
||||
<Compile Include="Security\Providers\BaseUserDataProvider.cs" />
|
||||
<Compile Include="Security\Providers\DefaultSecurityService.cs" />
|
||||
<Compile Include="Security\Providers\DefaultSslSettingsProvider.cs" />
|
||||
|
||||
@@ -7,14 +7,16 @@ namespace Orchard.Security {
|
||||
private readonly string _passwordQuestion;
|
||||
private readonly string _passwordAnswer;
|
||||
private readonly bool _isApproved;
|
||||
private readonly bool _forcePasswordChange;
|
||||
|
||||
public CreateUserParams(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved) {
|
||||
public CreateUserParams(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, bool forcePasswordChange) {
|
||||
_username = username;
|
||||
_password = password;
|
||||
_email = email;
|
||||
_passwordQuestion = passwordQuestion;
|
||||
_passwordAnswer = passwordAnswer;
|
||||
_isApproved = isApproved;
|
||||
_forcePasswordChange = forcePasswordChange;
|
||||
}
|
||||
|
||||
public string Username {
|
||||
@@ -40,5 +42,9 @@ namespace Orchard.Security {
|
||||
public bool IsApproved {
|
||||
get { return _isApproved; }
|
||||
}
|
||||
|
||||
public bool ForcePasswordChange {
|
||||
get { return _forcePasswordChange; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,5 +19,7 @@ namespace Orchard.Security {
|
||||
bool EnablePasswordExpiration { get; set; }
|
||||
int PasswordExpirationTimeInDays { get; set; }
|
||||
MembershipPasswordFormat PasswordFormat { get; set; }
|
||||
bool EnablePasswordHistoryPolicy { get; set; }
|
||||
int PasswordReuseLimit { get; set; }
|
||||
}
|
||||
}
|
||||
14
src/Orchard/Security/IPasswordHistoryService.cs
Normal file
14
src/Orchard/Security/IPasswordHistoryService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Orchard.Security;
|
||||
|
||||
namespace Orchard.Security {
|
||||
public interface IPasswordHistoryService : IDependency {
|
||||
void CreateEntry(PasswordHistoryEntry context);
|
||||
IEnumerable<PasswordHistoryEntry> GetLastPasswords(IUser user, int count);
|
||||
bool PasswordMatchLastOnes(string Password, IUser user, int count);
|
||||
}
|
||||
}
|
||||
5
src/Orchard/Security/IPasswordService.cs
Normal file
5
src/Orchard/Security/IPasswordService.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace Orchard.Security {
|
||||
public interface IPasswordService : IDependency {
|
||||
bool IsMatch(PasswordContext context, string plaintextPassword);
|
||||
}
|
||||
}
|
||||
13
src/Orchard/Security/PasswordContext.cs
Normal file
13
src/Orchard/Security/PasswordContext.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Web;
|
||||
using System.Web.Security;
|
||||
|
||||
namespace Orchard.Security {
|
||||
public class PasswordContext {
|
||||
public string Password { get; set; }
|
||||
public string PasswordSalt { get; set; }
|
||||
public string HashAlgorithm { get; set; }
|
||||
public MembershipPasswordFormat PasswordFormat { get; set; }
|
||||
}
|
||||
}
|
||||
35
src/Orchard/Security/PasswordExtensions.cs
Normal file
35
src/Orchard/Security/PasswordExtensions.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web.Helpers;
|
||||
|
||||
namespace Orchard.Security {
|
||||
public static class PasswordExtensions {
|
||||
|
||||
public const string PBKDF2 = "PBKDF2";
|
||||
|
||||
public static string ComputeHashBase64(string hashAlgorithmName, byte[] saltBytes, string password) {
|
||||
var combinedBytes = CombineSaltAndPassword(saltBytes, password);
|
||||
|
||||
// Extending HashAlgorithm would be too complicated: http://stackoverflow.com/questions/6460711/adding-a-custom-hashalgorithmtype-in-c-sharp-asp-net?lq=1
|
||||
if (hashAlgorithmName == PBKDF2) {
|
||||
// HashPassword() already returns a base64 string.
|
||||
return Crypto.HashPassword(Encoding.Unicode.GetString(combinedBytes));
|
||||
}
|
||||
else {
|
||||
using (var hashAlgorithm = HashAlgorithm.Create(hashAlgorithmName)) {
|
||||
return Convert.ToBase64String(hashAlgorithm.ComputeHash(combinedBytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] CombineSaltAndPassword(byte[] saltBytes, string password) {
|
||||
var passwordBytes = Encoding.Unicode.GetBytes(password);
|
||||
return saltBytes.Concat(passwordBytes).ToArray();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
8
src/Orchard/Security/PasswordHistoryEntry.cs
Normal file
8
src/Orchard/Security/PasswordHistoryEntry.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using System;
|
||||
|
||||
namespace Orchard.Security {
|
||||
public class PasswordHistoryEntry : PasswordContext {
|
||||
public IUser User { get; set; }
|
||||
public DateTime? LastPasswordChangeUtc { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user