diff --git a/Piranha.sln b/Piranha.sln index 3234fdc2d..5ffc339b4 100644 --- a/Piranha.sln +++ b/Piranha.sln @@ -77,6 +77,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "identity", "identity", "{9C EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Piranha.Manager.LocalAuth", "core\Piranha.Manager.LocalAuth\Piranha.Manager.LocalAuth.csproj", "{A88D6D8D-725E-494F-91CD-440D87BFF21B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Piranha.AspNetCore.Identity.EntityFrameworkCore", "identity\Piranha.AspNetCore.Identity.EntityFrameworkCore\Piranha.AspNetCore.Identity.EntityFrameworkCore.csproj", "{52E9EC4E-1E63-47E1-92EC-EE6BB09AEFFC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/examples/MvcWeb/Startup.cs b/examples/MvcWeb/Startup.cs index b01f12750..07df53d32 100644 --- a/examples/MvcWeb/Startup.cs +++ b/examples/MvcWeb/Startup.cs @@ -14,6 +14,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Piranha; +using Piranha.AspNetCore.Identity; +using Piranha.AspNetCore.Identity.EntityFrameworkCore; using Piranha.Data.EF.SQLite; using Piranha.AspNetCore.Identity.SQLite; using Piranha.AttributeBuilder; @@ -39,8 +41,9 @@ public void ConfigureServices(IServiceCollection services) options.UseEF(db => db.UseSqlite("Filename=./piranha.mvcweb.db")); - options.UseIdentityWithSeed(db => + options.UseIdentityEF(db => db.UseSqlite("Filename=./piranha.mvcweb.db")); + options.UseIdentityWithSeed(); options.UseSecurity(o => { diff --git a/examples/RazorWeb/RazorWeb.csproj b/examples/RazorWeb/RazorWeb.csproj index 2b1e4bee9..bae24229b 100644 --- a/examples/RazorWeb/RazorWeb.csproj +++ b/examples/RazorWeb/RazorWeb.csproj @@ -17,7 +17,6 @@ - diff --git a/examples/RazorWeb/Startup.cs b/examples/RazorWeb/Startup.cs index 1fb19cf25..8e013ce3a 100644 --- a/examples/RazorWeb/Startup.cs +++ b/examples/RazorWeb/Startup.cs @@ -14,6 +14,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Piranha; +using Piranha.AspNetCore.Identity; +using Piranha.AspNetCore.Identity.EntityFrameworkCore; using Piranha.Data.EF.SQLite; using Piranha.AspNetCore.Identity.SQLite; using Piranha.AttributeBuilder; @@ -39,8 +41,8 @@ public void ConfigureServices(IServiceCollection services) options.UseEF(db => db.UseSqlite("Filename=./piranha.razorweb.db")); - options.UseIdentityWithSeed(db => - db.UseSqlite("Filename=./piranha.razorweb.db")); + options.UseIdentityEF(db => + db.UseSqlite("Filename=./piranha.mvcweb.db")); options.UseSecurity(o => { diff --git a/identity/Piranha.AspNetCore.Identity/Db.cs b/identity/Piranha.AspNetCore.Identity.EntityFrameworkCore/Db.cs similarity index 98% rename from identity/Piranha.AspNetCore.Identity/Db.cs rename to identity/Piranha.AspNetCore.Identity.EntityFrameworkCore/Db.cs index 5ae2ba9e7..9613bb5a1 100644 --- a/identity/Piranha.AspNetCore.Identity/Db.cs +++ b/identity/Piranha.AspNetCore.Identity.EntityFrameworkCore/Db.cs @@ -15,7 +15,7 @@ using Microsoft.EntityFrameworkCore; using Piranha.AspNetCore.Identity.Data; -namespace Piranha.AspNetCore.Identity +namespace Piranha.AspNetCore.Identity.EntityFrameworkCore { public abstract class Db : IdentityDbContext + /// Adds the Piranha identity module. + /// + /// The current service collection + /// Options for configuring the database + /// The services + public static IServiceCollection AddPiranhaIdentityEF(this IServiceCollection services, Action dbOptions) where T : DbContext, IDb + { + services.AddDbContext(dbOptions); + services.AddScoped(); + services.AddScoped(); + services.AddIdentity() + .AddEntityFrameworkStores(); + + return services; + } + } +} \ No newline at end of file diff --git a/identity/Piranha.AspNetCore.Identity.EntityFrameworkCore/IdentityStartupExtensions.cs b/identity/Piranha.AspNetCore.Identity.EntityFrameworkCore/IdentityStartupExtensions.cs new file mode 100644 index 000000000..edac2d5a5 --- /dev/null +++ b/identity/Piranha.AspNetCore.Identity.EntityFrameworkCore/IdentityStartupExtensions.cs @@ -0,0 +1,33 @@ +/* + * Copyright (c) .NET Foundation and Contributors + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * http://github.com/piranhacms/piranha.core + * + */ + +using System; +using Microsoft.EntityFrameworkCore; + +namespace Piranha.AspNetCore.Identity.EntityFrameworkCore +{ + public static class IdentityStartupExtensions + { + /// + /// Extensions method for simplified setup. + /// + /// The current service builder + /// The db options + /// The DbContext type + /// The builder + public static PiranhaServiceBuilder UseIdentityEF(this PiranhaServiceBuilder serviceBuilder, Action dbOptions) + where T : Db + { + serviceBuilder.Services.AddPiranhaIdentityEF(dbOptions); + + return serviceBuilder; + } + } +} \ No newline at end of file diff --git a/identity/Piranha.AspNetCore.Identity.EntityFrameworkCore/Piranha.AspNetCore.Identity.EntityFrameworkCore.csproj b/identity/Piranha.AspNetCore.Identity.EntityFrameworkCore/Piranha.AspNetCore.Identity.EntityFrameworkCore.csproj new file mode 100644 index 000000000..fd60432a1 --- /dev/null +++ b/identity/Piranha.AspNetCore.Identity.EntityFrameworkCore/Piranha.AspNetCore.Identity.EntityFrameworkCore.csproj @@ -0,0 +1,21 @@ + + + + net5.0 + $(NoWarn);1591 + true + Piranha CMS security provider for AspNetCore Identity + + + + true + + + + + + + + + + diff --git a/identity/Piranha.AspNetCore.Identity.MySQL/IdentityMySQLDb.cs b/identity/Piranha.AspNetCore.Identity.MySQL/IdentityMySQLDb.cs index 8a221db70..90720c23f 100644 --- a/identity/Piranha.AspNetCore.Identity.MySQL/IdentityMySQLDb.cs +++ b/identity/Piranha.AspNetCore.Identity.MySQL/IdentityMySQLDb.cs @@ -12,7 +12,7 @@ namespace Piranha.AspNetCore.Identity.MySQL { - public class IdentityMySQLDb : Identity.Db + public class IdentityMySQLDb : EntityFrameworkCore.Db { /// /// Default constructor. diff --git a/identity/Piranha.AspNetCore.Identity.MySQL/Piranha.AspNetCore.Identity.MySQL.csproj b/identity/Piranha.AspNetCore.Identity.MySQL/Piranha.AspNetCore.Identity.MySQL.csproj index 87a31d5cb..0fdab89bd 100644 --- a/identity/Piranha.AspNetCore.Identity.MySQL/Piranha.AspNetCore.Identity.MySQL.csproj +++ b/identity/Piranha.AspNetCore.Identity.MySQL/Piranha.AspNetCore.Identity.MySQL.csproj @@ -11,10 +11,9 @@ - - + \ No newline at end of file diff --git a/identity/Piranha.AspNetCore.Identity.PostgreSQL/IdentityPostgreSQLDb.cs b/identity/Piranha.AspNetCore.Identity.PostgreSQL/IdentityPostgreSQLDb.cs index 62da245e9..747a3d1c5 100644 --- a/identity/Piranha.AspNetCore.Identity.PostgreSQL/IdentityPostgreSQLDb.cs +++ b/identity/Piranha.AspNetCore.Identity.PostgreSQL/IdentityPostgreSQLDb.cs @@ -12,7 +12,7 @@ namespace Piranha.AspNetCore.Identity.PostgreSQL { - public class IdentityPostgreSQLDb : Db + public class IdentityPostgreSQLDb : EntityFrameworkCore.Db { /// /// Default constructor. diff --git a/identity/Piranha.AspNetCore.Identity.PostgreSQL/Piranha.AspNetCore.Identity.PostgreSQL.csproj b/identity/Piranha.AspNetCore.Identity.PostgreSQL/Piranha.AspNetCore.Identity.PostgreSQL.csproj index 84b3ad0de..505442c03 100644 --- a/identity/Piranha.AspNetCore.Identity.PostgreSQL/Piranha.AspNetCore.Identity.PostgreSQL.csproj +++ b/identity/Piranha.AspNetCore.Identity.PostgreSQL/Piranha.AspNetCore.Identity.PostgreSQL.csproj @@ -11,10 +11,12 @@ - - + + + + diff --git a/identity/Piranha.AspNetCore.Identity.SQLite/IdentitySQLiteDb.cs b/identity/Piranha.AspNetCore.Identity.SQLite/IdentitySQLiteDb.cs index cbc447f98..11b5220c2 100644 --- a/identity/Piranha.AspNetCore.Identity.SQLite/IdentitySQLiteDb.cs +++ b/identity/Piranha.AspNetCore.Identity.SQLite/IdentitySQLiteDb.cs @@ -12,7 +12,7 @@ namespace Piranha.AspNetCore.Identity.SQLite { - public class IdentitySQLiteDb : Db + public class IdentitySQLiteDb : EntityFrameworkCore.Db { /// /// Default constructor. diff --git a/identity/Piranha.AspNetCore.Identity.SQLite/Piranha.AspNetCore.Identity.SQLite.csproj b/identity/Piranha.AspNetCore.Identity.SQLite/Piranha.AspNetCore.Identity.SQLite.csproj index 4eaac8e78..124c94f41 100644 --- a/identity/Piranha.AspNetCore.Identity.SQLite/Piranha.AspNetCore.Identity.SQLite.csproj +++ b/identity/Piranha.AspNetCore.Identity.SQLite/Piranha.AspNetCore.Identity.SQLite.csproj @@ -11,10 +11,9 @@ - - + diff --git a/identity/Piranha.AspNetCore.Identity/Controllers/RoleController.cs b/identity/Piranha.AspNetCore.Identity/Controllers/RoleController.cs index dc621c9b7..09867429f 100644 --- a/identity/Piranha.AspNetCore.Identity/Controllers/RoleController.cs +++ b/identity/Piranha.AspNetCore.Identity/Controllers/RoleController.cs @@ -9,9 +9,14 @@ */ using System; +using System.Collections.Generic; using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Piranha.AspNetCore.Identity.Data; using Piranha.AspNetCore.Identity.Models; using Piranha.Manager.Controllers; @@ -20,27 +25,62 @@ namespace Piranha.AspNetCore.Identity.Controllers [Area("Manager")] public class RoleController : ManagerController { - private readonly IDb _db; + private readonly UserManager _userManager; + private readonly RoleManager _roleManager; - public RoleController(IDb db) + /// + /// Default constructor. + /// + /// The current user manager + /// The current role manager + public RoleController(UserManager userManager, RoleManager roleManager) { - _db = db; + _userManager = userManager; + _roleManager = roleManager; } - + [HttpGet] [Route("/manager/roles")] [Authorize(Policy = Permissions.Roles)] - public IActionResult List() + public async Task List() { - return View(RoleListModel.Get(_db)); + var model = new RoleListModel + { + Roles = _roleManager.Roles + .OrderBy(r => r.Name) + .Select(r => new RoleListModel.ListItem + { + Id = r.Id, + Name = r.Name + }).ToList() + }; + + foreach (var role in model.Roles) + { + role.UserCount = (await _userManager.GetUsersInRoleAsync(role.Id.ToString())).Count; + } + + return View(model); } [HttpGet] [Route("/manager/role/{id:Guid}")] [Authorize(Policy = Permissions.RolesEdit)] - public IActionResult Edit(Guid id) + public async Task Edit(Guid id) { - return View("Edit", RoleEditModel.GetById(_db, id)); + var role = await _roleManager.FindByIdAsync(id.ToString()); + + if (role == null) + { + return View("Edit", null); + } + + var model = new RoleEditModel + { + Role = role, + SelectedClaims = (await _roleManager.GetClaimsAsync(role)).Select(claim => claim.Type).ToList() + }; + return View("Edit", model); } [HttpGet] @@ -48,18 +88,49 @@ public IActionResult Edit(Guid id) [Authorize(Policy = Permissions.RolesAdd)] public IActionResult Add() { - return View("Edit", RoleEditModel.Create()); + return View("Edit", new RoleEditModel() {Role = new Role()}); } [HttpPost] [Route("/manager/role/save")] [Authorize(Policy = Permissions.RolesSave)] - public IActionResult Save(RoleEditModel model) + public async Task Save(RoleEditModel model) { - if (model.Save(_db)) + var role = await _roleManager.FindByIdAsync(model.Role.Id.ToString()); + + if (role == null) { - SuccessMessage("The role has been saved."); - return RedirectToAction("Edit", new {id = model.Role.Id}); + await _roleManager.CreateAsync(model.Role); + } + else + { + role.Name = model.Role.Name; + + var currentClaims = await _roleManager.GetClaimsAsync(role); + + foreach (var old in currentClaims) + { + if (model.SelectedClaims.Contains(old.Type) == false) + { + await _roleManager.RemoveClaimAsync(role, old); + } + } + + foreach (var selected in model.SelectedClaims) + { + if (currentClaims.All(c => c.Type != selected)) + { + await _roleManager.AddClaimAsync(role, new Claim(selected, selected)); + } + } + + var result = await _roleManager.UpdateAsync(role); + + if (result.Succeeded) + { + SuccessMessage("The role has been saved."); + return RedirectToAction("Edit", new {id = model.Role.Id}); + } } ErrorMessage("The role could not be saved.", false); @@ -69,22 +140,21 @@ public IActionResult Save(RoleEditModel model) [HttpGet] [Route("/manager/role/delete")] [Authorize(Policy = Permissions.RolesDelete)] - public IActionResult Delete(Guid id) + public async Task Delete(Guid id) { - var role = _db.Roles - .FirstOrDefault(r => r.Id == id); + var role = await _roleManager.FindByIdAsync(id.ToString()); + var result = await _roleManager.DeleteAsync(role); - if (role != null) + if (result.Succeeded) { - _db.Roles.Remove(role); - _db.SaveChanges(); - SuccessMessage("The role has been deleted."); - return RedirectToAction("List"); + } + else + { + ErrorMessage("The role could not be deleted.", false); } - ErrorMessage("The role could not be deleted.", false); return RedirectToAction("List"); } } -} \ No newline at end of file +} diff --git a/identity/Piranha.AspNetCore.Identity/Controllers/UserController.cs b/identity/Piranha.AspNetCore.Identity/Controllers/UserController.cs index d01b997d3..ce9d69ce2 100644 --- a/identity/Piranha.AspNetCore.Identity/Controllers/UserController.cs +++ b/identity/Piranha.AspNetCore.Identity/Controllers/UserController.cs @@ -11,11 +11,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using Piranha.AspNetCore.Identity.Data; using Piranha.AspNetCore.Identity.Models; using Piranha.Manager; @@ -30,21 +30,21 @@ namespace Piranha.AspNetCore.Identity.Controllers [Area("Manager")] public class UserController : ManagerController { - private readonly IDb _db; private readonly UserManager _userManager; + private readonly RoleManager _roleManager; private readonly ManagerLocalizer _localizer; /// /// Default constructor. /// - /// The current db context /// The current user manager + /// The current role manager /// The manager localizer - public UserController(IDb db, UserManager userManager, ManagerLocalizer localizer) + public UserController(UserManager userManager, RoleManager roleManager, ManagerLocalizer localizer) { - _db = db; - _userManager = userManager; _localizer = localizer; + _userManager = userManager; + _roleManager = roleManager; } /// @@ -64,12 +64,40 @@ public IActionResult List() [HttpGet] [Route("/manager/users/list")] [Authorize(Policy = Permissions.Users)] - public UserListModel Get() + public async Task Get() { - return UserListModel.Get(_db); + var userListModel = new UserListModel(); + + foreach (var u in await GetUsers()) + { + userListModel.Users.Add(new UserListModel.ListItem() + { + Id = u.Id, + UserName = u.UserName, + Email = u.Email, + GravatarUrl = !string.IsNullOrWhiteSpace(u.Email) ? Utils.GetGravatarUrl(u.Email, 25) : null, + Roles = await _userManager.GetRolesAsync(u) + }); + } + + return userListModel; + + async Task> GetUsers() + { + if (_userManager.SupportsQueryableUsers) + { + return _userManager.Users.ToList(); + } + if (_userManager.SupportsUserRole) + { + return (await _userManager.GetUsersInRoleAsync(default)).ToList(); + } + + throw new NotSupportedException($""); + } } - /// + /// /// Gets the edit view for an existing user. /// /// The user id @@ -89,10 +117,20 @@ public IActionResult Edit(Guid id) [HttpGet] [Route("/manager/user/edit/{id:Guid}")] [Authorize(Policy = Permissions.UsersEdit)] - public UserEditModel Get(Guid id) + public async Task Get(Guid id) { - return UserEditModel.GetById(_db, id); - //return View(UserEditModel.GetById(_db, id)); + var user = await _userManager.FindByIdAsync(id.ToString()); + + if (user == null) + { + return null; + } + return new UserEditModel + { + User = user, + SelectedRoles = await _userManager.GetRolesAsync(user), + Roles = _roleManager.Roles.OrderBy(r => r.Name).ToList() + }; } /// @@ -103,8 +141,11 @@ public UserEditModel Get(Guid id) [Authorize(Policy = Permissions.UsersEdit)] public UserEditModel Add() { - return UserEditModel.Create(_db); - //return View("Edit", UserEditModel.Create(_db)); + return new() + { + User = new User(), + Roles = _roleManager.Roles.OrderBy(r => r.Name).ToList() + }; } /// @@ -120,15 +161,13 @@ public async Task Save([FromBody] UserEditModel model) //var temp = UserEditModel.Create(_db); //model.Roles = temp.Roles; - if(model.User == null) + if (model.User == null) { return BadRequest(GetErrorMessage(_localizer.Security["The user could not be found."])); } try { - var userId = model.User.Id; - if (string.IsNullOrWhiteSpace(model.User.UserName)) { return BadRequest(GetErrorMessage(_localizer.General["Username is mandatory."])); @@ -165,18 +204,18 @@ public async Task Save([FromBody] UserEditModel model) } //check username - if (await _db.Users.CountAsync(u => u.UserName.ToLower().Trim() == model.User.UserName.ToLower().Trim() && u.Id != userId) > 0) + if (await _userManager.FindByNameAsync(model.User.UserName.ToLower().Trim()) is not null) { return BadRequest(GetErrorMessage(_localizer.Security["Username is used by another user."])); } //check email - if (await _db.Users.CountAsync(u => u.Email.ToLower().Trim() == model.User.Email.ToLower().Trim() && u.Id != userId) > 0) + if (await _userManager.FindByEmailAsync(model.User.Email.ToLower().Trim()) is not null) { return BadRequest(GetErrorMessage(_localizer.Security["Email address is used by another user."])); } - var result = await model.Save(_userManager); + var result = await _userManager.UpdateAsync(model.User); if (result.Succeeded) { return Ok(Get(model.User.Id)); @@ -202,19 +241,17 @@ public async Task Save([FromBody] UserEditModel model) [Authorize(Policy = Permissions.UsersDelete)] public async Task Delete(Guid id) { - var user = _db.Users.FirstOrDefault(u => u.Id == id); var currentUser = await _userManager.GetUserAsync(HttpContext.User); - if (currentUser != null && user.Id == currentUser.Id) + if (currentUser?.Id == id) { return BadRequest(GetErrorMessage(_localizer.Security["Can't delete yourself."])); } + var user = await _userManager.FindByIdAsync(id.ToString()); if (user != null) { - _db.Users.Remove(user); - _db.SaveChanges(); - + await _userManager.DeleteAsync(user); return Ok(GetSuccessMessage(_localizer.Security["The user has been deleted."])); } diff --git a/identity/Piranha.AspNetCore.Identity/DefaultIdentitySeed.cs b/identity/Piranha.AspNetCore.Identity/DefaultIdentitySeed.cs index b70118066..7462f7be0 100644 --- a/identity/Piranha.AspNetCore.Identity/DefaultIdentitySeed.cs +++ b/identity/Piranha.AspNetCore.Identity/DefaultIdentitySeed.cs @@ -21,11 +21,6 @@ namespace Piranha.AspNetCore.Identity /// public class DefaultIdentitySeed : IIdentitySeed { - /// - /// The private DbContext. - /// - private readonly IDb _db; - /// /// The private user manager. /// @@ -34,11 +29,9 @@ public class DefaultIdentitySeed : IIdentitySeed /// /// Default constructor. /// - /// The current DbContext /// The current UserManager - public DefaultIdentitySeed(IDb db, UserManager userManager) + public DefaultIdentitySeed(UserManager userManager) { - _db = db; _userManager = userManager; } @@ -47,13 +40,12 @@ public DefaultIdentitySeed(IDb db, UserManager userManager) /// public async Task CreateAsync() { - if (!_db.Users.Any()) + if (!_userManager.Users.Any()) { var user = new User { UserName = "admin", - Email = "admin@piranhacms.org", - SecurityStamp = Guid.NewGuid().ToString() + Email = "admin@piranhacms.org" }; var createResult = await _userManager.CreateAsync(user, "password"); diff --git a/identity/Piranha.AspNetCore.Identity/IdentityModuleExtensions.cs b/identity/Piranha.AspNetCore.Identity/IdentityModuleExtensions.cs index d0ab52d1d..c46788b63 100644 --- a/identity/Piranha.AspNetCore.Identity/IdentityModuleExtensions.cs +++ b/identity/Piranha.AspNetCore.Identity/IdentityModuleExtensions.cs @@ -12,216 +12,187 @@ using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; -using Piranha; -using Piranha.AspNetCore.Identity; using Piranha.AspNetCore.Identity.Data; using Piranha.Manager; using Piranha.Manager.LocalAuth; -using IDb = Piranha.AspNetCore.Identity.IDb; -using Module = Piranha.AspNetCore.Identity.Module; - -public static class IdentityModuleExtensions +namespace Piranha.AspNetCore.Identity { - /// - /// Adds the Piranha identity module. - /// - /// The current service collection - /// Options for configuring the database - /// Optional options for identity - /// Optional options for cookies - /// The services - public static IServiceCollection AddPiranhaIdentity(this IServiceCollection services, - Action dbOptions, - Action identityOptions = null, - Action cookieOptions = null) - where T : Db + public static class IdentityModuleExtensions { - services - .AddRazorPages() - .AddRazorPagesOptions(options => - { - options.Conventions.AllowAnonymousToAreaPage("Manager", "/login"); - }); - - // Add the identity module - App.Modules.Register(); - - // Setup authorization policies - services.AddAuthorization(o => + /// + /// Adds the Piranha identity module. + /// + /// The current service collection + /// Optional options for identity + /// Optional options for cookies + /// The services + public static IServiceCollection AddPiranhaIdentity(this IServiceCollection services, + Action identityOptions = null, + Action cookieOptions = null) { - // Role policies - o.AddPolicy(Permissions.Roles, policy => - { - policy.RequireClaim(Permission.Admin, Permission.Admin); - policy.RequireClaim(Permissions.Roles, Permissions.Roles); - }); - o.AddPolicy(Permissions.RolesAdd, policy => - { - policy.RequireClaim(Permission.Admin, Permission.Admin); - policy.RequireClaim(Permissions.Roles, Permissions.Roles); - policy.RequireClaim(Permissions.RolesAdd, Permissions.RolesAdd); - }); - o.AddPolicy(Permissions.RolesDelete, policy => - { - policy.RequireClaim(Permission.Admin, Permission.Admin); - policy.RequireClaim(Permissions.Roles, Permissions.Roles); - policy.RequireClaim(Permissions.RolesDelete, Permissions.RolesDelete); - }); - o.AddPolicy(Permissions.RolesEdit, policy => - { - policy.RequireClaim(Permission.Admin, Permission.Admin); - policy.RequireClaim(Permissions.Roles, Permissions.Roles); - policy.RequireClaim(Permissions.RolesEdit, Permissions.RolesEdit); - }); - o.AddPolicy(Permissions.RolesSave, policy => + services + .AddRazorPages() + .AddRazorPagesOptions(options => + { + options.Conventions.AllowAnonymousToAreaPage("Manager", "/login"); + }); + + // Add the identity module + App.Modules.Register(); + + // Setup authorization policies + services.AddAuthorization(o => { - policy.RequireClaim(Permission.Admin, Permission.Admin); - policy.RequireClaim(Permissions.Roles, Permissions.Roles); - policy.RequireClaim(Permissions.RolesSave, Permissions.RolesSave); + // Role policies + o.AddPolicy(Permissions.Roles, policy => + { + policy.RequireClaim(Permission.Admin, Permission.Admin); + policy.RequireClaim(Permissions.Roles, Permissions.Roles); + }); + o.AddPolicy(Permissions.RolesAdd, policy => + { + policy.RequireClaim(Permission.Admin, Permission.Admin); + policy.RequireClaim(Permissions.Roles, Permissions.Roles); + policy.RequireClaim(Permissions.RolesAdd, Permissions.RolesAdd); + }); + o.AddPolicy(Permissions.RolesDelete, policy => + { + policy.RequireClaim(Permission.Admin, Permission.Admin); + policy.RequireClaim(Permissions.Roles, Permissions.Roles); + policy.RequireClaim(Permissions.RolesDelete, Permissions.RolesDelete); + }); + o.AddPolicy(Permissions.RolesEdit, policy => + { + policy.RequireClaim(Permission.Admin, Permission.Admin); + policy.RequireClaim(Permissions.Roles, Permissions.Roles); + policy.RequireClaim(Permissions.RolesEdit, Permissions.RolesEdit); + }); + o.AddPolicy(Permissions.RolesSave, policy => + { + policy.RequireClaim(Permission.Admin, Permission.Admin); + policy.RequireClaim(Permissions.Roles, Permissions.Roles); + policy.RequireClaim(Permissions.RolesSave, Permissions.RolesSave); + }); + + // User policies + o.AddPolicy(Permissions.Users, policy => + { + policy.RequireClaim(Permission.Admin, Permission.Admin); + policy.RequireClaim(Permissions.Users, Permissions.Users); + }); + o.AddPolicy(Permissions.UsersAdd, policy => + { + policy.RequireClaim(Permission.Admin, Permission.Admin); + policy.RequireClaim(Permissions.Users, Permissions.Users); + policy.RequireClaim(Permissions.UsersAdd, Permissions.UsersAdd); + }); + o.AddPolicy(Permissions.UsersDelete, policy => + { + policy.RequireClaim(Permission.Admin, Permission.Admin); + policy.RequireClaim(Permissions.Users, Permissions.Users); + policy.RequireClaim(Permissions.UsersDelete, Permissions.UsersDelete); + }); + o.AddPolicy(Permissions.UsersEdit, policy => + { + policy.RequireClaim(Permission.Admin, Permission.Admin); + policy.RequireClaim(Permissions.Users, Permissions.Users); + policy.RequireClaim(Permissions.UsersEdit, Permissions.UsersEdit); + }); + o.AddPolicy(Permissions.UsersSave, policy => + { + policy.RequireClaim(Permission.Admin, Permission.Admin); + policy.RequireClaim(Permissions.Users, Permissions.Users); + policy.RequireClaim(Permissions.UsersSave, Permissions.UsersSave); + }); }); - // User policies - o.AddPolicy(Permissions.Users, policy => - { - policy.RequireClaim(Permission.Admin, Permission.Admin); - policy.RequireClaim(Permissions.Users, Permissions.Users); - }); - o.AddPolicy(Permissions.UsersAdd, policy => - { - policy.RequireClaim(Permission.Admin, Permission.Admin); - policy.RequireClaim(Permissions.Users, Permissions.Users); - policy.RequireClaim(Permissions.UsersAdd, Permissions.UsersAdd); - }); - o.AddPolicy(Permissions.UsersDelete, policy => - { - policy.RequireClaim(Permission.Admin, Permission.Admin); - policy.RequireClaim(Permissions.Users, Permissions.Users); - policy.RequireClaim(Permissions.UsersDelete, Permissions.UsersDelete); - }); - o.AddPolicy(Permissions.UsersEdit, policy => - { - policy.RequireClaim(Permission.Admin, Permission.Admin); - policy.RequireClaim(Permissions.Users, Permissions.Users); - policy.RequireClaim(Permissions.UsersEdit, Permissions.UsersEdit); - }); - o.AddPolicy(Permissions.UsersSave, policy => + services.AddIdentity() + .AddDefaultTokenProviders(); + + services.Configure(identityOptions ?? SetDefaultOptions); + services.ConfigureApplicationCookie(cookieOptions ?? SetDefaultCookieOptions); + services.AddScoped(); + + return services; + } + + /// + /// Adds the Piranha identity module. + /// + /// The current service collection + /// Optional options for identity + /// Optional options for cookies + /// The services + public static IServiceCollection AddPiranhaIdentityWithSeed(this IServiceCollection services, + Action identityOptions = null, + Action cookieOptions = null) + where TSeed : class, IIdentitySeed + { + services = AddPiranhaIdentity(services, identityOptions, cookieOptions); + services.AddScoped(); + + return services; + } + + /// + /// Sets the default identity options if none was provided. Please note that + /// these settings provide very LOW security in terms of password rules, but + /// this is just so the default user can be seeded on first startup. + /// + /// The identity options + private static void SetDefaultOptions(IdentityOptions options) + { + // Password settings + options.Password.RequireDigit = false; + options.Password.RequiredLength = 6; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = false; + options.Password.RequireLowercase = false; + options.Password.RequiredUniqueChars = 1; + + // Lockout settings + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30); + options.Lockout.MaxFailedAccessAttempts = 10; + options.Lockout.AllowedForNewUsers = true; + + // User settings + options.User.RequireUniqueEmail = true; + } + + /// + /// Sets the default cookie options if none was provided. + /// + /// The cookie options + private static void SetDefaultCookieOptions(CookieAuthenticationOptions options) + { + options.Cookie.HttpOnly = true; + options.ExpireTimeSpan = TimeSpan.FromMinutes(30); + options.LoginPath = "/manager/login"; + options.AccessDeniedPath = "/manager/login"; + options.SlidingExpiration = true; + } + + /// + /// Uses the Piranha identity module. + /// + /// The current application builder + /// The builder + public static IApplicationBuilder UsePiranhaIdentity(this IApplicationBuilder builder) + { + // Set logout url to point to local auth + Piranha.App.Modules.Manager().LogoutUrl = "~/manager/logout"; + + // + // Add the embedded resources + // + return builder.UseStaticFiles(new StaticFileOptions { - policy.RequireClaim(Permission.Admin, Permission.Admin); - policy.RequireClaim(Permissions.Users, Permissions.Users); - policy.RequireClaim(Permissions.UsersSave, Permissions.UsersSave); + FileProvider = new EmbeddedFileProvider(typeof(IdentityModuleExtensions).Assembly, "Piranha.AspNetCore.Identity.assets"), + RequestPath = "/manager/identity" }); - }); - - services.AddDbContext(dbOptions); - services.AddScoped(); - services.AddScoped(); - services.AddIdentity() - .AddEntityFrameworkStores() - .AddDefaultTokenProviders(); - services.Configure(identityOptions != null ? identityOptions : SetDefaultOptions); - services.ConfigureApplicationCookie(cookieOptions != null ? cookieOptions : SetDefaultCookieOptions); - services.AddScoped(); - - return services; - } - - /// - /// Adds the Piranha identity module. - /// - /// The current service collection - /// Options for configuring the database - /// Optional options for identity - /// Optional options for cookies - /// The services - public static IServiceCollection AddPiranhaIdentityWithSeed(this IServiceCollection services, - Action dbOptions, - Action identityOptions = null, - Action cookieOptions = null) - where T : Db - where TSeed : class, IIdentitySeed - { - services = AddPiranhaIdentity(services, dbOptions, identityOptions, cookieOptions); - services.AddScoped(); - - return services; - } - - /// - /// Adds the Piranha identity module. - /// - /// The current service collection - /// Options for configuring the database - /// Optional options for identity - /// Optional options for cookies - /// The services - public static IServiceCollection AddPiranhaIdentityWithSeed(this IServiceCollection services, - Action dbOptions, - Action identityOptions = null, - Action cookieOptions = null) - where T : Db - { - return AddPiranhaIdentityWithSeed(services, dbOptions, identityOptions, cookieOptions); - } - - /// - /// Sets the default identity options if none was provided. Please note that - /// these settings provide very LOW security in terms of password rules, but - /// this is just so the default user can be seeded on first startup. - /// - /// The identity options - private static void SetDefaultOptions(IdentityOptions options) - { - // Password settings - options.Password.RequireDigit = false; - options.Password.RequiredLength = 6; - options.Password.RequireNonAlphanumeric = false; - options.Password.RequireUppercase = false; - options.Password.RequireLowercase = false; - options.Password.RequiredUniqueChars = 1; - - // Lockout settings - options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30); - options.Lockout.MaxFailedAccessAttempts = 10; - options.Lockout.AllowedForNewUsers = true; - - // User settings - options.User.RequireUniqueEmail = true; - } - - /// - /// Sets the default cookie options if none was provided. - /// - /// The cookie options - private static void SetDefaultCookieOptions(CookieAuthenticationOptions options) - { - options.Cookie.HttpOnly = true; - options.ExpireTimeSpan = TimeSpan.FromMinutes(30); - options.LoginPath = "/manager/login"; - options.AccessDeniedPath = "/manager/login"; - options.SlidingExpiration = true; - } - - /// - /// Uses the Piranha identity module. - /// - /// The current application builder - /// The builder - public static IApplicationBuilder UsePiranhaIdentity(this IApplicationBuilder builder) - { - // Set logout url to point to local auth - Piranha.App.Modules.Manager().LogoutUrl = "~/manager/logout"; - - // - // Add the embedded resources - // - return builder.UseStaticFiles(new StaticFileOptions - { - FileProvider = new EmbeddedFileProvider(typeof(IdentityModuleExtensions).Assembly, "Piranha.AspNetCore.Identity.assets"), - RequestPath = "/manager/identity" - }); + } } } \ No newline at end of file diff --git a/identity/Piranha.AspNetCore.Identity/IdentityStartupExtensions.cs b/identity/Piranha.AspNetCore.Identity/IdentityStartupExtensions.cs index d70f7c947..bcc51fcde 100644 --- a/identity/Piranha.AspNetCore.Identity/IdentityStartupExtensions.cs +++ b/identity/Piranha.AspNetCore.Identity/IdentityStartupExtensions.cs @@ -11,84 +11,74 @@ using System; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Piranha; -using Piranha.AspNetCore.Identity; -using Piranha.AspNetCore; -public static class IdentityStartupExtensions +namespace Piranha.AspNetCore.Identity { - /// - /// Extensions method for simplified setup. - /// - /// The current service builder - /// The db options - /// The optional identity options - /// The optional cookie options - /// The DbContext type - /// The builder - public static PiranhaServiceBuilder UseIdentity(this PiranhaServiceBuilder serviceBuilder, - Action dbOptions, - Action identityOptions = null, - Action cookieOptions = null) - where T : Db + public static class IdentityStartupExtensions { - serviceBuilder.Services.AddPiranhaIdentity(dbOptions, identityOptions, cookieOptions); + /// + /// Extensions method for simplified setup. + /// + /// The current service builder + /// The optional identity options + /// The optional cookie options + /// The DbContext type + /// The builder + public static PiranhaServiceBuilder UseIdentity(this PiranhaServiceBuilder serviceBuilder, + Action identityOptions = null, + Action cookieOptions = null) + { + serviceBuilder.Services.AddPiranhaIdentity(identityOptions, cookieOptions); - return serviceBuilder; - } + return serviceBuilder; + } - /// - /// Extensions method for simplified setup. - /// - /// The current service builder - /// The db options - /// The optional identity options - /// The optional cookie options - /// The DbContext type - /// The builder - public static PiranhaServiceBuilder UseIdentityWithSeed(this PiranhaServiceBuilder serviceBuilder, - Action dbOptions, - Action identityOptions = null, - Action cookieOptions = null) - where T : Db - { - serviceBuilder.Services.AddPiranhaIdentityWithSeed(dbOptions, identityOptions, cookieOptions); + /// + /// Extensions method for simplified setup. + /// + /// The current service builder + /// The optional identity options + /// The optional cookie options + /// The DbContext type + /// The builder + public static PiranhaServiceBuilder UseIdentityWithSeed(this PiranhaServiceBuilder serviceBuilder, + Action identityOptions = null, + Action cookieOptions = null) + { + serviceBuilder.Services.AddPiranhaIdentityWithSeed(identityOptions, cookieOptions); - return serviceBuilder; - } + return serviceBuilder; + } - /// - /// Extensions method for simplified setup. - /// - /// The current service builder - /// The db options - /// The optional identity options - /// The optional cookie options - /// The DbContext type - /// The seed type - /// The builder - public static PiranhaServiceBuilder UseIdentityWithSeed(this PiranhaServiceBuilder serviceBuilder, - Action dbOptions, - Action identityOptions = null, - Action cookieOptions = null) - where T : Db - where TSeed : class, IIdentitySeed - { - serviceBuilder.Services.AddPiranhaIdentityWithSeed(dbOptions, identityOptions, cookieOptions); + /// + /// Extensions method for simplified setup. + /// + /// The current service builder + /// The optional identity options + /// The optional cookie options + /// The DbContext type + /// The seed type + /// The builder + public static PiranhaServiceBuilder UseIdentityWithSeed(this PiranhaServiceBuilder serviceBuilder, + Action identityOptions = null, + Action cookieOptions = null) + where TSeed : class, IIdentitySeed + { + serviceBuilder.Services.AddPiranhaIdentityWithSeed(identityOptions, cookieOptions); - return serviceBuilder; - } + return serviceBuilder; + } - /// - /// Uses the Piranha identity module. - /// - /// The current application builder - /// The builder - public static PiranhaApplicationBuilder UseIdentity(this PiranhaApplicationBuilder applicationBuilder) - { - applicationBuilder.Builder.UsePiranhaIdentity(); + /// + /// Uses the Piranha identity module. + /// + /// The current application builder + /// The builder + public static PiranhaApplicationBuilder UseIdentity(this PiranhaApplicationBuilder applicationBuilder) + { + applicationBuilder.Builder.UsePiranhaIdentity(); - return applicationBuilder; + return applicationBuilder; + } } } \ No newline at end of file diff --git a/identity/Piranha.AspNetCore.Identity/Models/RoleEditModel.cs b/identity/Piranha.AspNetCore.Identity/Models/RoleEditModel.cs index e3d486387..5190b4f59 100644 --- a/identity/Piranha.AspNetCore.Identity/Models/RoleEditModel.cs +++ b/identity/Piranha.AspNetCore.Identity/Models/RoleEditModel.cs @@ -8,10 +8,7 @@ * */ -using System; using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNetCore.Identity; using Piranha.AspNetCore.Identity.Data; namespace Piranha.AspNetCore.Identity.Models @@ -25,89 +22,5 @@ public RoleEditModel() public Role Role { get; set; } public IList SelectedClaims { get; set; } - - public static RoleEditModel GetById(IDb db, Guid id) - { - var role = db.Roles.FirstOrDefault(r => r.Id == id); - - if (role != null) - { - var model = new RoleEditModel - { - Role = role - }; - - var roleClaims = db.RoleClaims.Where(r => r.RoleId == id).ToList(); - foreach (var claim in roleClaims) - { - model.SelectedClaims.Add(claim.ClaimType); - } - return model; - } - - return null; - } - - public static RoleEditModel Create() - { - return new RoleEditModel - { - Role = new Role() - }; - } - - public bool Save(IDb db) - { - var role = db.Roles.FirstOrDefault(r => r.Id == Role.Id); - - if (role == null) - { - Role.Id = Role.Id != Guid.Empty ? Role.Id : Guid.NewGuid(); - Role.NormalizedName = !string.IsNullOrEmpty(Role.NormalizedName) - ? Role.NormalizedName.ToUpper() - : Role.Name.ToUpper(); - - role = new Role - { - Id = Role.Id - }; - db.Roles.Add(role); - } - - role.Name = Role.Name; - role.NormalizedName = Role.NormalizedName; - - var claims = db.RoleClaims.Where(r => r.RoleId == role.Id).ToList(); - var delete = new List>(); - var add = new List>(); - - foreach (var old in claims) - { - if (!SelectedClaims.Contains(old.ClaimType)) - { - delete.Add(old); - } - } - - foreach (var selected in SelectedClaims) - { - if (!claims.Any(c => c.ClaimType == selected)) - { - add.Add(new IdentityRoleClaim - { - RoleId = role.Id, - ClaimType = selected, - ClaimValue = selected - }); - } - } - - db.RoleClaims.RemoveRange(delete); - db.RoleClaims.AddRange(add); - - db.SaveChanges(); - - return true; - } } -} \ No newline at end of file +} diff --git a/identity/Piranha.AspNetCore.Identity/Models/RoleListModel.cs b/identity/Piranha.AspNetCore.Identity/Models/RoleListModel.cs index 56ba8920d..79e18bf32 100644 --- a/identity/Piranha.AspNetCore.Identity/Models/RoleListModel.cs +++ b/identity/Piranha.AspNetCore.Identity/Models/RoleListModel.cs @@ -23,27 +23,6 @@ public RoleListModel() public IList Roles { get; set; } - public static RoleListModel Get(IDb db) - { - var model = new RoleListModel - { - Roles = db.Roles - .OrderBy(r => r.Name) - .Select(r => new ListItem - { - Id = r.Id, - Name = r.Name - }).ToList() - }; - - foreach (var role in model.Roles) - { - role.UserCount = db.UserRoles - .Count(r => r.RoleId == role.Id); - } - return model; - } - public class ListItem { public Guid Id { get; set; } diff --git a/identity/Piranha.AspNetCore.Identity/Models/UserEditModel.cs b/identity/Piranha.AspNetCore.Identity/Models/UserEditModel.cs index 19b570762..95f2e04ae 100644 --- a/identity/Piranha.AspNetCore.Identity/Models/UserEditModel.cs +++ b/identity/Piranha.AspNetCore.Identity/Models/UserEditModel.cs @@ -8,11 +8,7 @@ * */ -using System; using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; using Piranha.AspNetCore.Identity.Data; namespace Piranha.AspNetCore.Identity.Models @@ -24,104 +20,5 @@ public class UserEditModel public IList SelectedRoles { get; set; } = new List(); public string Password { get; set; } public string PasswordConfirm { get; set; } - - public static UserEditModel Create(IDb db) - { - return new UserEditModel - { - User = new User(), - Roles = db.Roles.OrderBy(r => r.Name).ToList() - }; - } - - public static UserEditModel GetById(IDb db, Guid id) - { - var user = db.Users.FirstOrDefault(u => u.Id == id); - - if (user != null) - { - var model = new UserEditModel - { - User = user, - Roles = db.Roles.OrderBy(r => r.Name).ToList() - }; - - var userRoles = db.UserRoles.Where(r => r.UserId == id).ToList(); - foreach (var role in userRoles) - { - model.SelectedRoles.Add(model.Roles.Single(r => r.Id == role.RoleId).Name); - } - return model; - } - - return null; - } - - public async Task Save(UserManager userManager) - { - IdentityResult result; - var user = await userManager.FindByIdAsync(User.Id.ToString()); - - if (user == null) - { - user = new User - { - Id = User.Id != Guid.Empty ? User.Id : Guid.NewGuid(), - UserName = User.UserName, - Email = User.Email - }; - User.Id = user.Id; - - result = await userManager.CreateAsync(user, Password); - if (!result.Succeeded) { - return result; - } - } - else - { - result = await userManager.SetUserNameAsync(user, User.UserName); - if (!result.Succeeded) - { - return result; - } - - result = await userManager.SetEmailAsync(user, User.Email); - if (!result.Succeeded) - { - return result; - } - } - - // Remove old roles - var roles = await userManager.GetRolesAsync(user); - result = await userManager.RemoveFromRolesAsync(user, roles); - if (!result.Succeeded) - { - return result; - } - - // Add current roles - result = await userManager.AddToRolesAsync(user, SelectedRoles); - if (!result.Succeeded) - { - return result; - } - - if (!string.IsNullOrWhiteSpace(Password)) - { - result = await userManager.RemovePasswordAsync(user); - if (!result.Succeeded) - { - return result; - } - result = await userManager.AddPasswordAsync(user, Password); - if (!result.Succeeded) - { - return result; - } - } - - return result; - } } } \ No newline at end of file diff --git a/identity/Piranha.AspNetCore.Identity/Models/UserListModel.cs b/identity/Piranha.AspNetCore.Identity/Models/UserListModel.cs index 92ba85c39..383698c7c 100644 --- a/identity/Piranha.AspNetCore.Identity/Models/UserListModel.cs +++ b/identity/Piranha.AspNetCore.Identity/Models/UserListModel.cs @@ -10,7 +10,6 @@ using System; using System.Collections.Generic; -using System.Linq; namespace Piranha.AspNetCore.Identity.Models { @@ -18,43 +17,6 @@ public class UserListModel { public IList Users { get; set; } = new List(); - public static UserListModel Get(IDb db) - { - var model = new UserListModel - { - Users = db.Users - .OrderBy(u => u.UserName) - .Select(u => new ListItem - { - Id = u.Id, - UserName = u.UserName, - Email = u.Email, - GravatarUrl = !string.IsNullOrWhiteSpace(u.Email) ? Utils.GetGravatarUrl(u.Email, 25) : null - }).ToList() - }; - - var roles = db.Roles - .ToList(); - - foreach (var user in model.Users) - { - var userRoles = db.UserRoles - .Where(r => r.UserId == user.Id) - .ToList(); - - foreach (var userRole in userRoles) - { - var role = roles.FirstOrDefault(r => r.Id == userRole.RoleId); - if (role != null) - { - user.Roles.Add(role.Name); - } - } - } - - return model; - } - public class ListItem { public Guid Id { get; set; } diff --git a/identity/Piranha.AspNetCore.Identity/Piranha.AspNetCore.Identity.csproj b/identity/Piranha.AspNetCore.Identity/Piranha.AspNetCore.Identity.csproj index 51d7a12c6..40c6d96aa 100644 --- a/identity/Piranha.AspNetCore.Identity/Piranha.AspNetCore.Identity.csproj +++ b/identity/Piranha.AspNetCore.Identity/Piranha.AspNetCore.Identity.csproj @@ -12,13 +12,12 @@ - - + - +