From 4ed1c6bc47811c710a998c89a389f9b991a31c7d Mon Sep 17 00:00:00 2001 From: Mohsen Esmailpour Date: Sun, 19 Jan 2020 00:27:13 +0330 Subject: [PATCH 01/77] Implement dynamic auth core functionality for MVC. --- .../DynamicAuthorization.Mvc.Core.csproj | 22 +++++ .../Extensions/ServiceCollectionExtensions.cs | 20 +++++ .../Filters/DynamicAuthorizationFilter.cs | 76 ++++++++++++++++ .../IMvcControllerDiscovery.cs | 9 ++ .../IRoleAccessStore.cs | 9 ++ .../Models/MvcActionInfo.cs | 13 +++ .../Models/MvcControllerInfo.cs | 17 ++++ .../MvcControllerDiscovery.cs | 86 +++++++++++++++++++ src/DynamicRoleBasedAuthorization.sln | 20 +++-- 9 files changed, 265 insertions(+), 7 deletions(-) create mode 100644 src/DynamicAuthorization.Mvc.Core/DynamicAuthorization.Mvc.Core.csproj create mode 100644 src/DynamicAuthorization.Mvc.Core/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/DynamicAuthorization.Mvc.Core/Filters/DynamicAuthorizationFilter.cs create mode 100644 src/DynamicAuthorization.Mvc.Core/IMvcControllerDiscovery.cs create mode 100644 src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs create mode 100644 src/DynamicAuthorization.Mvc.Core/Models/MvcActionInfo.cs create mode 100644 src/DynamicAuthorization.Mvc.Core/Models/MvcControllerInfo.cs create mode 100644 src/DynamicAuthorization.Mvc.Core/MvcControllerDiscovery.cs diff --git a/src/DynamicAuthorization.Mvc.Core/DynamicAuthorization.Mvc.Core.csproj b/src/DynamicAuthorization.Mvc.Core/DynamicAuthorization.Mvc.Core.csproj new file mode 100644 index 0000000..9698083 --- /dev/null +++ b/src/DynamicAuthorization.Mvc.Core/DynamicAuthorization.Mvc.Core.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0;netcoreapp3.0 + + + + NETCORE2;NETSTANDARD;NETSTANDARD2_0 + + + + NETCORE3;NETSTANDARD;NETSTANDARD2_1 + + + + + + + + + + \ No newline at end of file diff --git a/src/DynamicAuthorization.Mvc.Core/Extensions/ServiceCollectionExtensions.cs b/src/DynamicAuthorization.Mvc.Core/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..6f1e382 --- /dev/null +++ b/src/DynamicAuthorization.Mvc.Core/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; + +namespace DynamicAuthorization.Mvc.Core.Extensions +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddDynamicAuthorization(this IServiceCollection services) + { + services.AddSingleton(); + + services.Configure(options => + { + options.Filters.Add(typeof(DynamicAuthorizationFilter)); + }); + + return services; + } + } +} \ No newline at end of file diff --git a/src/DynamicAuthorization.Mvc.Core/Filters/DynamicAuthorizationFilter.cs b/src/DynamicAuthorization.Mvc.Core/Filters/DynamicAuthorizationFilter.cs new file mode 100644 index 0000000..e9354ff --- /dev/null +++ b/src/DynamicAuthorization.Mvc.Core/Filters/DynamicAuthorizationFilter.cs @@ -0,0 +1,76 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Authorization; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace DynamicAuthorization.Mvc.Core +{ + public class DynamicAuthorizationFilter : IAsyncAuthorizationFilter + { + private readonly IRoleAccessStore _roleAccessStore; + + public DynamicAuthorizationFilter(IRoleAccessStore roleAccessStore) + { + _roleAccessStore = roleAccessStore; + } + + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + if (!IsProtectedAction(context)) + return; + + if (!IsUserAuthenticated(context)) + { + context.Result = new UnauthorizedResult(); + return; + } + + var actionId = GetActionId(context); + var userName = context.HttpContext.User.Identity.Name; + + if (await _roleAccessStore.HasAccessToActionAsync(userName, actionId)) + return; + + context.Result = new ForbidResult(); + } + + private bool IsProtectedAction(AuthorizationFilterContext context) + { + if (context.Filters.Any(item => item is IAllowAnonymousFilter)) + return false; + + var controllerActionDescriptor = (ControllerActionDescriptor)context.ActionDescriptor; + var controllerTypeInfo = controllerActionDescriptor.ControllerTypeInfo; + var actionMethodInfo = controllerActionDescriptor.MethodInfo; + + var authorizeAttribute = controllerTypeInfo.GetCustomAttribute(); + if (authorizeAttribute != null) + return true; + + authorizeAttribute = actionMethodInfo.GetCustomAttribute(); + if (authorizeAttribute != null) + return true; + + return false; + } + + private bool IsUserAuthenticated(AuthorizationFilterContext context) + { + return context.HttpContext.User.Identity.IsAuthenticated; + } + + private string GetActionId(AuthorizationFilterContext context) + { + var controllerActionDescriptor = (ControllerActionDescriptor)context.ActionDescriptor; + var area = controllerActionDescriptor.ControllerTypeInfo.GetCustomAttribute()?.RouteValue; + var controller = controllerActionDescriptor.ControllerName; + var action = controllerActionDescriptor.ActionName; + + return $"{area}:{controller}:{action}"; + } + } +} \ No newline at end of file diff --git a/src/DynamicAuthorization.Mvc.Core/IMvcControllerDiscovery.cs b/src/DynamicAuthorization.Mvc.Core/IMvcControllerDiscovery.cs new file mode 100644 index 0000000..e14e3a6 --- /dev/null +++ b/src/DynamicAuthorization.Mvc.Core/IMvcControllerDiscovery.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace DynamicAuthorization.Mvc.Core +{ + public interface IMvcControllerDiscovery + { + IEnumerable GetControllers(); + } +} \ No newline at end of file diff --git a/src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs b/src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs new file mode 100644 index 0000000..29abe26 --- /dev/null +++ b/src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace DynamicAuthorization.Mvc.Core +{ + public interface IRoleAccessStore + { + Task HasAccessToActionAsync(string userName, string actionId); + } +} \ No newline at end of file diff --git a/src/DynamicAuthorization.Mvc.Core/Models/MvcActionInfo.cs b/src/DynamicAuthorization.Mvc.Core/Models/MvcActionInfo.cs new file mode 100644 index 0000000..4a8f0b6 --- /dev/null +++ b/src/DynamicAuthorization.Mvc.Core/Models/MvcActionInfo.cs @@ -0,0 +1,13 @@ +namespace DynamicAuthorization.Mvc.Core +{ + public class MvcActionInfo + { + public string Id => $"{ControllerId}:{Name}"; + + public string Name { get; set; } + + public string DisplayName { get; set; } + + public string ControllerId { get; set; } + } +} \ No newline at end of file diff --git a/src/DynamicAuthorization.Mvc.Core/Models/MvcControllerInfo.cs b/src/DynamicAuthorization.Mvc.Core/Models/MvcControllerInfo.cs new file mode 100644 index 0000000..7b551a1 --- /dev/null +++ b/src/DynamicAuthorization.Mvc.Core/Models/MvcControllerInfo.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace DynamicAuthorization.Mvc.Core +{ + public class MvcControllerInfo + { + public string Id => $"{AreaName}:{Name}"; + + public string Name { get; set; } + + public string DisplayName { get; set; } + + public string AreaName { get; set; } + + public IEnumerable Actions { get; set; } + } +} \ No newline at end of file diff --git a/src/DynamicAuthorization.Mvc.Core/MvcControllerDiscovery.cs b/src/DynamicAuthorization.Mvc.Core/MvcControllerDiscovery.cs new file mode 100644 index 0000000..47e94ac --- /dev/null +++ b/src/DynamicAuthorization.Mvc.Core/MvcControllerDiscovery.cs @@ -0,0 +1,86 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; + +namespace DynamicAuthorization.Mvc.Core +{ + public class MvcControllerDiscovery : IMvcControllerDiscovery + { + private List _mvcControllers; + private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; + + public MvcControllerDiscovery(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider) + { + _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; + } + + public IEnumerable GetControllers() + { + if (_mvcControllers != null) + return _mvcControllers; + + _mvcControllers = new List(); + var items = _actionDescriptorCollectionProvider + .ActionDescriptors.Items + .Where(descriptor => descriptor.GetType() == typeof(ControllerActionDescriptor)) + .Select(descriptor => (ControllerActionDescriptor)descriptor) + .GroupBy(descriptor => descriptor.ControllerTypeInfo.FullName) + .ToList(); + + foreach (var actionDescriptors in items) + { + if (!actionDescriptors.Any()) + continue; + + var actionDescriptor = actionDescriptors.First(); + var controllerTypeInfo = actionDescriptor.ControllerTypeInfo; + var currentController = new MvcControllerInfo + { + AreaName = controllerTypeInfo.GetCustomAttribute()?.RouteValue, + DisplayName = controllerTypeInfo.GetCustomAttribute()?.DisplayName, + Name = actionDescriptor.ControllerName, + }; + + var actions = new List(); + foreach (var descriptor in actionDescriptors.GroupBy(a => a.ActionName).Select(g => g.First())) + { + var methodInfo = descriptor.MethodInfo; + if (IsProtectedAction(controllerTypeInfo, methodInfo)) + actions.Add(new MvcActionInfo + { + ControllerId = currentController.Id, + Name = descriptor.ActionName, + DisplayName = methodInfo.GetCustomAttribute()?.DisplayName, + }); + } + + if (actions.Any()) + { + currentController.Actions = actions; + _mvcControllers.Add(currentController); + } + } + + return _mvcControllers; + } + + private static bool IsProtectedAction(MemberInfo controllerTypeInfo, MemberInfo actionMethodInfo) + { + if (actionMethodInfo.GetCustomAttribute(true) != null) + return false; + + if (controllerTypeInfo.GetCustomAttribute(true) != null) + return true; + + if (actionMethodInfo.GetCustomAttribute(true) != null) + return true; + + return false; + } + } +} \ No newline at end of file diff --git a/src/DynamicRoleBasedAuthorization.sln b/src/DynamicRoleBasedAuthorization.sln index 1ba6bdc..4ba7379 100644 --- a/src/DynamicRoleBasedAuthorization.sln +++ b/src/DynamicRoleBasedAuthorization.sln @@ -1,9 +1,11 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27703.2018 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29613.14 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynamicRoleBasedAuthorization", "DynamicRoleBasedAuthorization\DynamicRoleBasedAuthorization.csproj", "{FF9B4BAF-0879-4A28-9C80-710EA0B94829}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DynamicAuthorization.Mvc.Core", "DynamicAuthorization.Mvc.Core\DynamicAuthorization.Mvc.Core.csproj", "{89E57FDC-5AF7-4AAF-A40B-FBA91D41ECF4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DynamicRoleBasedAuthorization", "DynamicRoleBasedAuthorization\DynamicRoleBasedAuthorization.csproj", "{1C9180AD-3F20-4F16-9C2F-5DFB502A7FD5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -11,10 +13,14 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {FF9B4BAF-0879-4A28-9C80-710EA0B94829}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FF9B4BAF-0879-4A28-9C80-710EA0B94829}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FF9B4BAF-0879-4A28-9C80-710EA0B94829}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FF9B4BAF-0879-4A28-9C80-710EA0B94829}.Release|Any CPU.Build.0 = Release|Any CPU + {89E57FDC-5AF7-4AAF-A40B-FBA91D41ECF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89E57FDC-5AF7-4AAF-A40B-FBA91D41ECF4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89E57FDC-5AF7-4AAF-A40B-FBA91D41ECF4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89E57FDC-5AF7-4AAF-A40B-FBA91D41ECF4}.Release|Any CPU.Build.0 = Release|Any CPU + {1C9180AD-3F20-4F16-9C2F-5DFB502A7FD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C9180AD-3F20-4F16-9C2F-5DFB502A7FD5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C9180AD-3F20-4F16-9C2F-5DFB502A7FD5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C9180AD-3F20-4F16-9C2F-5DFB502A7FD5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From d6dd4ab46708fdfd82064a5016fc3f4b96dbe8bf Mon Sep 17 00:00:00 2001 From: Mohsen Esmailpour Date: Sun, 19 Jan 2020 10:31:49 +0330 Subject: [PATCH 02/77] Add authorization options model. --- .../Extensions/ServiceCollectionExtensions.cs | 14 +++++++++---- .../Filters/DynamicAuthorizationFilter.cs | 20 +++++++++++++++---- .../Models/DynamicAuthorizationOptions.cs | 7 +++++++ 3 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 src/DynamicAuthorization.Mvc.Core/Models/DynamicAuthorizationOptions.cs diff --git a/src/DynamicAuthorization.Mvc.Core/Extensions/ServiceCollectionExtensions.cs b/src/DynamicAuthorization.Mvc.Core/Extensions/ServiceCollectionExtensions.cs index 6f1e382..7f55a82 100644 --- a/src/DynamicAuthorization.Mvc.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/DynamicAuthorization.Mvc.Core/Extensions/ServiceCollectionExtensions.cs @@ -1,17 +1,23 @@ -using Microsoft.AspNetCore.Mvc; +using DynamicAuthorization.Mvc.Core.Models; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; +using System; namespace DynamicAuthorization.Mvc.Core.Extensions { public static class ServiceCollectionExtensions { - public static IServiceCollection AddDynamicAuthorization(this IServiceCollection services) + public static IServiceCollection AddDynamicAuthorization(this IServiceCollection services, Action options) { + var dynamicAuthorizationOptions = new DynamicAuthorizationOptions(); + options.Invoke(dynamicAuthorizationOptions); + services.AddSingleton(dynamicAuthorizationOptions); + services.AddSingleton(); - services.Configure(options => + services.Configure(mvcOptions => { - options.Filters.Add(typeof(DynamicAuthorizationFilter)); + mvcOptions.Filters.Add(typeof(DynamicAuthorizationFilter)); }); return services; diff --git a/src/DynamicAuthorization.Mvc.Core/Filters/DynamicAuthorizationFilter.cs b/src/DynamicAuthorization.Mvc.Core/Filters/DynamicAuthorizationFilter.cs index e9354ff..29d4f84 100644 --- a/src/DynamicAuthorization.Mvc.Core/Filters/DynamicAuthorizationFilter.cs +++ b/src/DynamicAuthorization.Mvc.Core/Filters/DynamicAuthorizationFilter.cs @@ -1,23 +1,32 @@ -using Microsoft.AspNetCore.Authorization; +using DynamicAuthorization.Mvc.Core.Models; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Authorization; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; +using System; using System.Linq; using System.Reflection; using System.Threading.Tasks; namespace DynamicAuthorization.Mvc.Core { - public class DynamicAuthorizationFilter : IAsyncAuthorizationFilter + public class DynamicAuthorizationFilter : IAuthorizationFilter, IAsyncAuthorizationFilter { + private readonly DynamicAuthorizationOptions _authorizationOptions; private readonly IRoleAccessStore _roleAccessStore; - public DynamicAuthorizationFilter(IRoleAccessStore roleAccessStore) + public DynamicAuthorizationFilter(DynamicAuthorizationOptions authorizationOptions, IRoleAccessStore roleAccessStore) { + _authorizationOptions = authorizationOptions; _roleAccessStore = roleAccessStore; } + public void OnAuthorization(AuthorizationFilterContext context) + { + OnAuthorizationAsync(context).RunSynchronously(); + } + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { if (!IsProtectedAction(context)) @@ -29,8 +38,11 @@ public async Task OnAuthorizationAsync(AuthorizationFilterContext context) return; } - var actionId = GetActionId(context); var userName = context.HttpContext.User.Identity.Name; + if (userName.Equals(_authorizationOptions.DefaultAdminUser, StringComparison.CurrentCultureIgnoreCase)) + return; + + var actionId = GetActionId(context); if (await _roleAccessStore.HasAccessToActionAsync(userName, actionId)) return; diff --git a/src/DynamicAuthorization.Mvc.Core/Models/DynamicAuthorizationOptions.cs b/src/DynamicAuthorization.Mvc.Core/Models/DynamicAuthorizationOptions.cs new file mode 100644 index 0000000..e831edb --- /dev/null +++ b/src/DynamicAuthorization.Mvc.Core/Models/DynamicAuthorizationOptions.cs @@ -0,0 +1,7 @@ +namespace DynamicAuthorization.Mvc.Core.Models +{ + public class DynamicAuthorizationOptions + { + public string DefaultAdminUser { get; set; } + } +} \ No newline at end of file From eb7e5c3cb8e0eb67cbd8ca8d05c4530a27616328 Mon Sep 17 00:00:00 2001 From: Mohsen Esmailpour Date: Tue, 21 Jan 2020 23:29:57 +0330 Subject: [PATCH 03/77] Add JSON role access store. --- .../IRoleAccessStore.cs | 8 ++- .../Models/RoleAccess.cs | 11 ++++ .../DynamicAuthorization.Mvc.JsonStore.csproj | 14 +++++ .../Extensions/ServiceCollectionExtensions.cs | 46 ++++++++++++++++ .../JsonOptions.cs | 9 ++++ .../RoleAccessStore.cs | 54 +++++++++++++++++++ src/DynamicRoleBasedAuthorization.sln | 6 +++ 7 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 src/DynamicAuthorization.Mvc.Core/Models/RoleAccess.cs create mode 100644 src/DynamicAuthorization.Mvc.JsonStore/DynamicAuthorization.Mvc.JsonStore.csproj create mode 100644 src/DynamicAuthorization.Mvc.JsonStore/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/DynamicAuthorization.Mvc.JsonStore/JsonOptions.cs create mode 100644 src/DynamicAuthorization.Mvc.JsonStore/RoleAccessStore.cs diff --git a/src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs b/src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs index 29abe26..f3662d4 100644 --- a/src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs +++ b/src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs @@ -4,6 +4,12 @@ namespace DynamicAuthorization.Mvc.Core { public interface IRoleAccessStore { - Task HasAccessToActionAsync(string userName, string actionId); + Task AddRoleAccessAsync(RoleAccess roleAccess); + + Task EditRoleAccessAsync(RoleAccess roleAccess); + + Task RemoveRoleAccessAsync(string roleId); + + bool HasAccessToAction(string actionId, params string[] roles); } } \ No newline at end of file diff --git a/src/DynamicAuthorization.Mvc.Core/Models/RoleAccess.cs b/src/DynamicAuthorization.Mvc.Core/Models/RoleAccess.cs new file mode 100644 index 0000000..38d5f26 --- /dev/null +++ b/src/DynamicAuthorization.Mvc.Core/Models/RoleAccess.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace DynamicAuthorization.Mvc.Core +{ + public class RoleAccess + { + public string RoleId { get; set; } + + public IEnumerable Controllers { get; set; } + } +} \ No newline at end of file diff --git a/src/DynamicAuthorization.Mvc.JsonStore/DynamicAuthorization.Mvc.JsonStore.csproj b/src/DynamicAuthorization.Mvc.JsonStore/DynamicAuthorization.Mvc.JsonStore.csproj new file mode 100644 index 0000000..9d99cca --- /dev/null +++ b/src/DynamicAuthorization.Mvc.JsonStore/DynamicAuthorization.Mvc.JsonStore.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.0 + + + + + + + + + + \ No newline at end of file diff --git a/src/DynamicAuthorization.Mvc.JsonStore/Extensions/ServiceCollectionExtensions.cs b/src/DynamicAuthorization.Mvc.JsonStore/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..8dfea13 --- /dev/null +++ b/src/DynamicAuthorization.Mvc.JsonStore/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,46 @@ +using DynamicAuthorization.Mvc.Core; +using JsonFlatFileDataStore; +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace DynamicAuthorization.Mvc.JsonStore.Extensions +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddDynamicAuthorization(this IServiceCollection services) + { + if (services == null) + throw new ArgumentNullException(nameof(services)); + + AddRequiredServices(services, new JsonOptions()); + + return services; + } + + public static IServiceCollection AddDynamicAuthorization(this IServiceCollection services, Action options) + { + if (services == null) + throw new ArgumentNullException(nameof(services)); + + if (options == null) + throw new ArgumentNullException(nameof(options)); + + var jsonOptions = new JsonOptions(); + options.Invoke(jsonOptions); + + if (jsonOptions.FileName == null) + throw new NullReferenceException(nameof(jsonOptions.FileName)); + + AddRequiredServices(services, jsonOptions); + + return services; + } + + private static void AddRequiredServices(IServiceCollection services, JsonOptions jsonOptions) + { + services.AddSingleton(jsonOptions); + services.AddSingleton(new DataStore(jsonOptions.FileName)); + services.AddScoped(); + } + } +} \ No newline at end of file diff --git a/src/DynamicAuthorization.Mvc.JsonStore/JsonOptions.cs b/src/DynamicAuthorization.Mvc.JsonStore/JsonOptions.cs new file mode 100644 index 0000000..d16294f --- /dev/null +++ b/src/DynamicAuthorization.Mvc.JsonStore/JsonOptions.cs @@ -0,0 +1,9 @@ +namespace DynamicAuthorization.Mvc.JsonStore +{ + public class JsonOptions + { + public string FileName { get; set; } = "RoleAccess.json"; + + public bool UseMemoryCache { get; set; } = true; + } +} \ No newline at end of file diff --git a/src/DynamicAuthorization.Mvc.JsonStore/RoleAccessStore.cs b/src/DynamicAuthorization.Mvc.JsonStore/RoleAccessStore.cs new file mode 100644 index 0000000..180d569 --- /dev/null +++ b/src/DynamicAuthorization.Mvc.JsonStore/RoleAccessStore.cs @@ -0,0 +1,54 @@ +using DynamicAuthorization.Mvc.Core; +using JsonFlatFileDataStore; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace DynamicAuthorization.Mvc.JsonStore +{ + public class RoleAccessStore : IRoleAccessStore + { + //private readonly JsonOptions _jsonOptions; + private readonly DataStore _store; + + public RoleAccessStore(DataStore store) + { + //_jsonOptions = jsonOptions; + _store = store; + } + + public Task AddRoleAccessAsync(RoleAccess roleAccess) + { + var collection = _store.GetCollection(); + + return collection.InsertOneAsync(roleAccess); + } + + public Task EditRoleAccessAsync(RoleAccess roleAccess) + { + var collection = _store.GetCollection(); + + return collection.UpdateOneAsync(roleAccess.RoleId, roleAccess); + } + + public Task RemoveRoleAccessAsync(string roleId) + { + var collection = _store.GetCollection(); + return collection.DeleteOneAsync(roleId); + } + + public bool HasAccessToAction(string actionId, params string[] roles) + { + if (roles == null || !roles.Any()) + return false; + + var accessList = _store.GetCollection() + .AsQueryable() + .Where(ra => roles.Contains(ra.RoleId)) + .SelectMany(ra => ra.Controllers) + .ToList(); + + return accessList.SelectMany(c => c.Actions).Any(a => a.Id == actionId); + } + } +} \ No newline at end of file diff --git a/src/DynamicRoleBasedAuthorization.sln b/src/DynamicRoleBasedAuthorization.sln index 4ba7379..a613edf 100644 --- a/src/DynamicRoleBasedAuthorization.sln +++ b/src/DynamicRoleBasedAuthorization.sln @@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DynamicAuthorization.Mvc.Co EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DynamicRoleBasedAuthorization", "DynamicRoleBasedAuthorization\DynamicRoleBasedAuthorization.csproj", "{1C9180AD-3F20-4F16-9C2F-5DFB502A7FD5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynamicAuthorization.Mvc.JsonStore", "DynamicAuthorization.Mvc.JsonStore\DynamicAuthorization.Mvc.JsonStore.csproj", "{3E397C28-EDC7-4DCC-896F-476A9EE74E76}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {1C9180AD-3F20-4F16-9C2F-5DFB502A7FD5}.Debug|Any CPU.Build.0 = Debug|Any CPU {1C9180AD-3F20-4F16-9C2F-5DFB502A7FD5}.Release|Any CPU.ActiveCfg = Release|Any CPU {1C9180AD-3F20-4F16-9C2F-5DFB502A7FD5}.Release|Any CPU.Build.0 = Release|Any CPU + {3E397C28-EDC7-4DCC-896F-476A9EE74E76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E397C28-EDC7-4DCC-896F-476A9EE74E76}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E397C28-EDC7-4DCC-896F-476A9EE74E76}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E397C28-EDC7-4DCC-896F-476A9EE74E76}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From b6fca705536a27805febe007ed4b36154f28c39a Mon Sep 17 00:00:00 2001 From: Mohsen Esmailpour Date: Sun, 1 Mar 2020 23:11:32 +0330 Subject: [PATCH 04/77] Make HasAccessToAction method async. --- src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs | 2 +- src/DynamicAuthorization.Mvc.JsonStore/RoleAccessStore.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs b/src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs index f3662d4..6e96f1b 100644 --- a/src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs +++ b/src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs @@ -10,6 +10,6 @@ public interface IRoleAccessStore Task RemoveRoleAccessAsync(string roleId); - bool HasAccessToAction(string actionId, params string[] roles); + Task HasAccessToActionAsync(string actionId, params string[] roles); } } \ No newline at end of file diff --git a/src/DynamicAuthorization.Mvc.JsonStore/RoleAccessStore.cs b/src/DynamicAuthorization.Mvc.JsonStore/RoleAccessStore.cs index 180d569..9dc42b9 100644 --- a/src/DynamicAuthorization.Mvc.JsonStore/RoleAccessStore.cs +++ b/src/DynamicAuthorization.Mvc.JsonStore/RoleAccessStore.cs @@ -37,10 +37,10 @@ public Task RemoveRoleAccessAsync(string roleId) return collection.DeleteOneAsync(roleId); } - public bool HasAccessToAction(string actionId, params string[] roles) + public Task HasAccessToActionAsync(string actionId, params string[] roles) { if (roles == null || !roles.Any()) - return false; + return Task.FromResult(false); var accessList = _store.GetCollection() .AsQueryable() @@ -48,7 +48,7 @@ public bool HasAccessToAction(string actionId, params string[] roles) .SelectMany(ra => ra.Controllers) .ToList(); - return accessList.SelectMany(c => c.Actions).Any(a => a.Id == actionId); + return Task.FromResult(accessList.SelectMany(c => c.Actions).Any(a => a.Id == actionId)); } } } \ No newline at end of file From f2fe0460c5b9c3f187af0de0b6a0a5f5c1a6ebb2 Mon Sep 17 00:00:00 2001 From: Mohsen Esmailpour Date: Sun, 1 Mar 2020 23:13:20 +0330 Subject: [PATCH 05/77] Split IsProtectedAction for different target frameworks. --- .../Filters/DynamicAuthorizationFilter.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/DynamicAuthorization.Mvc.Core/Filters/DynamicAuthorizationFilter.cs b/src/DynamicAuthorization.Mvc.Core/Filters/DynamicAuthorizationFilter.cs index 29d4f84..a733666 100644 --- a/src/DynamicAuthorization.Mvc.Core/Filters/DynamicAuthorizationFilter.cs +++ b/src/DynamicAuthorization.Mvc.Core/Filters/DynamicAuthorizationFilter.cs @@ -50,6 +50,35 @@ public async Task OnAuthorizationAsync(AuthorizationFilterContext context) context.Result = new ForbidResult(); } +#if NETCORE3 + + private static bool IsProtectedAction(AuthorizationFilterContext context) + { + var controllerActionDescriptor = (ControllerActionDescriptor)context.ActionDescriptor; + var controllerTypeInfo = controllerActionDescriptor.ControllerTypeInfo; + + var anonymousAttribute = controllerTypeInfo.GetCustomAttribute(); + if (anonymousAttribute != null) + return false; + + var actionMethodInfo = controllerActionDescriptor.MethodInfo; + anonymousAttribute = actionMethodInfo.GetCustomAttribute(); + if (anonymousAttribute != null) + return false; + + var authorizeAttribute = controllerTypeInfo.GetCustomAttribute(); + if (authorizeAttribute != null) + return true; + + authorizeAttribute = actionMethodInfo.GetCustomAttribute(); + if (authorizeAttribute != null) + return true; + + return false; + } + +#else + private bool IsProtectedAction(AuthorizationFilterContext context) { if (context.Filters.Any(item => item is IAllowAnonymousFilter)) @@ -70,6 +99,8 @@ private bool IsProtectedAction(AuthorizationFilterContext context) return false; } +#endif + private bool IsUserAuthenticated(AuthorizationFilterContext context) { return context.HttpContext.User.Identity.IsAuthenticated; From c44ffe9b134723f269b59b298f2116081e16d7e3 Mon Sep 17 00:00:00 2001 From: Mohsen Esmailpour Date: Sun, 1 Mar 2020 23:36:12 +0330 Subject: [PATCH 06/77] Fix sln path. --- ...edAuthorization.sln => DynamicRoleBasedAuthorization.sln | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename src/DynamicRoleBasedAuthorization.sln => DynamicRoleBasedAuthorization.sln (78%) diff --git a/src/DynamicRoleBasedAuthorization.sln b/DynamicRoleBasedAuthorization.sln similarity index 78% rename from src/DynamicRoleBasedAuthorization.sln rename to DynamicRoleBasedAuthorization.sln index a613edf..7178dc4 100644 --- a/src/DynamicRoleBasedAuthorization.sln +++ b/DynamicRoleBasedAuthorization.sln @@ -3,11 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29613.14 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DynamicAuthorization.Mvc.Core", "DynamicAuthorization.Mvc.Core\DynamicAuthorization.Mvc.Core.csproj", "{89E57FDC-5AF7-4AAF-A40B-FBA91D41ECF4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DynamicAuthorization.Mvc.Core", "src\DynamicAuthorization.Mvc.Core\DynamicAuthorization.Mvc.Core.csproj", "{89E57FDC-5AF7-4AAF-A40B-FBA91D41ECF4}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DynamicRoleBasedAuthorization", "DynamicRoleBasedAuthorization\DynamicRoleBasedAuthorization.csproj", "{1C9180AD-3F20-4F16-9C2F-5DFB502A7FD5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DynamicRoleBasedAuthorization", "src\DynamicRoleBasedAuthorization\DynamicRoleBasedAuthorization.csproj", "{1C9180AD-3F20-4F16-9C2F-5DFB502A7FD5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynamicAuthorization.Mvc.JsonStore", "DynamicAuthorization.Mvc.JsonStore\DynamicAuthorization.Mvc.JsonStore.csproj", "{3E397C28-EDC7-4DCC-896F-476A9EE74E76}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DynamicAuthorization.Mvc.JsonStore", "src\DynamicAuthorization.Mvc.JsonStore\DynamicAuthorization.Mvc.JsonStore.csproj", "{3E397C28-EDC7-4DCC-896F-476A9EE74E76}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution From d817541fc61fafae27792753f269503a05e0c854 Mon Sep 17 00:00:00 2001 From: Mohsen Esmailpour Date: Thu, 5 Mar 2020 00:12:06 +0330 Subject: [PATCH 07/77] Add dynamic authorization configuration builder. --- .../Builder/DynamicAuthorizationBuilder.cs | 14 +++++++++++++ .../Builder/IDynamicAuthorizationBuilder.cs | 15 ++++++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 9 ++++++--- ...amicAuthorizationJsonBuilderExtensions.cs} | 20 +++++++++---------- 4 files changed, 45 insertions(+), 13 deletions(-) create mode 100644 src/DynamicAuthorization.Mvc.Core/Builder/DynamicAuthorizationBuilder.cs create mode 100644 src/DynamicAuthorization.Mvc.Core/Builder/IDynamicAuthorizationBuilder.cs rename src/DynamicAuthorization.Mvc.JsonStore/Extensions/{ServiceCollectionExtensions.cs => DynamicAuthorizationJsonBuilderExtensions.cs} (59%) diff --git a/src/DynamicAuthorization.Mvc.Core/Builder/DynamicAuthorizationBuilder.cs b/src/DynamicAuthorization.Mvc.Core/Builder/DynamicAuthorizationBuilder.cs new file mode 100644 index 0000000..5409f02 --- /dev/null +++ b/src/DynamicAuthorization.Mvc.Core/Builder/DynamicAuthorizationBuilder.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace DynamicAuthorization.Mvc.Core.Builder +{ + internal class DynamicAuthorizationBuilder : IDynamicAuthorizationBuilder + { + public DynamicAuthorizationBuilder(IServiceCollection services) + { + Services = services; + } + + public IServiceCollection Services { get; } + } +} \ No newline at end of file diff --git a/src/DynamicAuthorization.Mvc.Core/Builder/IDynamicAuthorizationBuilder.cs b/src/DynamicAuthorization.Mvc.Core/Builder/IDynamicAuthorizationBuilder.cs new file mode 100644 index 0000000..66f3c41 --- /dev/null +++ b/src/DynamicAuthorization.Mvc.Core/Builder/IDynamicAuthorizationBuilder.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace DynamicAuthorization.Mvc.Core +{ + /// + /// An interface for configuring dynamic authorization services. + /// + public interface IDynamicAuthorizationBuilder + { + /// + /// Gets the where essential services are configured. + /// + IServiceCollection Services { get; } + } +} \ No newline at end of file diff --git a/src/DynamicAuthorization.Mvc.Core/Extensions/ServiceCollectionExtensions.cs b/src/DynamicAuthorization.Mvc.Core/Extensions/ServiceCollectionExtensions.cs index 7f55a82..a09ec2a 100644 --- a/src/DynamicAuthorization.Mvc.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/DynamicAuthorization.Mvc.Core/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ -using DynamicAuthorization.Mvc.Core.Models; +using DynamicAuthorization.Mvc.Core.Builder; +using DynamicAuthorization.Mvc.Core.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using System; @@ -7,7 +8,7 @@ namespace DynamicAuthorization.Mvc.Core.Extensions { public static class ServiceCollectionExtensions { - public static IServiceCollection AddDynamicAuthorization(this IServiceCollection services, Action options) + public static IDynamicAuthorizationBuilder AddDynamicAuthorization(this IServiceCollection services, Action options) { var dynamicAuthorizationOptions = new DynamicAuthorizationOptions(); options.Invoke(dynamicAuthorizationOptions); @@ -20,7 +21,9 @@ public static IServiceCollection AddDynamicAuthorization(this IServiceCollection mvcOptions.Filters.Add(typeof(DynamicAuthorizationFilter)); }); - return services; + IDynamicAuthorizationBuilder builder = new DynamicAuthorizationBuilder(services); + + return builder; } } } \ No newline at end of file diff --git a/src/DynamicAuthorization.Mvc.JsonStore/Extensions/ServiceCollectionExtensions.cs b/src/DynamicAuthorization.Mvc.JsonStore/Extensions/DynamicAuthorizationJsonBuilderExtensions.cs similarity index 59% rename from src/DynamicAuthorization.Mvc.JsonStore/Extensions/ServiceCollectionExtensions.cs rename to src/DynamicAuthorization.Mvc.JsonStore/Extensions/DynamicAuthorizationJsonBuilderExtensions.cs index 8dfea13..8643f6e 100644 --- a/src/DynamicAuthorization.Mvc.JsonStore/Extensions/ServiceCollectionExtensions.cs +++ b/src/DynamicAuthorization.Mvc.JsonStore/Extensions/DynamicAuthorizationJsonBuilderExtensions.cs @@ -7,20 +7,20 @@ namespace DynamicAuthorization.Mvc.JsonStore.Extensions { public static class ServiceCollectionExtensions { - public static IServiceCollection AddDynamicAuthorization(this IServiceCollection services) + public static IDynamicAuthorizationBuilder AddJsonStore(this IDynamicAuthorizationBuilder builder) { - if (services == null) - throw new ArgumentNullException(nameof(services)); + if (builder == null) + throw new ArgumentNullException(nameof(builder)); - AddRequiredServices(services, new JsonOptions()); + AddRequiredServices(builder.Services, new JsonOptions()); - return services; + return builder; } - public static IServiceCollection AddDynamicAuthorization(this IServiceCollection services, Action options) + public static IDynamicAuthorizationBuilder AddJsonStore(this IDynamicAuthorizationBuilder builder, Action options) { - if (services == null) - throw new ArgumentNullException(nameof(services)); + if (builder == null) + throw new ArgumentNullException(nameof(builder)); if (options == null) throw new ArgumentNullException(nameof(options)); @@ -31,9 +31,9 @@ public static IServiceCollection AddDynamicAuthorization(this IServiceCollection if (jsonOptions.FileName == null) throw new NullReferenceException(nameof(jsonOptions.FileName)); - AddRequiredServices(services, jsonOptions); + AddRequiredServices(builder.Services, jsonOptions); - return services; + return builder; } private static void AddRequiredServices(IServiceCollection services, JsonOptions jsonOptions) From dfe542288c331c40fc66718493ff80376e2bb55d Mon Sep 17 00:00:00 2001 From: Mohsen Esmailpour Date: Thu, 5 Mar 2020 13:03:36 +0330 Subject: [PATCH 08/77] Return result of add, edit and delete RoleAccess operation is successful. --- src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs | 6 +++--- src/DynamicAuthorization.Mvc.JsonStore/RoleAccessStore.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs b/src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs index 6e96f1b..ab2f180 100644 --- a/src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs +++ b/src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs @@ -4,11 +4,11 @@ namespace DynamicAuthorization.Mvc.Core { public interface IRoleAccessStore { - Task AddRoleAccessAsync(RoleAccess roleAccess); + Task AddRoleAccessAsync(RoleAccess roleAccess); - Task EditRoleAccessAsync(RoleAccess roleAccess); + Task EditRoleAccessAsync(RoleAccess roleAccess); - Task RemoveRoleAccessAsync(string roleId); + Task RemoveRoleAccessAsync(string roleId); Task HasAccessToActionAsync(string actionId, params string[] roles); } diff --git a/src/DynamicAuthorization.Mvc.JsonStore/RoleAccessStore.cs b/src/DynamicAuthorization.Mvc.JsonStore/RoleAccessStore.cs index 9dc42b9..89e90aa 100644 --- a/src/DynamicAuthorization.Mvc.JsonStore/RoleAccessStore.cs +++ b/src/DynamicAuthorization.Mvc.JsonStore/RoleAccessStore.cs @@ -17,21 +17,21 @@ public RoleAccessStore(DataStore store) _store = store; } - public Task AddRoleAccessAsync(RoleAccess roleAccess) + public Task AddRoleAccessAsync(RoleAccess roleAccess) { var collection = _store.GetCollection(); return collection.InsertOneAsync(roleAccess); } - public Task EditRoleAccessAsync(RoleAccess roleAccess) + public Task EditRoleAccessAsync(RoleAccess roleAccess) { var collection = _store.GetCollection(); return collection.UpdateOneAsync(roleAccess.RoleId, roleAccess); } - public Task RemoveRoleAccessAsync(string roleId) + public Task RemoveRoleAccessAsync(string roleId) { var collection = _store.GetCollection(); return collection.DeleteOneAsync(roleId); From d4fc9e01b7c2cf07539a544aa569fa1577a38003 Mon Sep 17 00:00:00 2001 From: Mohsen Esmailpour Date: Thu, 5 Mar 2020 15:09:24 +0330 Subject: [PATCH 09/77] Fix json store update. --- src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs | 2 ++ .../DynamicAuthorizationJsonBuilderExtensions.cs | 10 +++++++--- src/DynamicAuthorization.Mvc.JsonStore/JsonOptions.cs | 2 +- .../RoleAccessStore.cs | 10 +++++++++- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs b/src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs index ab2f180..af099f1 100644 --- a/src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs +++ b/src/DynamicAuthorization.Mvc.Core/IRoleAccessStore.cs @@ -10,6 +10,8 @@ public interface IRoleAccessStore Task RemoveRoleAccessAsync(string roleId); + Task GetRoleAccessAsync(string roleId); + Task HasAccessToActionAsync(string actionId, params string[] roles); } } \ No newline at end of file diff --git a/src/DynamicAuthorization.Mvc.JsonStore/Extensions/DynamicAuthorizationJsonBuilderExtensions.cs b/src/DynamicAuthorization.Mvc.JsonStore/Extensions/DynamicAuthorizationJsonBuilderExtensions.cs index 8643f6e..2813cf8 100644 --- a/src/DynamicAuthorization.Mvc.JsonStore/Extensions/DynamicAuthorizationJsonBuilderExtensions.cs +++ b/src/DynamicAuthorization.Mvc.JsonStore/Extensions/DynamicAuthorizationJsonBuilderExtensions.cs @@ -2,6 +2,7 @@ using JsonFlatFileDataStore; using Microsoft.Extensions.DependencyInjection; using System; +using System.IO; namespace DynamicAuthorization.Mvc.JsonStore.Extensions { @@ -28,8 +29,8 @@ public static IDynamicAuthorizationBuilder AddJsonStore(this IDynamicAuthorizati var jsonOptions = new JsonOptions(); options.Invoke(jsonOptions); - if (jsonOptions.FileName == null) - throw new NullReferenceException(nameof(jsonOptions.FileName)); + if (jsonOptions.FilePath == null) + throw new NullReferenceException(nameof(jsonOptions.FilePath)); AddRequiredServices(builder.Services, jsonOptions); @@ -38,8 +39,11 @@ public static IDynamicAuthorizationBuilder AddJsonStore(this IDynamicAuthorizati private static void AddRequiredServices(IServiceCollection services, JsonOptions jsonOptions) { + if (jsonOptions.FilePath == "RoleAccess.json") + jsonOptions.FilePath = $"{Directory.GetCurrentDirectory()}\\{jsonOptions.FilePath}"; + services.AddSingleton(jsonOptions); - services.AddSingleton(new DataStore(jsonOptions.FileName)); + services.AddSingleton(provider => new DataStore(jsonOptions.FilePath, keyProperty: "roleId")); services.AddScoped(); } } diff --git a/src/DynamicAuthorization.Mvc.JsonStore/JsonOptions.cs b/src/DynamicAuthorization.Mvc.JsonStore/JsonOptions.cs index d16294f..6c343d4 100644 --- a/src/DynamicAuthorization.Mvc.JsonStore/JsonOptions.cs +++ b/src/DynamicAuthorization.Mvc.JsonStore/JsonOptions.cs @@ -2,7 +2,7 @@ { public class JsonOptions { - public string FileName { get; set; } = "RoleAccess.json"; + public string FilePath { get; set; } = "RoleAccess.json"; public bool UseMemoryCache { get; set; } = true; } diff --git a/src/DynamicAuthorization.Mvc.JsonStore/RoleAccessStore.cs b/src/DynamicAuthorization.Mvc.JsonStore/RoleAccessStore.cs index 89e90aa..1c03425 100644 --- a/src/DynamicAuthorization.Mvc.JsonStore/RoleAccessStore.cs +++ b/src/DynamicAuthorization.Mvc.JsonStore/RoleAccessStore.cs @@ -28,15 +28,23 @@ public Task EditRoleAccessAsync(RoleAccess roleAccess) { var collection = _store.GetCollection(); - return collection.UpdateOneAsync(roleAccess.RoleId, roleAccess); + return collection.ReplaceOneAsync(roleAccess.RoleId, roleAccess); } public Task RemoveRoleAccessAsync(string roleId) { var collection = _store.GetCollection(); + return collection.DeleteOneAsync(roleId); } + public Task GetRoleAccessAsync(string roleId) + { + var collection = _store.GetCollection(); + + return Task.FromResult(collection.AsQueryable().FirstOrDefault(ra => ra.RoleId == roleId)); + } + public Task HasAccessToActionAsync(string actionId, params string[] roles) { if (roles == null || !roles.Any()) From cf37e99bdc5112843bf64a902ed950fa748916fc Mon Sep 17 00:00:00 2001 From: Mohsen Esmailpour Date: Thu, 5 Mar 2020 21:09:34 +0330 Subject: [PATCH 10/77] Add asp.net core 3.x sample project. --- DynamicRoleBasedAuthorization.sln | 18 + .../Access/Controllers/RoleController.cs | 186 + .../Areas/Access/Models/RoleViewModel.cs | 15 + .../Areas/Access/Views/Role/Create.cshtml | 63 + .../Areas/Access/Views/Role/Edit.cshtml | 70 + .../Areas/Access/Views/Role/Index.cshtml | 39 + .../Areas/Access/Views/_ViewImports.cshtml | 5 + .../Areas/Access/Views/_ViewStart.cshtml | 3 + .../Areas/Identity/Pages/_ViewStart.cshtml | 3 + .../Controllers/HomeController.cs | 37 + .../Data/ApplicationDbContext.cs | 16 + ...000000000_CreateIdentitySchema.Designer.cs | 277 + .../00000000000000_CreateIdentitySchema.cs | 220 + .../ApplicationDbContextModelSnapshot.cs | 275 + .../SampleMvcWebApp/Models/ErrorViewModel.cs | 11 + samples/netcore3.x/SampleMvcWebApp/Program.cs | 26 + .../SampleMvcWebApp/RoleAccess.json | 2 + .../SampleMvcWebApp/SampleMvcWebApp.csproj | 24 + .../SampleMvcWebApp/ScaffoldingReadMe.txt | 12 + samples/netcore3.x/SampleMvcWebApp/Startup.cs | 76 + .../SampleMvcWebApp/Views/Home/Index.cshtml | 9 + .../SampleMvcWebApp/Views/Home/Privacy.cshtml | 6 + .../SampleMvcWebApp/Views/Shared/Error.cshtml | 25 + .../Views/Shared/_Layout.cshtml | 52 + .../Views/Shared/_LoginPartial.cshtml | 26 + .../Shared/_ValidationScriptsPartial.cshtml | 2 + .../SampleMvcWebApp/Views/_ViewImports.cshtml | 3 + .../SampleMvcWebApp/Views/_ViewStart.cshtml | 3 + .../appsettings.Development.json | 9 + .../SampleMvcWebApp/appsettings.json | 13 + .../netcore3.x/SampleMvcWebApp/libman.json | 5 + .../SampleMvcWebApp/wwwroot/css/site.css | 78 + .../SampleMvcWebApp/wwwroot/favicon.ico | Bin 0 -> 32038 bytes .../wwwroot/js/jquery-bonsai/.bower.json | 25 + .../wwwroot/js/jquery-bonsai/CHANGELOG.md | 9 + .../wwwroot/js/jquery-bonsai/LICENSE.txt | 21 + .../wwwroot/js/jquery-bonsai/README.md | 108 + .../js/jquery-bonsai/assets/svg-icons.css | 16 + .../wwwroot/js/jquery-bonsai/bower.json | 15 + .../js/jquery-bonsai/jquery.bonsai.css | 42 + .../wwwroot/js/jquery-bonsai/jquery.bonsai.js | 305 + .../wwwroot/js/jquery-qubit/.bower.json | 36 + .../wwwroot/js/jquery-qubit/LICENSE.txt | 21 + .../wwwroot/js/jquery-qubit/README.md | 16 + .../wwwroot/js/jquery-qubit/bower.json | 26 + .../wwwroot/js/jquery-qubit/jquery.qubit.js | 93 + .../SampleMvcWebApp/wwwroot/js/site.js | 69 + .../wwwroot/lib/bootstrap/LICENSE | 22 + .../lib/bootstrap/dist/css/bootstrap-grid.css | 3719 ++++++ .../bootstrap/dist/css/bootstrap-grid.css.map | 1 + .../bootstrap/dist/css/bootstrap-grid.min.css | 7 + .../dist/css/bootstrap-grid.min.css.map | 1 + .../bootstrap/dist/css/bootstrap-reboot.css | 331 + .../dist/css/bootstrap-reboot.css.map | 1 + .../dist/css/bootstrap-reboot.min.css | 8 + .../dist/css/bootstrap-reboot.min.css.map | 1 + .../lib/bootstrap/dist/css/bootstrap.css | 10038 +++++++++++++++ .../lib/bootstrap/dist/css/bootstrap.css.map | 1 + .../lib/bootstrap/dist/css/bootstrap.min.css | 7 + .../bootstrap/dist/css/bootstrap.min.css.map | 1 + .../lib/bootstrap/dist/js/bootstrap.bundle.js | 7013 +++++++++++ .../bootstrap/dist/js/bootstrap.bundle.js.map | 1 + .../bootstrap/dist/js/bootstrap.bundle.min.js | 7 + .../dist/js/bootstrap.bundle.min.js.map | 1 + .../lib/bootstrap/dist/js/bootstrap.js | 4435 +++++++ .../lib/bootstrap/dist/js/bootstrap.js.map | 1 + .../lib/bootstrap/dist/js/bootstrap.min.js | 7 + .../bootstrap/dist/js/bootstrap.min.js.map | 1 + .../jquery-validation-unobtrusive/LICENSE.txt | 12 + .../jquery.validate.unobtrusive.js | 432 + .../jquery.validate.unobtrusive.min.js | 5 + .../wwwroot/lib/jquery-validation/LICENSE.md | 22 + .../dist/additional-methods.js | 1158 ++ .../dist/additional-methods.min.js | 4 + .../jquery-validation/dist/jquery.validate.js | 1601 +++ .../dist/jquery.validate.min.js | 4 + .../wwwroot/lib/jquery/LICENSE.txt | 36 + .../wwwroot/lib/jquery/dist/jquery.js | 10364 ++++++++++++++++ .../wwwroot/lib/jquery/dist/jquery.min.js | 2 + .../wwwroot/lib/jquery/dist/jquery.min.map | 1 + 80 files changed, 41656 insertions(+) create mode 100644 samples/netcore3.x/SampleMvcWebApp/Areas/Access/Controllers/RoleController.cs create mode 100644 samples/netcore3.x/SampleMvcWebApp/Areas/Access/Models/RoleViewModel.cs create mode 100644 samples/netcore3.x/SampleMvcWebApp/Areas/Access/Views/Role/Create.cshtml create mode 100644 samples/netcore3.x/SampleMvcWebApp/Areas/Access/Views/Role/Edit.cshtml create mode 100644 samples/netcore3.x/SampleMvcWebApp/Areas/Access/Views/Role/Index.cshtml create mode 100644 samples/netcore3.x/SampleMvcWebApp/Areas/Access/Views/_ViewImports.cshtml create mode 100644 samples/netcore3.x/SampleMvcWebApp/Areas/Access/Views/_ViewStart.cshtml create mode 100644 samples/netcore3.x/SampleMvcWebApp/Areas/Identity/Pages/_ViewStart.cshtml create mode 100644 samples/netcore3.x/SampleMvcWebApp/Controllers/HomeController.cs create mode 100644 samples/netcore3.x/SampleMvcWebApp/Data/ApplicationDbContext.cs create mode 100644 samples/netcore3.x/SampleMvcWebApp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs create mode 100644 samples/netcore3.x/SampleMvcWebApp/Data/Migrations/00000000000000_CreateIdentitySchema.cs create mode 100644 samples/netcore3.x/SampleMvcWebApp/Data/Migrations/ApplicationDbContextModelSnapshot.cs create mode 100644 samples/netcore3.x/SampleMvcWebApp/Models/ErrorViewModel.cs create mode 100644 samples/netcore3.x/SampleMvcWebApp/Program.cs create mode 100644 samples/netcore3.x/SampleMvcWebApp/RoleAccess.json create mode 100644 samples/netcore3.x/SampleMvcWebApp/SampleMvcWebApp.csproj create mode 100644 samples/netcore3.x/SampleMvcWebApp/ScaffoldingReadMe.txt create mode 100644 samples/netcore3.x/SampleMvcWebApp/Startup.cs create mode 100644 samples/netcore3.x/SampleMvcWebApp/Views/Home/Index.cshtml create mode 100644 samples/netcore3.x/SampleMvcWebApp/Views/Home/Privacy.cshtml create mode 100644 samples/netcore3.x/SampleMvcWebApp/Views/Shared/Error.cshtml create mode 100644 samples/netcore3.x/SampleMvcWebApp/Views/Shared/_Layout.cshtml create mode 100644 samples/netcore3.x/SampleMvcWebApp/Views/Shared/_LoginPartial.cshtml create mode 100644 samples/netcore3.x/SampleMvcWebApp/Views/Shared/_ValidationScriptsPartial.cshtml create mode 100644 samples/netcore3.x/SampleMvcWebApp/Views/_ViewImports.cshtml create mode 100644 samples/netcore3.x/SampleMvcWebApp/Views/_ViewStart.cshtml create mode 100644 samples/netcore3.x/SampleMvcWebApp/appsettings.Development.json create mode 100644 samples/netcore3.x/SampleMvcWebApp/appsettings.json create mode 100644 samples/netcore3.x/SampleMvcWebApp/libman.json create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/css/site.css create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/favicon.ico create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/.bower.json create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/CHANGELOG.md create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/LICENSE.txt create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/README.md create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/assets/svg-icons.css create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/bower.json create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/jquery.bonsai.css create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/jquery.bonsai.js create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-qubit/.bower.json create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-qubit/LICENSE.txt create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-qubit/README.md create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-qubit/bower.json create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-qubit/jquery.qubit.js create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/js/site.js create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/LICENSE create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/dist/css/bootstrap.css create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/dist/js/bootstrap.js create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/jquery-validation/LICENSE.md create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/jquery-validation/dist/additional-methods.js create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/jquery-validation/dist/additional-methods.min.js create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/jquery-validation/dist/jquery.validate.js create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/jquery/LICENSE.txt create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/jquery/dist/jquery.js create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/jquery/dist/jquery.min.js create mode 100644 samples/netcore3.x/SampleMvcWebApp/wwwroot/lib/jquery/dist/jquery.min.map diff --git a/DynamicRoleBasedAuthorization.sln b/DynamicRoleBasedAuthorization.sln index 7178dc4..54f27f7 100644 --- a/DynamicRoleBasedAuthorization.sln +++ b/DynamicRoleBasedAuthorization.sln @@ -9,6 +9,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DynamicRoleBasedAuthorizati EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DynamicAuthorization.Mvc.JsonStore", "src\DynamicAuthorization.Mvc.JsonStore\DynamicAuthorization.Mvc.JsonStore.csproj", "{3E397C28-EDC7-4DCC-896F-476A9EE74E76}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A7EEBB7E-C64D-4475-93D0-872E080A4E03}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{BAAF9837-1DB5-4032-AB2A-2901E6EE6EF8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "netcore3.x", "netcore3.x", "{5FEB9007-1EFA-4814-BC15-DB0370B84E22}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleMvcWebApp", "samples\netcore3.x\SampleMvcWebApp\SampleMvcWebApp.csproj", "{0833E296-398F-42A4-9531-D125483AB019}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,10 +35,20 @@ Global {3E397C28-EDC7-4DCC-896F-476A9EE74E76}.Debug|Any CPU.Build.0 = Debug|Any CPU {3E397C28-EDC7-4DCC-896F-476A9EE74E76}.Release|Any CPU.ActiveCfg = Release|Any CPU {3E397C28-EDC7-4DCC-896F-476A9EE74E76}.Release|Any CPU.Build.0 = Release|Any CPU + {0833E296-398F-42A4-9531-D125483AB019}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0833E296-398F-42A4-9531-D125483AB019}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0833E296-398F-42A4-9531-D125483AB019}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0833E296-398F-42A4-9531-D125483AB019}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {89E57FDC-5AF7-4AAF-A40B-FBA91D41ECF4} = {A7EEBB7E-C64D-4475-93D0-872E080A4E03} + {3E397C28-EDC7-4DCC-896F-476A9EE74E76} = {A7EEBB7E-C64D-4475-93D0-872E080A4E03} + {5FEB9007-1EFA-4814-BC15-DB0370B84E22} = {BAAF9837-1DB5-4032-AB2A-2901E6EE6EF8} + {0833E296-398F-42A4-9531-D125483AB019} = {5FEB9007-1EFA-4814-BC15-DB0370B84E22} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {18C80710-C9E9-488B-9100-1E4BF8B18038} EndGlobalSection diff --git a/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Controllers/RoleController.cs b/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Controllers/RoleController.cs new file mode 100644 index 0000000..5b3244b --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Controllers/RoleController.cs @@ -0,0 +1,186 @@ +using DynamicAuthorization.Mvc.Core; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using SampleMvcWebApp.Areas.Access.Models; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; + +namespace SampleMvcWebApp.Areas.Access.Controllers +{ + [Area("Access"), Authorize] + [DisplayName("Role Management")] + public class RoleController : Controller + { + private readonly IMvcControllerDiscovery _mvcControllerDiscovery; + private readonly IRoleAccessStore _roleAccessStore; + private readonly RoleManager _roleManager; + + public RoleController( + IMvcControllerDiscovery mvcControllerDiscovery, + IRoleAccessStore roleAccessStore, + RoleManager roleManager + ) + { + _mvcControllerDiscovery = mvcControllerDiscovery; + _roleManager = roleManager; + _roleAccessStore = roleAccessStore; + } + + // GET: Role + [DisplayName("Role List")] + public async Task Index() + { + var roles = await _roleManager.Roles.ToListAsync(); + + return View(roles); + } + + [DisplayName("Create Role")] + // GET: Role/Create + public ActionResult Create() + { + var controllers = _mvcControllerDiscovery.GetControllers(); + ViewData["Controllers"] = controllers; + + return View(); + } + + // POST: Role/Create + [HttpPost, ValidateAntiForgeryToken] + public async Task Create(RoleViewModel viewModel) + { + if (!ModelState.IsValid) + { + ViewData["Controllers"] = _mvcControllerDiscovery.GetControllers(); + return View(viewModel); + } + + var role = new IdentityRole { Name = viewModel.Name }; + var result = await _roleManager.CreateAsync(role); + + if (!result.Succeeded) + { + foreach (var error in result.Errors) + ModelState.AddModelError("", error.Description); + + ViewData["Controllers"] = _mvcControllerDiscovery.GetControllers(); + return View(viewModel); + } + + if (viewModel.SelectedControllers != null && viewModel.SelectedControllers.Any()) + { + foreach (var controller in viewModel.SelectedControllers) + foreach (var action in controller.Actions) + action.ControllerId = controller.Id; + + var roleAccess = new RoleAccess + { + Controllers = viewModel.SelectedControllers, + RoleId = role.Id + }; + await _roleAccessStore.AddRoleAccessAsync(roleAccess); + } + + return RedirectToAction(nameof(Index)); + } + + // GET: Role/Edit/5 + [DisplayName("Edit Role")] + public async Task Edit(string id) + { + ViewData["Controllers"] = _mvcControllerDiscovery.GetControllers(); + var role = await _roleManager.FindByIdAsync(id); + if (role == null) + return NotFound(); + + var accessList = await _roleAccessStore.GetRoleAccessAsync(role.Id); + var viewModel = new RoleViewModel + { + Name = role.Name, + SelectedControllers = accessList?.Controllers + }; + + return View(viewModel); + } + + // POST: Role/Edit/5 + [HttpPost, ValidateAntiForgeryToken] + public async Task Edit(string id, RoleViewModel viewModel) + { + if (!ModelState.IsValid) + { + ViewData["Controllers"] = _mvcControllerDiscovery.GetControllers(); + return View(viewModel); + } + + // Check role exit + var role = await _roleManager.FindByIdAsync(id); + if (role == null) + { + ModelState.AddModelError("", "Role not found"); + ViewData["Controllers"] = _mvcControllerDiscovery.GetControllers(); + return View(); + } + + // Update role if role's name is changed + if (role.Name != viewModel.Name) + { + role.Name = viewModel.Name; + var result = await _roleManager.UpdateAsync(role); + if (!result.Succeeded) + { + foreach (var error in result.Errors) + ModelState.AddModelError("", error.Description); + + ViewData["Controllers"] = _mvcControllerDiscovery.GetControllers(); + return View(viewModel); + } + } + + // Update role access list + if (viewModel.SelectedControllers != null && viewModel.SelectedControllers.Any()) + { + foreach (var controller in viewModel.SelectedControllers) + foreach (var action in controller.Actions) + action.ControllerId = controller.Id; + } + + var roleAccess = new RoleAccess + { + Controllers = viewModel.SelectedControllers, + RoleId = role.Id + }; + await _roleAccessStore.EditRoleAccessAsync(roleAccess); + + return RedirectToAction(nameof(Index)); + } + + // POST: Role/Delete/5 + [HttpDelete("role/{id}")] + public async Task Delete(string id) + { + var role = await _roleManager.FindByIdAsync(id); + if (role == null) + { + ModelState.AddModelError("Error", "Role not found"); + return BadRequest(ModelState); + } + + var result = await _roleManager.DeleteAsync(role); + if (!result.Succeeded) + { + foreach (var error in result.Errors) + ModelState.AddModelError("Error", error.Description); + + return BadRequest(ModelState); + } + + await _roleAccessStore.RemoveRoleAccessAsync(role.Id); + + return Ok(new { }); + } + } +} \ No newline at end of file diff --git a/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Models/RoleViewModel.cs b/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Models/RoleViewModel.cs new file mode 100644 index 0000000..ba590e6 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Models/RoleViewModel.cs @@ -0,0 +1,15 @@ +using DynamicAuthorization.Mvc.Core; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace SampleMvcWebApp.Areas.Access.Models +{ + public class RoleViewModel + { + [Required] + [StringLength(256, ErrorMessage = "The {0} must be at least {2} characters long.")] + public string Name { get; set; } + + public IEnumerable SelectedControllers { get; set; } + } +} \ No newline at end of file diff --git a/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Views/Role/Create.cshtml b/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Views/Role/Create.cshtml new file mode 100644 index 0000000..2530cb0 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Views/Role/Create.cshtml @@ -0,0 +1,63 @@ +@model RoleViewModel +@{ + ViewData["Title"] = "Create Role"; + var controllers = (IEnumerable)ViewData["Controllers"]; +} + +

Create Role

+ +
+ +
+
+
+
+ +
+ +
+ + +
+
+ +
+ +
+
    + @foreach (var controller in controllers) + { + var name = controller.DisplayName ?? controller.Name; + +
  1. + + @name + @if (controller.Actions.Any()) + { +
      + @foreach (var action in controller.Actions) + { + name = action.DisplayName ?? action.Name; +
    • @name
    • + } +
    + } +
  2. + } +
+
+
+
+ +
+
+
+
+ + + +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +} \ No newline at end of file diff --git a/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Views/Role/Edit.cshtml b/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Views/Role/Edit.cshtml new file mode 100644 index 0000000..9ac589e --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Views/Role/Edit.cshtml @@ -0,0 +1,70 @@ +@model RoleViewModel +@{ + ViewData["Title"] = "Edit Role"; + var controllers = (IEnumerable)ViewData["Controllers"]; +} + +

Edit Role

+ +
+ +
+
+
+
+ +
+ +
+ + +
+
+ +
+ +
+
    + @foreach (var controller in controllers) + { + var selectedController = Model?.SelectedControllers?.SingleOrDefault(c => c.Id == controller.Id); + var name = controller.DisplayName ?? controller.Name; + +
  1. + + + @name + @if (controller.Actions.Any()) + { +
      + @foreach (var action in controller.Actions) + { + { + name = action.DisplayName ?? action.Name; + } +
    • a.Id == action.Id)) { data-checked='1' }> + @name +
    • + } +
    + } +
  2. + } +
+
+
+
+ +
+
+
+
+ + + +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +} \ No newline at end of file diff --git a/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Views/Role/Index.cshtml b/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Views/Role/Index.cshtml new file mode 100644 index 0000000..cf65a89 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Views/Role/Index.cshtml @@ -0,0 +1,39 @@ +@model IEnumerable + +@{ + ViewData["Title"] = "Role List"; +} + +

Role List

+ +Create Role +
+
+ + + + + + + + + @if (!Model.Any()) + { + + + + } + @foreach (var role in Model) + { + + + + + } + +
@Html.DisplayNameFor(m => m.Name) Actions
There is no role!
@Html.DisplayFor(m => role.Name) + Edit | + Delete +
+
+
\ No newline at end of file diff --git a/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Views/_ViewImports.cshtml b/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Views/_ViewImports.cshtml new file mode 100644 index 0000000..3a1a90d --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Views/_ViewImports.cshtml @@ -0,0 +1,5 @@ +@using SampleMvcWebApp +@using SampleMvcWebApp.Areas.Access.Models +@using Microsoft.AspNetCore.Identity +@using DynamicAuthorization.Mvc.Core +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers \ No newline at end of file diff --git a/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Views/_ViewStart.cshtml b/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Views/_ViewStart.cshtml new file mode 100644 index 0000000..a5f1004 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Areas/Access/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/samples/netcore3.x/SampleMvcWebApp/Areas/Identity/Pages/_ViewStart.cshtml b/samples/netcore3.x/SampleMvcWebApp/Areas/Identity/Pages/_ViewStart.cshtml new file mode 100644 index 0000000..c4284f6 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Areas/Identity/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "/Views/Shared/_Layout.cshtml"; +} diff --git a/samples/netcore3.x/SampleMvcWebApp/Controllers/HomeController.cs b/samples/netcore3.x/SampleMvcWebApp/Controllers/HomeController.cs new file mode 100644 index 0000000..88edc4f --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Controllers/HomeController.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using SampleMvcWebApp.Models; + +namespace SampleMvcWebApp.Controllers +{ + public class HomeController : Controller + { + private readonly ILogger _logger; + + public HomeController(ILogger logger) + { + _logger = logger; + } + + public IActionResult Index() + { + return View(); + } + + public IActionResult Privacy() + { + return View(); + } + + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public IActionResult Error() + { + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + } + } +} diff --git a/samples/netcore3.x/SampleMvcWebApp/Data/ApplicationDbContext.cs b/samples/netcore3.x/SampleMvcWebApp/Data/ApplicationDbContext.cs new file mode 100644 index 0000000..c0b1b0d --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Data/ApplicationDbContext.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace SampleMvcWebApp.Data +{ + public class ApplicationDbContext : IdentityDbContext + { + public ApplicationDbContext(DbContextOptions options) + : base(options) + { + } + } +} diff --git a/samples/netcore3.x/SampleMvcWebApp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs b/samples/netcore3.x/SampleMvcWebApp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs new file mode 100644 index 0000000..47fb17b --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs @@ -0,0 +1,277 @@ +// +using System; +using SampleMvcWebApp.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace SampleMvcWebApp.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("00000000000000_CreateIdentitySchema")] + partial class CreateIdentitySchema + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(128)") + .HasMaxLength(128); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(128)") + .HasMaxLength(128); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(128)") + .HasMaxLength(128); + + b.Property("Name") + .HasColumnType("nvarchar(128)") + .HasMaxLength(128); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/samples/netcore3.x/SampleMvcWebApp/Data/Migrations/00000000000000_CreateIdentitySchema.cs b/samples/netcore3.x/SampleMvcWebApp/Data/Migrations/00000000000000_CreateIdentitySchema.cs new file mode 100644 index 0000000..3901a08 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Data/Migrations/00000000000000_CreateIdentitySchema.cs @@ -0,0 +1,220 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace SampleMvcWebApp.Data.Migrations +{ + public partial class CreateIdentitySchema : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(nullable: false), + Name = table.Column(maxLength: 256, nullable: true), + NormalizedName = table.Column(maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(nullable: false), + UserName = table.Column(maxLength: 256, nullable: true), + NormalizedUserName = table.Column(maxLength: 256, nullable: true), + Email = table.Column(maxLength: 256, nullable: true), + NormalizedEmail = table.Column(maxLength: 256, nullable: true), + EmailConfirmed = table.Column(nullable: false), + PasswordHash = table.Column(nullable: true), + SecurityStamp = table.Column(nullable: true), + ConcurrencyStamp = table.Column(nullable: true), + PhoneNumber = table.Column(nullable: true), + PhoneNumberConfirmed = table.Column(nullable: false), + TwoFactorEnabled = table.Column(nullable: false), + LockoutEnd = table.Column(nullable: true), + LockoutEnabled = table.Column(nullable: false), + AccessFailedCount = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + RoleId = table.Column(nullable: false), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + UserId = table.Column(nullable: false), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(maxLength: 128, nullable: false), + ProviderKey = table.Column(maxLength: 128, nullable: false), + ProviderDisplayName = table.Column(nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(nullable: false), + RoleId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(nullable: false), + LoginProvider = table.Column(maxLength: 128, nullable: false), + Name = table.Column(maxLength: 128, nullable: false), + Value = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/samples/netcore3.x/SampleMvcWebApp/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/samples/netcore3.x/SampleMvcWebApp/Data/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..10c4d2a --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,275 @@ +// +using System; +using SampleMvcWebApp.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace SampleMvcWebApp.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(128)") + .HasMaxLength(128); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(128)") + .HasMaxLength(128); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(128)") + .HasMaxLength(128); + + b.Property("Name") + .HasColumnType("nvarchar(128)") + .HasMaxLength(128); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/samples/netcore3.x/SampleMvcWebApp/Models/ErrorViewModel.cs b/samples/netcore3.x/SampleMvcWebApp/Models/ErrorViewModel.cs new file mode 100644 index 0000000..42e121e --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Models/ErrorViewModel.cs @@ -0,0 +1,11 @@ +using System; + +namespace SampleMvcWebApp.Models +{ + public class ErrorViewModel + { + public string RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + } +} diff --git a/samples/netcore3.x/SampleMvcWebApp/Program.cs b/samples/netcore3.x/SampleMvcWebApp/Program.cs new file mode 100644 index 0000000..29e070b --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace SampleMvcWebApp +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/samples/netcore3.x/SampleMvcWebApp/RoleAccess.json b/samples/netcore3.x/SampleMvcWebApp/RoleAccess.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/RoleAccess.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/samples/netcore3.x/SampleMvcWebApp/SampleMvcWebApp.csproj b/samples/netcore3.x/SampleMvcWebApp/SampleMvcWebApp.csproj new file mode 100644 index 0000000..67ad11e --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/SampleMvcWebApp.csproj @@ -0,0 +1,24 @@ + + + + netcoreapp3.1 + aspnet-SampleMvcWebApp-2FEC7F74-D6B5-4631-999B-A0E62051C3B1 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/netcore3.x/SampleMvcWebApp/ScaffoldingReadMe.txt b/samples/netcore3.x/SampleMvcWebApp/ScaffoldingReadMe.txt new file mode 100644 index 0000000..987f834 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/ScaffoldingReadMe.txt @@ -0,0 +1,12 @@ +Scaffolding has generated all the files and added the required dependencies. + +However the Application's Startup code may required additional changes for things to work end to end. +Add the following code to the Configure method in your Application's Startup class if not already done: + + app.UseMvc(routes => + { + routes.MapRoute( + name : "areas", + template : "{area:exists}/{controller=Home}/{action=Index}/{id?}" + ); + }); diff --git a/samples/netcore3.x/SampleMvcWebApp/Startup.cs b/samples/netcore3.x/SampleMvcWebApp/Startup.cs new file mode 100644 index 0000000..df51963 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Startup.cs @@ -0,0 +1,76 @@ +using DynamicAuthorization.Mvc.Core.Extensions; +using DynamicAuthorization.Mvc.JsonStore.Extensions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using SampleMvcWebApp.Data; + +namespace SampleMvcWebApp +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddDbContext(options => + options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); + + services.AddIdentity(options => options.SignIn.RequireConfirmedAccount = false) + .AddEntityFrameworkStores() + .AddDefaultUI(); + + services.AddDynamicAuthorization(options => options.DefaultAdminUser = "mo.esmp@gmail.com") + .AddJsonStore(options => options.FilePath = + @"D:\Workspace\Github\DynamicRoleBasedAuthorizationNETCore\samples\netcore3.x\SampleMvcWebApp\bin\Debug\netcoreapp3.1\RoleAccess.json"); + + services.AddControllersWithViews(); + services.AddRazorPages(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseDatabaseErrorPage(); + } + else + { + app.UseExceptionHandler("/Home/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + } + + app.UseHttpsRedirection(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default-area", + pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + endpoints.MapRazorPages(); + }); + } + } +} \ No newline at end of file diff --git a/samples/netcore3.x/SampleMvcWebApp/Views/Home/Index.cshtml b/samples/netcore3.x/SampleMvcWebApp/Views/Home/Index.cshtml new file mode 100644 index 0000000..9cb4fa1 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Views/Home/Index.cshtml @@ -0,0 +1,9 @@ +@{ + ViewData["Title"] = "Home Page"; +} + +
+

Welcome

+

Learn about building Web apps with ASP.NET Core.

+ +
\ No newline at end of file diff --git a/samples/netcore3.x/SampleMvcWebApp/Views/Home/Privacy.cshtml b/samples/netcore3.x/SampleMvcWebApp/Views/Home/Privacy.cshtml new file mode 100644 index 0000000..af4fb19 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Views/Home/Privacy.cshtml @@ -0,0 +1,6 @@ +@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

diff --git a/samples/netcore3.x/SampleMvcWebApp/Views/Shared/Error.cshtml b/samples/netcore3.x/SampleMvcWebApp/Views/Shared/Error.cshtml new file mode 100644 index 0000000..a1e0478 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Views/Shared/Error.cshtml @@ -0,0 +1,25 @@ +@model ErrorViewModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

diff --git a/samples/netcore3.x/SampleMvcWebApp/Views/Shared/_Layout.cshtml b/samples/netcore3.x/SampleMvcWebApp/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000..5f81760 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Views/Shared/_Layout.cshtml @@ -0,0 +1,52 @@ + + + + + + @ViewData["Title"] - SampleMvcWebApp + + + + + +
+ +
+
+
+ @RenderBody() +
+
+ +
+
+ © 2020 - SampleMvcWebApp - Privacy +
+
+ + + + + + @RenderSection("Scripts", required: false) + + \ No newline at end of file diff --git a/samples/netcore3.x/SampleMvcWebApp/Views/Shared/_LoginPartial.cshtml b/samples/netcore3.x/SampleMvcWebApp/Views/Shared/_LoginPartial.cshtml new file mode 100644 index 0000000..cd256aa --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Views/Shared/_LoginPartial.cshtml @@ -0,0 +1,26 @@ +@using Microsoft.AspNetCore.Identity +@inject SignInManager SignInManager +@inject UserManager UserManager + + diff --git a/samples/netcore3.x/SampleMvcWebApp/Views/Shared/_ValidationScriptsPartial.cshtml b/samples/netcore3.x/SampleMvcWebApp/Views/Shared/_ValidationScriptsPartial.cshtml new file mode 100644 index 0000000..5a16d80 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Views/Shared/_ValidationScriptsPartial.cshtml @@ -0,0 +1,2 @@ + + diff --git a/samples/netcore3.x/SampleMvcWebApp/Views/_ViewImports.cshtml b/samples/netcore3.x/SampleMvcWebApp/Views/_ViewImports.cshtml new file mode 100644 index 0000000..e80d781 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Views/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using SampleMvcWebApp +@using SampleMvcWebApp.Models +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/samples/netcore3.x/SampleMvcWebApp/Views/_ViewStart.cshtml b/samples/netcore3.x/SampleMvcWebApp/Views/_ViewStart.cshtml new file mode 100644 index 0000000..a5f1004 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/samples/netcore3.x/SampleMvcWebApp/appsettings.Development.json b/samples/netcore3.x/SampleMvcWebApp/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/samples/netcore3.x/SampleMvcWebApp/appsettings.json b/samples/netcore3.x/SampleMvcWebApp/appsettings.json new file mode 100644 index 0000000..06439c5 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/appsettings.json @@ -0,0 +1,13 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-SampleMvcWebApp-2FEC7F74-D6B5-4631-999B-A0E62051C3B1;Trusted_Connection=True;MultipleActiveResultSets=true" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/netcore3.x/SampleMvcWebApp/libman.json b/samples/netcore3.x/SampleMvcWebApp/libman.json new file mode 100644 index 0000000..ceee271 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/libman.json @@ -0,0 +1,5 @@ +{ + "version": "1.0", + "defaultProvider": "cdnjs", + "libraries": [] +} \ No newline at end of file diff --git a/samples/netcore3.x/SampleMvcWebApp/wwwroot/css/site.css b/samples/netcore3.x/SampleMvcWebApp/wwwroot/css/site.css new file mode 100644 index 0000000..a87c12c --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/wwwroot/css/site.css @@ -0,0 +1,78 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} + +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} + +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} + +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} + +.bonsai li input[type="checkbox"] { + margin: 5px !important; +} \ No newline at end of file diff --git a/samples/netcore3.x/SampleMvcWebApp/wwwroot/favicon.ico b/samples/netcore3.x/SampleMvcWebApp/wwwroot/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a3a799985c43bc7309d701b2cad129023377dc71 GIT binary patch literal 32038 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 input". +- Remove the need for guid, generatedIdPrefix, specifiedIdPrefix and adding generated ids to list items. diff --git a/samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/LICENSE.txt b/samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/LICENSE.txt new file mode 100644 index 0000000..239d4ed --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Simon Wade + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/README.md b/samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/README.md new file mode 100644 index 0000000..afed610 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/README.md @@ -0,0 +1,108 @@ +# jQuery Bonsai + +#### [Visit project page and demos](http://aexmachina.info/jquery-bonsai) + +`jquery-bonsai` is a lightweight jQuery plugin that takes a big nested list and prunes it down to a small expandable +tree control. + +Also includes support for checkboxes (including 'indeterminate' state) and for populating the tree using a JSON data source. + +See [aexmachina.github.io/jquery-bonsai/](http://aexmachina.github.io/jquery-bonsai/) for more information. + +## Installation + +``` +bower install jquery-bonsai --save +``` + +## Usage + +``` +$('ul#my-nested-list').bonsai(); +``` + +## API + +### `$.fn.bonsai(options)` + +```js +$('#list').bonsai({ + expandAll: false, // expand all items + expand: null, // optional function to expand an item + collapse: null, // optional function to collapse an item + addExpandAll: false, // add a link to expand all items + addSelectAll: false, // add a link to select all checkboxes + selectAllExclude: null, // a filter selector or function for selectAll + idAttribute: 'id', // which attribute of the list items to use as an id + + // createInputs: create checkboxes or radio buttons for each list item + // using a value of "checkbox" or "radio". + // + // The id, name and value for the inputs can be declared in the + // markup using `data-id`, `data-name` and `data-value`. + // + // The name is inherited from parent items if not specified. + // + // Checked state can be indicated using `data-checked`. + createInputs: false, + // checkboxes: run qubit(this.options) on the root node (requires jquery.qubit) + checkboxes: false, + // handleDuplicateCheckboxes: update any other checkboxes that + // have the same value + handleDuplicateCheckboxes: false +}); +``` + +### `Bonsai#update()` + +If the DOM changes then you'll need to call `#update`: + +```js +$('#list').bonsai('update'); +``` + +### `Bonsai#listItem(id)` + +Return a jQuery object containing the `
  • ` with the specified `id`. + +### Expanding/collapsing a single items + +- `Bonsai#expand(listItem)` +- `Bonsai#collapse(listItem)` +- `Bonsai#toggle(listItem)` +- `Bonsai#expandTo(listItem)` + +```js +var bonsai = $('#list').data('bonsai'); +bonsai.expand(listItem); +``` + +All of these methods accept either a DOMElement, a jQuery object or an `id` and return a +jQuery object containing the list item. + +### Expanding/collapsing the whole tree + +- `Bonsai#expandAll(listItem)` +- `Bonsai#collapseAll(listItem)` + +### `Bonsai#serialize()` + +Returns an object representing the expanded/collapsed state of the list, using the items' id +to identify the list items. + +```js +var bonsai = $('#list').data('bonsai'); +var state = bonsai.serialize(); +``` + +### `Bonsai#restore()` + +Restores the expanded/collapsed state of the list using the return value of `#serialize()`. + +```js +var bonsai = $('#list').data('bonsai'); +var state = bonsai.serialize(); +// do stuff that changes the DOM, and may not retain collapsed state +bonsai.update(); // update to handle any new DOM elements +bonsai.restore(state); // restores the collapsed state +``` diff --git a/samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/assets/svg-icons.css b/samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/assets/svg-icons.css new file mode 100644 index 0000000..06f4fb8 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/assets/svg-icons.css @@ -0,0 +1,16 @@ +li.has-children > .thumb { + background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4yLjEsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iNTEycHgiIGhlaWdodD0iNTEycHgiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8cGF0aCBkPSJNMTg0LjcsNDEzLjFsMi4xLTEuOGwxNTYuNS0xMzZjNS4zLTQuNiw4LjYtMTEuNSw4LjYtMTkuMmMwLTcuNy0zLjQtMTQuNi04LjYtMTkuMkwxODcuMSwxMDFsLTIuNi0yLjMNCglDMTgyLDk3LDE3OSw5NiwxNzUuOCw5NmMtOC43LDAtMTUuOCw3LjQtMTUuOCwxNi42aDB2Mjg2LjhoMGMwLDkuMiw3LjEsMTYuNiwxNS44LDE2LjZDMTc5LjEsNDE2LDE4Mi4yLDQxNC45LDE4NC43LDQxMy4xeiIvPg0KPC9zdmc+DQo='); + width: 12px; + height: 12px; + background-size: 100%; + margin-top: 6px; +} +li.has-children.expanded > .thumb { + background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4yLjEsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iNTEycHgiIGhlaWdodD0iNTEycHgiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8cGF0aCBkPSJNOTguOSwxODQuN2wxLjgsMi4xbDEzNiwxNTYuNWM0LjYsNS4zLDExLjUsOC42LDE5LjIsOC42YzcuNywwLDE0LjYtMy40LDE5LjItOC42TDQxMSwxODcuMWwyLjMtMi42DQoJYzEuNy0yLjUsMi43LTUuNSwyLjctOC43YzAtOC43LTcuNC0xNS44LTE2LjYtMTUuOHYwSDExMi42djBjLTkuMiwwLTE2LjYsNy4xLTE2LjYsMTUuOEM5NiwxNzkuMSw5Ny4xLDE4Mi4yLDk4LjksMTg0Ljd6Ii8+DQo8L3N2Zz4NCg=='); +} +li.has-children > .thumb:after { + content: ''; +} +li.has-children.expanded > .thumb:after { + content: ''; +} diff --git a/samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/bower.json b/samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/bower.json new file mode 100644 index 0000000..11c5d8e --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/bower.json @@ -0,0 +1,15 @@ +{ + "name": "jquery-bonsai", + "version": "2.1.1", + "main": "jquery.bonsai.js", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "jquery": "*" + } +} diff --git a/samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/jquery.bonsai.css b/samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/jquery.bonsai.css new file mode 100644 index 0000000..488fef5 --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/jquery.bonsai.css @@ -0,0 +1,42 @@ +@charset "UTF-8"; + +.bonsai, +.bonsai li { + margin: 0; + padding: 0; + list-style: none; + overflow: hidden; +} + +.bonsai li { + position: relative; + padding-left: 1.3em; /* padding for the thumb */ +} + +li .thumb { + margin: -1px 0 0 -1em; /* negative margin into the padding of the li */ + position: absolute; + cursor: pointer; +} + +.bonsai li input[type="radio"], input[type="checkbox"] { + float: left; +} + +li.has-children > .thumb:after { + content: '▸'; +} + +li.has-children.expanded > .thumb:after { + content: '▾'; +} + +li.collapsed > ol.bonsai { + height: 0; + overflow: hidden; +} + +.bonsai .all, +.bonsai .none { + cursor: pointer; +} diff --git a/samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/jquery.bonsai.js b/samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/jquery.bonsai.js new file mode 100644 index 0000000..95867fa --- /dev/null +++ b/samples/netcore3.x/SampleMvcWebApp/wwwroot/js/jquery-bonsai/jquery.bonsai.js @@ -0,0 +1,305 @@ +(function($){ + $.fn.bonsai = function(options) { + var args = arguments; + return this.each(function() { + var bonsai = $(this).data('bonsai'); + if (!bonsai) { + bonsai = new Bonsai(this, options); + $(this).data('bonsai', bonsai); + } + if (typeof options == 'string') { + var method = options; + return bonsai[method].apply(bonsai, [].slice.call(args, 1)); + } + }); + }; + $.bonsai = {}; + $.bonsai.defaults = { + expandAll: false, // expand all items + expand: null, // optional function to expand an item + collapse: null, // optional function to collapse an item + addExpandAll: false, // add a link to expand all items + addSelectAll: false, // add a link to select all checkboxes + selectAllExclude: null, // a filter selector or function for selectAll + idAttribute: 'id', // which attribute of the list items to use as an id + + // createInputs: create checkboxes or radio buttons for each list item + // by setting createInputs to "checkbox" or "radio". + // + // The name and value for the inputs can be declared in the + // markup using `data-name` and `data-value`. + // + // The name is inherited from parent items if not specified. + // + // Checked state can be indicated using `data-checked`. + createInputs: false, + // checkboxes: run qubit(this.options) on the root node (requires jquery.qubit) + checkboxes: false, + // handleDuplicateCheckboxes: update any other checkboxes that + // have the same value + handleDuplicateCheckboxes: false, + // createRadioButtons: creates radio buttons for each list item. + // + // The name and value for the checkboxes can be declared in the + // markup using `data-name` and `data-value`. + // + // The name is inherited from parent items if not specified. + // + // Checked state can be indicated using `data-checked`. + createRadioButtons: false + }; + var Bonsai = function(el, options) { + var self = this; + options = options || {}; + this.options = $.extend({}, $.bonsai.defaults, options); + this.el = $(el).addClass('bonsai').data('bonsai', this); + + // store the scope in the options for child nodes + if (!this.options.scope) { + this.options.scope = this.el; + } + this.update(); + if (this.isRootNode()) { + if (this.options.createCheckboxes) this.createInputs = 'checkbox'; + if (this.options.handleDuplicateCheckboxes) this.handleDuplicateCheckboxes(); + if (this.options.checkboxes) this.el.qubit(this.options); + if (this.options.addExpandAll) this.addExpandAllLink(); + if (this.options.addSelectAll) this.addSelectAllLink(); + this.el.on('click', '.thumb', function(ev) { + self.toggle($(ev.currentTarget).closest('li')); + }); + } + if (this.options.expandAll) this.expandAll(); + }; + Bonsai.prototype = { + isRootNode: function() { + return this.options.scope == this.el; + }, + listItem: function(id) { + if (typeof id === 'object') return $(id); + return this.el.find('[' + this.options.idAttribute + '="' + id + '"]'); + }, + toggle: function(listItem) { + if (!$(listItem).hasClass('expanded')) { + return this.expand(listItem); + } + else { + return this.collapse(listItem); + } + }, + expand: function(listItem) { + return this.setExpanded(listItem, true); + }, + collapse: function(listItem) { + return this.setExpanded(listItem, false); + }, + setExpanded: function(listItem, expanded) { + var $li = this.listItem(listItem); + if ($li.length > 1) { + var self = this; + $li.each(function() { + self.setExpanded(this, expanded); + }); + return; + } + if (expanded) { + if (!$li.data('subList')) return; + $li = $($li).addClass('expanded').removeClass('collapsed'); + $($li.data('subList')).css('height', 'auto'); + } + else { + $li = $($li).addClass('collapsed') + .removeClass('expanded'); + $($li.data('subList')).height(0); + } + return $li; + }, + expandAll: function() { + this.expand(this.el.find('li')); + }, + collapseAll: function() { + this.collapse(this.el.find('li')); + }, + expandTo: function(listItem) { + var self = this; + var $li = this.listItem(listItem); + $li.parents('li').each(function () { + self.expand($(this)); + }); + return $li; + }, + update: function() { + var self = this; + // look for a nested list (if any) + this.el.children().each(function() { + var item = $(this); + if (self.options.createInputs) self.insertInput(item); + + // insert a thumb if it doesn't already exist + if (item.children().filter('.thumb').length == 0) { + var thumb = $('
    '); + item.prepend(thumb); + } + var subLists = item.children().filter('ol, ul'); + item.toggleClass('has-children', subLists.find('li').length > 0); + // if there is a child list + subLists.each(function() { + // that's not empty + if ($('li', this).length == 0) { + return; + } + // then this el has children + item.data('subList', this); + // collapse the nested list + if (item.hasClass('expanded')) { + self.expand(item); + } + else { + self.collapse(item); + } + // handle any deeper nested lists + var exists = !!$(this).data('bonsai'); + $(this).bonsai(exists ? 'update' : self.options); + }); + }); + + this.expand = this.options.expand || this.expand; + this.collapse = this.options.collapse || this.collapse; + }, + serialize: function() { + var idAttr = this.options.idAttribute; + return this.el.find('li').toArray().reduce(function(acc, li) { + var $li = $(li); + var id = $li.attr(idAttr); + // only items with IDs can be serialized + if (id) { + var state = $li.hasClass('expanded') + ? 'expanded' + : ($li.hasClass('collapsed') ? 'collapsed' : null); + if (state) acc[$li.hasClass('expanded') ? 'expanded' : 'collapsed'].push(id); + } + return acc; + }, {expanded: [], collapsed: [], version: 2}); + }, + restore: function(state) { + var self = this; + if (state.version > 1) { + state.expanded.map(this.expand.bind(this)); + state.collapsed.map(this.collapse.bind(this)); + } + else { + Object.keys(state).forEach(function(id) { + self.setExpanded(id, state[id] === 'expanded'); + }); + } + }, + insertInput: function(listItem) { + var type = this.options.createInputs; + if (listItem.find('> input[type=' + type + ']').length) return; + var id = this.inputIdFor(listItem); + var checkbox = $(' ' + ); + var children = listItem.children(); + // get the first text node for the label + var text = listItem.contents().filter(function() { + return this.nodeType == 3; + }).first(); + checkbox.val(listItem.data('value')); + checkbox.prop('checked', listItem.data('checked')) + children.detach(); + listItem.append(checkbox) + .append( + $('