diff --git a/Areas/admin/Controllers/usersController.cs b/Areas/admin/Controllers/usersController.cs index f07bb10..69cde78 100644 --- a/Areas/admin/Controllers/usersController.cs +++ b/Areas/admin/Controllers/usersController.cs @@ -30,5 +30,19 @@ public ActionResult unapprove(Guid id) provider.ChangeApproval(id, false); return RedirectToAction("Index"); } + + public ActionResult @lock(Guid id) + { + var provider = (Membership.Provider as RedisMembershipProvider); + provider.ChangeLockStatus(id, true); + return RedirectToAction("Index"); + } + + public ActionResult unlock(Guid id) + { + var provider = (Membership.Provider as RedisMembershipProvider); + provider.ChangeLockStatus(id, false); + return RedirectToAction("Index"); + } } } \ No newline at end of file diff --git a/Areas/admin/Views/users/index.cshtml b/Areas/admin/Views/users/index.cshtml index dcb2a53..ddcc3fa 100644 --- a/Areas/admin/Views/users/index.cshtml +++ b/Areas/admin/Views/users/index.cshtml @@ -17,6 +17,10 @@ Last Login Time + + Last Lockout Time + + @@ -32,16 +36,29 @@ @Html.DisplayFor(modelItem => mu.LastLoginDate) + + @Html.DisplayFor(modelItem => mu.LastLockoutDate) + @if (mu.IsApproved) { @Html.ActionLink("Unapprove", "unapprove", new { id = mu.ProviderUserKey }, new { @class = "btn btn-danger", style = "width:90px;" }) - } + } else { @Html.ActionLink("Approve", "approve", new { id = mu.ProviderUserKey }, new { @class = "btn btn-success", style = "width:90px;" }) } + + @if (!mu.IsLockedOut) + { + @Html.ActionLink("Lock", "lock", new { id = mu.ProviderUserKey }, new { @class = "btn btn-danger", style = "width:90px;" }) + } + else + { + @Html.ActionLink("Unlock", "unlock", new { id = mu.ProviderUserKey }, new { @class = "btn btn-success", style = "width:90px;" }) + } + } diff --git a/Auth/RedisMembershipProvider.cs b/Auth/RedisMembershipProvider.cs index 47f3073..e9181dd 100644 --- a/Auth/RedisMembershipProvider.cs +++ b/Auth/RedisMembershipProvider.cs @@ -1,11 +1,10 @@ using System; -using System.Collections.Generic; +using System.Collections.Specialized; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Security.Cryptography; using System.Text; -using System.Web; using System.Web.Security; using NServiceKit.DataAnnotations; using NServiceKit.DesignPatterns.Model; @@ -16,6 +15,13 @@ namespace BuildFeed.Auth { public class RedisMembershipProvider : MembershipProvider { + private bool _enablePasswordReset = true; + private int _maxInvalidPasswordAttempts = 5; + private int _minRequiredNonAlphanumericCharacters = 1; + private int _minRequriedPasswordLength = 12; + private int _passwordAttemptWindow = 60; + private bool _requiresUniqueEmail = true; + public override string ApplicationName { get { return ""; } @@ -24,7 +30,7 @@ public override string ApplicationName public override bool EnablePasswordReset { - get { return true; } + get { return _enablePasswordReset; } } public override bool EnablePasswordRetrieval @@ -34,22 +40,22 @@ public override bool EnablePasswordRetrieval public override int MaxInvalidPasswordAttempts { - get { return 5; } + get { return _maxInvalidPasswordAttempts; } } public override int MinRequiredNonAlphanumericCharacters { - get { return 1; } + get { return _minRequiredNonAlphanumericCharacters; } } public override int MinRequiredPasswordLength { - get { return 12; } + get { return _minRequriedPasswordLength; } } public override int PasswordAttemptWindow { - get { return 60; } + get { return _passwordAttemptWindow; } } public override MembershipPasswordFormat PasswordFormat @@ -69,7 +75,24 @@ public override bool RequiresQuestionAndAnswer public override bool RequiresUniqueEmail { - get { return true; } + get { return _requiresUniqueEmail; } + } + + public override void Initialize(string name, NameValueCollection config) + { + if (config == null) + { + throw new ArgumentNullException("config"); + } + + base.Initialize(name, config); + + _enablePasswordReset = tryReadBool(config["enablePasswordReset"], _enablePasswordReset); + _maxInvalidPasswordAttempts = tryReadInt(config["maxInvalidPasswordAttempts"], _maxInvalidPasswordAttempts); + _minRequiredNonAlphanumericCharacters = tryReadInt(config["minRequiredNonAlphanumericCharacters"], _minRequiredNonAlphanumericCharacters); + _minRequriedPasswordLength = tryReadInt(config["minRequriedPasswordLength"], _minRequriedPasswordLength); + _passwordAttemptWindow = tryReadInt(config["passwordAttemptWindow"], _passwordAttemptWindow); + _requiresUniqueEmail = tryReadBool(config["requiresUniqueEmail"], _requiresUniqueEmail); } public override bool ChangePassword(string username, string oldPassword, string newPassword) @@ -313,6 +336,32 @@ public void ChangeApproval(Guid Id, bool newStatus) } } + public void ChangeLockStatus(Guid Id, bool newStatus) + { + using (RedisClient rClient = new RedisClient(DatabaseConfig.Host, DatabaseConfig.Port, db: DatabaseConfig.Database)) + { + var client = rClient.As(); + var rm = client.GetById(Id); + + if (rm != null) + { + rm.IsLockedOut = newStatus; + + if (newStatus) + { + rm.LastLockoutDate = DateTime.Now; + } + else + { + rm.LockoutWindowAttempts = 0; + rm.LockoutWindowStart = DateTime.MinValue; + } + + client.Store(rm); + } + } + } + public override bool UnlockUser(string userName) { using (RedisClient rClient = new RedisClient(DatabaseConfig.Host, DatabaseConfig.Port, db: DatabaseConfig.Database)) @@ -340,7 +389,7 @@ public override bool ValidateUser(string username, string password) var client = rClient.As(); var rm = client.GetAll().SingleOrDefault(m => m.UserName.ToLower() == username.ToLower()); - if (rm == null || !rm.IsApproved) + if (rm == null || !(rm.IsApproved && !rm.IsLockedOut)) { return false; } @@ -355,15 +404,57 @@ public override bool ValidateUser(string username, string password) isFail |= (hash[i] != rm.PassHash[i]); } - if(!isFail) + if (isFail) + { + if (rm.LockoutWindowStart == DateTime.MinValue) + { + rm.LockoutWindowStart = DateTime.Now; + rm.LockoutWindowAttempts = 1; + } + else + { + if (rm.LockoutWindowStart.AddMinutes(PasswordAttemptWindow) > DateTime.Now) + { + // still within window + rm.LockoutWindowAttempts++; + if (rm.LockoutWindowAttempts >= MaxInvalidPasswordAttempts) + { + rm.IsLockedOut = true; + } + } + else + { + // outside of window, reset + rm.LockoutWindowStart = DateTime.Now; + rm.LockoutWindowAttempts = 1; + } + } + } + else { rm.LastLoginDate = DateTime.Now; - client.Store(rm); + rm.LockoutWindowStart = DateTime.MinValue; + rm.LockoutWindowAttempts = 0; } + client.Store(rm); return !isFail; } } + + private static bool tryReadBool(string config, bool defaultValue) + { + bool temp = false; + bool success = bool.TryParse(config, out temp); + return success ? temp : defaultValue; + } + + private static int tryReadInt(string config, int defaultValue) + { + int temp = 0; + bool success = int.TryParse(config, out temp); + return success ? temp : defaultValue; + } } [DataObject] @@ -398,5 +489,8 @@ public class RedisMember : IHasId public DateTime LastActivityDate { get; set; } public DateTime LastLockoutDate { get; set; } public DateTime LastLoginDate { get; set; } + + public DateTime LockoutWindowStart { get; set; } + public int LockoutWindowAttempts { get; set; } } } \ No newline at end of file diff --git a/Controllers/supportController.cs b/Controllers/supportController.cs index 589917d..084fe64 100644 --- a/Controllers/supportController.cs +++ b/Controllers/supportController.cs @@ -33,7 +33,13 @@ public ActionResult login(LoginUser ru) if (isAuthenticated) { - FormsAuthentication.SetAuthCookie(ru.UserName, ru.RememberMe); + int expiryLength = ru.RememberMe ? 129600 : 60; + var ticket = new FormsAuthenticationTicket(ru.UserName, true, expiryLength); + var encryptedTicket = FormsAuthentication.Encrypt(ticket); + var cookieTicket = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket); + cookieTicket.Expires = DateTime.Now.AddMinutes(expiryLength); + cookieTicket.Path = FormsAuthentication.FormsCookiePath; + Response.Cookies.Add(cookieTicket); string returnUrl = string.IsNullOrEmpty(Request.QueryString["ReturnUrl"]) ? "/" : Request.QueryString["ReturnUrl"]; diff --git a/Views/build/info.cshtml b/Views/build/info.cshtml index e22065e..a99e7c7 100644 --- a/Views/build/info.cshtml +++ b/Views/build/info.cshtml @@ -18,6 +18,22 @@

@Model.FullBuildString

+ @if (User.Identity.IsAuthenticated) + { +
+ +
+

+ @Html.ActionLink("Edit", "edit", new { id = Model.Id }, new { @class = "btn btn-default btn-xs" }) + + @if (User.Identity.Name == "hounsell") + { + @Html.ActionLink("Delete", "delete", new { id = Model.Id }, new { @class = "btn btn-danger btn-xs" }) + } +

+
+
+ }
@Html.LabelFor(model => model.MajorVersion, new { @class = "control-label col-sm-2" })
@@ -154,14 +170,6 @@
Return to Listing - @if (User.Identity.IsAuthenticated) - { - @Html.ActionLink("Edit", "edit", new { id = Model.Id }, new { @class = "btn btn-default" }) - } - @if (User.Identity.Name == "hounsell") - { - @Html.ActionLink("Delete", "delete", new { id = Model.Id }, new { @class = "btn btn-danger" }) - }
diff --git a/Web.Release.config b/Web.Release.config index ed47226..3d785dd 100644 --- a/Web.Release.config +++ b/Web.Release.config @@ -29,6 +29,10 @@ --> + + + + diff --git a/Web.config b/Web.config index 2dc02a6..0b27c5e 100644 --- a/Web.config +++ b/Web.config @@ -71,8 +71,6 @@ - -