├── .gitattributes ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── AuthorizeSetup ├── AddClaimsToCookie.cs ├── AddPermissionsDataKeyToUserClaims.cs ├── AddPermissionsToUserClaims.cs ├── AuthCookieValidateEverything.cs ├── AuthCookieValidateImpersonation.cs ├── AuthCookieValidatePermissionsDataKey.cs ├── AuthCookieValidatePermissionsOnly.cs ├── AuthCookieValidateRefreshClaims.cs ├── AuthorizeSetup.csproj └── IAuthCookieValidate.cs ├── CommonCache └── CommonCache.csproj ├── DataAuthorize ├── CalcDataKey.cs └── DataAuthorize.csproj ├── DataKeyParts ├── DataAuthConstants.cs ├── DataKeyParts.csproj ├── DbContextExtensions.cs ├── DemoSetupOptions.cs ├── GetClaimsFromUser.cs ├── IDataKey.cs ├── IGetClaimsProvider.cs ├── IShopLevelDataKey.cs ├── NoQueryFilterNeeded.cs └── ShopLevelDataKeyBase.cs ├── DataLayer ├── DataLayer.csproj ├── EfCode │ ├── CombinedDbContext.cs │ ├── CompanyDbContext.cs │ ├── Configurations │ │ └── ConfigExtensions.cs │ └── ExtraAuthorizeDbContext.cs ├── ExtraAuthClasses │ ├── ExtraAuthConstants.cs │ ├── ModulesForUser.cs │ ├── RoleToPermissions.cs │ ├── Support │ │ ├── ChangeExtensions.cs │ │ ├── IAddRemoveEffectsUser.cs │ │ └── IChangeEffectsUser.cs │ ├── TimeStore.cs │ ├── UserDataAccess.cs │ ├── UserDataAccessBase.cs │ ├── UserDataHierarchical.cs │ └── UserToRole.cs └── MultiTenantClasses │ ├── Company.cs │ ├── RetailOutlet.cs │ ├── ShopSale.cs │ ├── ShopStock.cs │ ├── SubGroup.cs │ └── TenantBase.cs ├── FeatureAuthorize ├── CalcFeaturePermissions.cs ├── FeatureAuthorize.csproj ├── PermissionConstants.cs ├── PermissionExtensions.cs └── PolicyCode │ ├── AuthorizationPolicyProvider.cs │ ├── HasPermissionAttribute.cs │ ├── PermissionHandler.cs │ └── PermissionRequirement.cs ├── LICENSE ├── PermissionAccessControl2.sln ├── PermissionAccessControl2.sln.DotSettings ├── PermissionAccessControl2 ├── AddDatabases.cs ├── Areas │ └── Identity │ │ └── Pages │ │ └── _ViewStart.cshtml ├── Controllers │ ├── CacheController.cs │ ├── CompanyController.cs │ ├── FrontEndController.cs │ ├── HomeController.cs │ ├── ImpersonateController.cs │ ├── ShopController.cs │ └── UsersController.cs ├── Data │ ├── ApplicationDbContext.cs │ └── Migrations │ │ ├── 00000000000000_CreateIdentitySchema.Designer.cs │ │ ├── 00000000000000_CreateIdentitySchema.cs │ │ └── ApplicationDbContextModelSnapshot.cs ├── Models │ └── ErrorViewModel.cs ├── PermissionAccessControl2.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── SeedDemo │ ├── Internal │ │ ├── AspNetUserExtension.cs │ │ ├── DemoUsersSetup.cs │ │ ├── HierarchicalSeeder.cs │ │ └── SetupShopStock.cs │ ├── SeedExtensions.cs │ └── SuperAdminSetup.cs ├── Startup.cs ├── Views │ ├── Cache │ │ ├── Index.cshtml │ │ └── ShowUpdateTime.cshtml │ ├── Company │ │ └── Index.cshtml │ ├── Home │ │ ├── Index.cshtml │ │ └── Privacy.cshtml │ ├── Impersonate │ │ ├── Index.cshtml │ │ └── Message.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ ├── _CookieConsentPartial.cshtml │ │ ├── _Layout.cshtml │ │ ├── _LoginPartial.cshtml │ │ └── _ValidationScriptsPartial.cshtml │ ├── Shop │ │ ├── BuySuccess.cshtml │ │ ├── Sales.cshtml │ │ ├── Stock.cshtml │ │ └── Till.cshtml │ ├── Users │ │ ├── AllRoles.cshtml │ │ ├── Index.cshtml │ │ ├── UserPermissions.cshtml │ │ └── Users.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── appsettings.Development.json ├── appsettings.json └── wwwroot │ ├── SeedData │ ├── Companies.txt │ ├── Roles.txt │ ├── ShopStock.txt │ └── Users.json │ ├── css │ └── site.css │ ├── favicon.ico │ ├── js │ └── site.js │ └── lib │ ├── bootstrap │ ├── LICENSE │ └── dist │ │ ├── css │ │ ├── bootstrap-grid.css │ │ ├── bootstrap-grid.css.map │ │ ├── bootstrap-grid.min.css │ │ ├── bootstrap-grid.min.css.map │ │ ├── bootstrap-reboot.css │ │ ├── bootstrap-reboot.css.map │ │ ├── bootstrap-reboot.min.css │ │ ├── bootstrap-reboot.min.css.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ │ └── js │ │ ├── bootstrap.bundle.js │ │ ├── bootstrap.bundle.js.map │ │ ├── bootstrap.bundle.min.js │ │ ├── bootstrap.bundle.min.js.map │ │ ├── bootstrap.js │ │ ├── bootstrap.js.map │ │ ├── bootstrap.min.js │ │ └── bootstrap.min.js.map │ ├── jquery-validation-unobtrusive │ ├── LICENSE.txt │ ├── jquery.validate.unobtrusive.js │ └── jquery.validate.unobtrusive.min.js │ ├── jquery-validation │ ├── LICENSE.md │ └── dist │ │ ├── additional-methods.js │ │ ├── additional-methods.min.js │ │ ├── jquery.validate.js │ │ └── jquery.validate.min.js │ └── jquery │ ├── LICENSE.txt │ └── dist │ ├── jquery.js │ ├── jquery.min.js │ └── jquery.min.map ├── PermissionParts ├── LinkedToModuleAttribute.cs ├── PaidForModules.cs ├── PermissionChecker.cs ├── PermissionDisplay.cs ├── PermissionPackers.cs ├── PermissionParts.csproj └── Permissions.cs ├── README.md ├── RefreshClaimsParts ├── AuthChanges.cs ├── AuthChangesConsts.cs ├── IAuthChanges.cs ├── ITimeStore.cs └── RefreshClaimsParts.csproj ├── ServiceLayer ├── AppStart │ └── StartupExtensions.cs ├── CompanyServices │ ├── Concrete │ │ └── ListCompaniesService.cs │ └── IListCompaniesService.cs ├── ServiceLayer.csproj ├── Shop │ ├── ListSalesDto.cs │ ├── ListStockDto.cs │ ├── SellItemDto.cs │ └── StockSelectDto.cs └── UserServices │ ├── Concrete │ ├── CacheRoleService.cs │ └── ListUsersService.cs │ ├── ExtraAuthUsersSetup.cs │ ├── ICacheRoleService.cs │ ├── IListUsersService.cs │ ├── ListUsersDto.cs │ └── UserExtensions.cs ├── Test ├── DiConfigHelpers │ ├── ConfigureServices.cs │ └── MockHostingEnvironment.cs ├── EfHelpers │ ├── AuthRoleServiceHelpers.cs │ └── CheckEntitiesHelpers.cs ├── FakesAndMocks │ ├── CookieRequestHelpers.cs │ ├── FakeAuthChanges.cs │ ├── FakeGetClaimsProvider.cs │ └── FakeTimeStore.cs ├── Test.csproj ├── TestData │ └── SeedData │ │ ├── Companies.txt │ │ ├── Roles.txt │ │ ├── ShopStock.txt │ │ └── Users.json ├── UnitCommands │ └── DeleteAllUnitTestDatabases.cs ├── UnitTests │ ├── DataAuthorizeTests │ │ ├── TestHierarchicalBuildingChanging.cs │ │ ├── TestHierarchicalFiltering.cs │ │ └── TestShopData.cs │ ├── DataLayerTests │ │ └── TestCombinedDbContext.cs │ ├── FeatureAuthorizeTests │ │ ├── TestAuthChanges.cs │ │ ├── TestCacheRoleService.cs │ │ ├── TestCalcAllowedPermissions.cs │ │ ├── TestExtraAuthorizeDbContext.cs │ │ ├── TestPermissionsChangedDatabase.cs │ │ ├── TestPermissionsChangedFakeDatabase.cs │ │ └── TestTimeStore.cs │ ├── ImpersonateTests │ │ ├── TestImpersonationCookie.cs │ │ └── TestImpersonationHandler.cs │ ├── SecurityChecks │ │ ├── CheckEntitiesAreSecure.cs │ │ └── CheckPermissions.cs │ ├── ShopServices │ │ └── TestShopServices.cs │ └── TestSeedDemo │ │ └── TestSeedExtensions.cs ├── appsettings.json └── demosettings.json └── UserImpersonation ├── AppStart └── StartupExtensions.cs ├── Concrete ├── AuthCookieSignout.cs ├── ImpersonationCookie.cs ├── ImpersonationData.cs ├── ImpersonationHandler.cs └── ImpersonationService.cs ├── IImpersonationService.cs ├── ImpersonateExtensions.cs └── UserImpersonation.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/PermissionAccessControl2/bin/Debug/netcoreapp2.2/PermissionAccessControl2.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/PermissionAccessControl2", 16 | "stopAtEntry": false, 17 | "launchBrowser": { 18 | "enabled": true 19 | }, 20 | "env": { 21 | "ASPNETCORE_ENVIRONMENT": "Development" 22 | }, 23 | "sourceFileMap": { 24 | "/Views": "${workspaceFolder}/Views" 25 | } 26 | }, 27 | { 28 | "name": ".NET Core Attach", 29 | "type": "coreclr", 30 | "request": "attach", 31 | "processId": "${command:pickProcess}" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/PermissionAccessControl2/PermissionAccessControl2.csproj" 11 | ], 12 | "problemMatcher": "$tsc" 13 | }, 14 | { 15 | "label": "publish", 16 | "command": "dotnet", 17 | "type": "process", 18 | "args": [ 19 | "publish", 20 | "${workspaceFolder}/PermissionAccessControl2/PermissionAccessControl2.csproj" 21 | ], 22 | "problemMatcher": "$tsc" 23 | }, 24 | { 25 | "label": "watch", 26 | "command": "dotnet", 27 | "type": "process", 28 | "args": [ 29 | "watch", 30 | "run", 31 | "${workspaceFolder}/PermissionAccessControl2/PermissionAccessControl2.csproj" 32 | ], 33 | "problemMatcher": "$tsc" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /AuthorizeSetup/AddPermissionsDataKeyToUserClaims.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Security.Claims; 5 | using System.Threading.Tasks; 6 | using DataAuthorize; 7 | using DataKeyParts; 8 | using DataLayer.EfCode; 9 | using FeatureAuthorize; 10 | using Microsoft.AspNetCore.Identity; 11 | using Microsoft.Extensions.Options; 12 | 13 | namespace AuthorizeSetup 14 | { 15 | /// 16 | /// This version provides: 17 | /// - Adds Permissions to the user's claims. 18 | /// - Adds DataKey to the user's claims 19 | /// 20 | // Thanks to https://korzh.com/blogs/net-tricks/aspnet-identity-store-user-data-in-claims 21 | public class AddPermissionsDataKeyToUserClaims : UserClaimsPrincipalFactory 22 | { 23 | private readonly ExtraAuthorizeDbContext _extraAuthDbContext; 24 | 25 | public AddPermissionsDataKeyToUserClaims(UserManager userManager, IOptions optionsAccessor, 26 | ExtraAuthorizeDbContext extraAuthDbContext) 27 | : base(userManager, optionsAccessor) 28 | { 29 | _extraAuthDbContext = extraAuthDbContext; 30 | } 31 | 32 | protected override async Task GenerateClaimsAsync(IdentityUser user) 33 | { 34 | var identity = await base.GenerateClaimsAsync(user); 35 | var userId = identity.Claims.GetUserIdFromClaims(); 36 | var rtoPCalcer = new CalcAllowedPermissions(_extraAuthDbContext); 37 | identity.AddClaim(new Claim(PermissionConstants.PackedPermissionClaimType, await rtoPCalcer.CalcPermissionsForUserAsync(userId))); 38 | var dataKeyCalcer = new CalcDataKey(_extraAuthDbContext); 39 | identity.AddClaim(new Claim(DataAuthConstants.HierarchicalKeyClaimName, dataKeyCalcer.CalcDataKeyForUser(userId))); 40 | return identity; 41 | } 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /AuthorizeSetup/AddPermissionsToUserClaims.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Security.Claims; 5 | using System.Threading.Tasks; 6 | using DataAuthorize; 7 | using DataKeyParts; 8 | using DataLayer.EfCode; 9 | using FeatureAuthorize; 10 | using Microsoft.AspNetCore.Identity; 11 | using Microsoft.Extensions.Options; 12 | 13 | namespace AuthorizeSetup 14 | { 15 | /// 16 | /// This version provides: 17 | /// - Adds Permissions to the user's claims. 18 | /// 19 | // Thanks to https://korzh.com/blogs/net-tricks/aspnet-identity-store-user-data-in-claims 20 | public class AddPermissionsToUserClaims : UserClaimsPrincipalFactory 21 | { 22 | private readonly ExtraAuthorizeDbContext _extraAuthDbContext; 23 | 24 | public AddPermissionsToUserClaims(UserManager userManager, IOptions optionsAccessor, 25 | ExtraAuthorizeDbContext extraAuthDbContext) 26 | : base(userManager, optionsAccessor) 27 | { 28 | _extraAuthDbContext = extraAuthDbContext; 29 | } 30 | 31 | protected override async Task GenerateClaimsAsync(IdentityUser user) 32 | { 33 | var identity = await base.GenerateClaimsAsync(user); 34 | var userId = identity.Claims.GetUserIdFromClaims(); 35 | var rtoPCalcer = new CalcAllowedPermissions(_extraAuthDbContext); 36 | identity.AddClaim(new Claim(PermissionConstants.PackedPermissionClaimType, await rtoPCalcer.CalcPermissionsForUserAsync(userId))); 37 | return identity; 38 | } 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /AuthorizeSetup/AuthCookieValidatePermissionsDataKey.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Security.Claims; 7 | using System.Threading.Tasks; 8 | using DataAuthorize; 9 | using DataKeyParts; 10 | using DataLayer.EfCode; 11 | using FeatureAuthorize; 12 | using Microsoft.AspNetCore.Authentication.Cookies; 13 | using Microsoft.Extensions.DependencyInjection; 14 | 15 | namespace AuthorizeSetup 16 | { 17 | /// 18 | /// This version provides: 19 | /// - Adds Permissions to the user's claims. 20 | /// - Adds DataKey to the user's claims 21 | /// 22 | public class AuthCookieValidatePermissionsDataKey : IAuthCookieValidate 23 | { 24 | public async Task ValidateAsync(CookieValidatePrincipalContext context) 25 | { 26 | if (context.Principal.Claims.Any(x => x.Type == PermissionConstants.PackedPermissionClaimType)) 27 | return; 28 | 29 | //No permissions in the claims, so we need to add it. This is only happen once after the user has logged in 30 | var extraContext = context.HttpContext.RequestServices.GetRequiredService(); 31 | var rtoPCalcer = new CalcAllowedPermissions(extraContext); 32 | var dataKeyCalc = new CalcDataKey(extraContext); 33 | 34 | var claims = new List(); 35 | claims.AddRange(context.Principal.Claims); //Copy over existing claims 36 | var userId = context.Principal.Claims.GetUserIdFromClaims(); 37 | //Now calculate the Permissions Claim value and add it 38 | claims.Add(new Claim(PermissionConstants.PackedPermissionClaimType, 39 | await rtoPCalcer.CalcPermissionsForUserAsync(userId))); 40 | //and the same for the DataKey 41 | claims.Add(new Claim(DataAuthConstants.HierarchicalKeyClaimName, 42 | dataKeyCalc.CalcDataKeyForUser(userId))); 43 | 44 | //Build a new ClaimsPrincipal and use it to replace the current ClaimsPrincipal 45 | var identity = new ClaimsIdentity(claims, "Cookie"); 46 | var newPrincipal = new ClaimsPrincipal(identity); 47 | context.ReplacePrincipal(newPrincipal); 48 | //THIS IS IMPORTANT: This updates the cookie, otherwise this calc will be done every HTTP request 49 | context.ShouldRenew = true; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /AuthorizeSetup/AuthCookieValidatePermissionsOnly.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Security.Claims; 7 | using System.Threading.Tasks; 8 | using DataLayer.EfCode; 9 | using FeatureAuthorize; 10 | using Microsoft.AspNetCore.Authentication.Cookies; 11 | using Microsoft.Extensions.DependencyInjection; 12 | 13 | namespace AuthorizeSetup 14 | { 15 | /// 16 | /// This is the very basic version which only provides: 17 | /// - Adds Permissions to the user's claims. 18 | /// - NO DataKey 19 | /// 20 | public class AuthCookieValidatePermissionsOnly : IAuthCookieValidate 21 | { 22 | public async Task ValidateAsync(CookieValidatePrincipalContext context) 23 | { 24 | if (context.Principal.Claims.Any(x => x.Type == PermissionConstants.PackedPermissionClaimType)) 25 | return; 26 | 27 | //No permissions in the claims, so we need to add it. This is only happen once after the user has logged in 28 | var dbContext = context.HttpContext.RequestServices.GetRequiredService(); 29 | var rtoPCalcer = new CalcAllowedPermissions(dbContext); 30 | 31 | var claims = new List(); 32 | claims.AddRange(context.Principal.Claims); //Copy over existing claims 33 | //Now calculate the Permissions Claim value and add it 34 | claims.Add(new Claim(PermissionConstants.PackedPermissionClaimType, 35 | await rtoPCalcer.CalcPermissionsForUserAsync(context.Principal.Claims.GetUserIdFromClaims()))); 36 | 37 | //Build a new ClaimsPrincipal and use it to replace the current ClaimsPrincipal 38 | var identity = new ClaimsIdentity(claims, "Cookie"); 39 | var newPrincipal = new ClaimsPrincipal(identity); 40 | context.ReplacePrincipal(newPrincipal); 41 | //THIS IS IMPORTANT: This updates the cookie, otherwise this calc will be done every HTTP request 42 | context.ShouldRenew = true; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /AuthorizeSetup/AuthorizeSetup.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /AuthorizeSetup/IAuthCookieValidate.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authentication.Cookies; 6 | 7 | namespace AuthorizeSetup 8 | { 9 | public interface IAuthCookieValidate 10 | { 11 | Task ValidateAsync(CookieValidatePrincipalContext context); 12 | } 13 | } -------------------------------------------------------------------------------- /CommonCache/CommonCache.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /DataAuthorize/CalcDataKey.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using DataLayer.EfCode; 7 | 8 | namespace DataAuthorize 9 | { 10 | public class CalcDataKey 11 | { 12 | private readonly ExtraAuthorizeDbContext _context; 13 | 14 | public CalcDataKey(ExtraAuthorizeDbContext context) 15 | { 16 | _context = context; 17 | } 18 | 19 | /// 20 | /// This looks for a DataKey for the current user, which can be missing 21 | /// 22 | /// 23 | /// The found data key, or random guid string to stop it matching anything 24 | public string CalcDataKeyForUser(string userId) 25 | { 26 | return _context.DataAccess.Where(x => x.UserId == userId) 27 | .Select(x => x.LinkedTenant.DataKey).SingleOrDefault() 28 | //If no data key then set to random guid to stop it matching anything 29 | ?? Guid.NewGuid().ToString("N"); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /DataAuthorize/DataAuthorize.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /DataKeyParts/DataAuthConstants.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | namespace DataKeyParts 5 | { 6 | public static class DataAuthConstants 7 | { 8 | public const int HierarchicalKeySize = 64; 9 | public const int AccessKeySize = 64; 10 | 11 | public const string HierarchicalKeyClaimName = "DataKey"; 12 | } 13 | } -------------------------------------------------------------------------------- /DataKeyParts/DataKeyParts.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /DataKeyParts/DbContextExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Linq; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace DataKeyParts 8 | { 9 | public static class DbContextExtensions 10 | { 11 | /// 12 | /// This is called in the overridden SaveChanges in the application's DbContext 13 | /// Its job is to see if a entity has the IShopLevelDataKey interface and set the appropriate key 14 | /// 15 | /// 16 | /// 17 | public static void MarkWithDataKeyIfNeeded(this DbContext context, string accessKey) 18 | { 19 | //at startup access key can be null. The demo setup sets the DataKey directly. 20 | if (accessKey == null) 21 | return; 22 | 23 | foreach (var entityEntry in context.ChangeTracker.Entries() 24 | .Where(e => e.State == EntityState.Added)) 25 | { 26 | if (entityEntry.Entity is IShopLevelDataKey {DataKey: null} hasDataKey) 27 | hasDataKey.SetShopLevelDataKey(accessKey); 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /DataKeyParts/DemoSetupOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | namespace DataKeyParts 5 | { 6 | public enum AuthCookieVersions 7 | { 8 | Off, 9 | //These use UserClaimsPrincipalFactory on login 10 | LoginPermissions, LoginPermissionsDataKey, 11 | //These use The Cookie OnValidatePrincipal event 12 | PermissionsOnly, PermissionsDataKey, Impersonation, RefreshClaims, Everything 13 | } 14 | 15 | public class DemoSetupOptions 16 | { 17 | public string DatabaseSetup { get; set; } 18 | public bool CreateAndSeed { get; set; } 19 | public AuthCookieVersions AuthVersion { get; set; } 20 | } 21 | 22 | 23 | } -------------------------------------------------------------------------------- /DataKeyParts/GetClaimsFromUser.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Linq; 5 | using Microsoft.AspNetCore.Http; 6 | 7 | namespace DataKeyParts 8 | { 9 | public class GetClaimsFromUser : IGetClaimsProvider 10 | { 11 | public GetClaimsFromUser(IHttpContextAccessor accessor) 12 | { 13 | DataKey = accessor.HttpContext?.User.Claims 14 | .SingleOrDefault(x => x.Type == DataAuthConstants.HierarchicalKeyClaimName)?.Value; 15 | } 16 | 17 | public string DataKey { get; } 18 | } 19 | } -------------------------------------------------------------------------------- /DataKeyParts/IDataKey.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | namespace DataKeyParts 5 | { 6 | public interface IDataKey 7 | { 8 | string DataKey { get; } 9 | } 10 | } -------------------------------------------------------------------------------- /DataKeyParts/IGetClaimsProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | namespace DataKeyParts 4 | { 5 | public interface IGetClaimsProvider 6 | { 7 | string DataKey { get; } 8 | } 9 | } -------------------------------------------------------------------------------- /DataKeyParts/IShopLevelDataKey.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | namespace DataKeyParts 5 | { 6 | public interface IShopLevelDataKey : IDataKey 7 | { 8 | //This method is used to set the shop-level classes' DataKey - the TenantBase classes set the property directly. 9 | void SetShopLevelDataKey(string tenantKey); 10 | } 11 | } -------------------------------------------------------------------------------- /DataKeyParts/NoQueryFilterNeeded.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace DataKeyParts 7 | { 8 | /// 9 | /// This is used to mark a database class that doesn't need a query filter. 10 | /// This is only there so that you can unit test that all filters are set up for the classes that do have an access key 11 | /// 12 | public class NoQueryFilterNeeded : Attribute 13 | { 14 | 15 | } 16 | } -------------------------------------------------------------------------------- /DataKeyParts/ShopLevelDataKeyBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | namespace DataKeyParts 8 | { 9 | public class ShopLevelDataKeyBase : IShopLevelDataKey 10 | { 11 | [Required] //This means SQL will throw an error if we don't fill it in 12 | [MaxLength(DataAuthConstants.HierarchicalKeySize)] 13 | public string DataKey { get; private set; } 14 | 15 | //This method is used to set the shop-level classes' DataKey - the TenantBase classes set the property directly. 16 | public void SetShopLevelDataKey(string key) 17 | { 18 | if (key != null && !key.EndsWith("*")) 19 | //The shop key must end in "*" (if null then we assume something else will set the DataKey 20 | throw new ApplicationException("You tried to set a shop-level DataKey but your key didn't end with *"); 21 | 22 | DataKey = key; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /DataLayer/DataLayer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /DataLayer/EfCode/CombinedDbContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using DataLayer.EfCode.Configurations; 5 | using DataLayer.ExtraAuthClasses; 6 | using DataLayer.MultiTenantClasses; 7 | using Microsoft.EntityFrameworkCore; 8 | 9 | namespace DataLayer.EfCode 10 | { 11 | /// 12 | /// This only exists to create one database that covers both ExtraAuthorizeDbContext and CompanyDbContext 13 | /// 14 | public class CombinedDbContext : DbContext 15 | { 16 | //ExtraAuthorizeDbContext 17 | public DbSet UserToRoles { get; set; } 18 | public DbSet RolesToPermissions { get; set; } 19 | public DbSet ModulesForUsers { get; set; } 20 | public DbSet DataAccess { get; set; } 21 | 22 | //CompanyDbContext 23 | public DbSet Tenants { get; set; } 24 | public DbSet ShopStocks { get; set; } 25 | public DbSet ShopSales { get; set; } 26 | 27 | //TimeStore data 28 | public DbSet TimeStores { get; set; } 29 | 30 | public CombinedDbContext(DbContextOptions options) 31 | : base(options) { } 32 | 33 | protected override void OnModelCreating(ModelBuilder modelBuilder) 34 | { 35 | modelBuilder.TenantBaseConfig(); 36 | modelBuilder.ExtraAuthorizeConfig(); 37 | modelBuilder.CompanyDbConfig(null); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /DataLayer/EfCode/CompanyDbContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using DataKeyParts; 8 | using DataLayer.EfCode.Configurations; 9 | using DataLayer.ExtraAuthClasses; 10 | using DataLayer.MultiTenantClasses; 11 | 12 | namespace DataLayer.EfCode 13 | { 14 | public class CompanyDbContext : DbContext 15 | { 16 | internal readonly string DataKey; 17 | 18 | public DbSet Tenants { get; set; } 19 | public DbSet ShopStocks { get; set; } 20 | public DbSet ShopSales { get; set; } 21 | 22 | public DbSet TimeStores { get; set; } 23 | 24 | public CompanyDbContext(DbContextOptions options, IGetClaimsProvider claimsProvider) 25 | : base(options) 26 | { 27 | DataKey = claimsProvider.DataKey; 28 | } 29 | 30 | //I only have to override these two version of SaveChanges, as the other two SaveChanges versions call these 31 | public override int SaveChanges(bool acceptAllChangesOnSuccess) 32 | { 33 | this.MarkWithDataKeyIfNeeded(DataKey); 34 | return base.SaveChanges(acceptAllChangesOnSuccess); 35 | } 36 | 37 | public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, 38 | CancellationToken cancellationToken = default(CancellationToken)) 39 | { 40 | this.MarkWithDataKeyIfNeeded(DataKey); 41 | return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); 42 | } 43 | 44 | protected override void OnModelCreating(ModelBuilder modelBuilder) 45 | { 46 | modelBuilder.TenantBaseConfig(); 47 | modelBuilder.CompanyDbConfig(this); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /DataLayer/EfCode/Configurations/ConfigExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using DataKeyParts; 5 | using DataLayer.ExtraAuthClasses; 6 | using DataLayer.MultiTenantClasses; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 9 | 10 | namespace DataLayer.EfCode.Configurations 11 | { 12 | /// 13 | /// I need to place the configs for the databases in one place because I use context.Database.EnsureCreated to create it. 14 | /// This is only for a demo app - I would normally do this via SQL scripts and EFSchemaCompare 15 | /// https://www.thereformedprogrammer.net/handling-entity-framework-core-database-migrations-in-production-part-1/#2b-hand-coding-sql-migration-scripts 16 | /// 17 | public static class ConfigExtensions 18 | { 19 | public static void TenantBaseConfig(this ModelBuilder modelBuilder) 20 | { 21 | //for some reason ExtraAuthorizeConfig doesn't config this properly so I need to add this 22 | modelBuilder.Entity() 23 | .HasDiscriminator("TenantType") 24 | .HasValue(nameof(Company)) 25 | .HasValue(nameof(SubGroup)) 26 | .HasValue(nameof(RetailOutlet)); 27 | } 28 | 29 | public static void ExtraAuthorizeConfig(this ModelBuilder modelBuilder) 30 | { 31 | modelBuilder.Entity().HasKey(x => new { x.UserId, x.RoleName }); 32 | 33 | modelBuilder.Entity() 34 | .Property("_permissionsInRole") 35 | .HasColumnName("PermissionsInRole"); 36 | } 37 | 38 | public static void CompanyDbConfig(this ModelBuilder modelBuilder, CompanyDbContext context) 39 | { 40 | AddHierarchicalQueryFilter(modelBuilder.Entity(), context); 41 | AddHierarchicalQueryFilter(modelBuilder.Entity(), context); 42 | AddHierarchicalQueryFilter(modelBuilder.Entity(), context); 43 | } 44 | 45 | private static void AddHierarchicalQueryFilter(EntityTypeBuilder builder, CompanyDbContext context) where T : class, IDataKey 46 | { 47 | builder.HasQueryFilter(x => x.DataKey.StartsWith(context.DataKey)); 48 | builder.HasIndex(x => x.DataKey); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /DataLayer/ExtraAuthClasses/ExtraAuthConstants.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | namespace DataLayer.ExtraAuthClasses 4 | { 5 | public static class ExtraAuthConstants 6 | { 7 | public const int UserIdSize = 36; //This is the size of a GUID when returned as a string 8 | 9 | public const int RoleNameSize = 100; 10 | } 11 | } -------------------------------------------------------------------------------- /DataLayer/ExtraAuthClasses/ModulesForUser.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.ComponentModel.DataAnnotations; 6 | using DataLayer.ExtraAuthClasses.Support; 7 | using PermissionParts; 8 | 9 | namespace DataLayer.ExtraAuthClasses 10 | { 11 | /// 12 | /// This holds what modules a user or tenant has 13 | /// 14 | public class ModulesForUser : IChangeEffectsUser, IAddRemoveEffectsUser 15 | { 16 | /// 17 | /// This links modules to a user 18 | /// 19 | /// 20 | /// 21 | public ModulesForUser(string userId, PaidForModules allowedPaidForModules) 22 | { 23 | UserId = userId ?? throw new ArgumentNullException(nameof(userId)); 24 | AllowedPaidForModules = allowedPaidForModules; 25 | } 26 | 27 | [Key] 28 | [MaxLength(ExtraAuthConstants.UserIdSize)] 29 | public string UserId { get; private set; } 30 | 31 | public PaidForModules AllowedPaidForModules { get; private set; } 32 | } 33 | } -------------------------------------------------------------------------------- /DataLayer/ExtraAuthClasses/Support/ChangeExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Linq; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace DataLayer.ExtraAuthClasses.Support 8 | { 9 | public static class ChangeExtensions 10 | { 11 | public static bool UserPermissionsMayHaveChanged(this DbContext context) 12 | { 13 | return context.ChangeTracker.Entries() 14 | .Any(x => (x.Entity is IChangeEffectsUser && x.State == EntityState.Modified) || 15 | (x.Entity is IAddRemoveEffectsUser && 16 | (x.State == EntityState.Added || x.State == EntityState.Deleted))); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /DataLayer/ExtraAuthClasses/Support/IAddRemoveEffectsUser.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | namespace DataLayer.ExtraAuthClasses.Support 5 | { 6 | public interface IAddRemoveEffectsUser 7 | { 8 | 9 | } 10 | } -------------------------------------------------------------------------------- /DataLayer/ExtraAuthClasses/Support/IChangeEffectsUser.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | namespace DataLayer.ExtraAuthClasses.Support 5 | { 6 | public interface IChangeEffectsUser 7 | { 8 | 9 | } 10 | } -------------------------------------------------------------------------------- /DataLayer/ExtraAuthClasses/TimeStore.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using DataKeyParts; 3 | using RefreshClaimsParts; 4 | 5 | namespace DataLayer.ExtraAuthClasses 6 | { 7 | [NoQueryFilterNeeded] 8 | public class TimeStore 9 | { 10 | [Key] 11 | [Required] 12 | [MaxLength(AuthChangesConsts.CacheKeyMaxSize)] 13 | public string Key { get; set; } 14 | 15 | public long LastUpdatedTicks { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /DataLayer/ExtraAuthClasses/UserDataAccess.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.ComponentModel.DataAnnotations; 6 | using DataKeyParts; 7 | 8 | namespace DataLayer.ExtraAuthClasses 9 | { 10 | /// 11 | /// This handles the data access authorization 12 | /// 13 | public class UserDataAccessKey : UserDataAccessBase 14 | { 15 | public UserDataAccessKey(string userId, string accessKey) : base(userId) 16 | { 17 | AccessKey = accessKey; 18 | } 19 | 20 | [MaxLength(DataAuthConstants.AccessKeySize)] 21 | public string AccessKey { get; private set; } 22 | } 23 | } -------------------------------------------------------------------------------- /DataLayer/ExtraAuthClasses/UserDataAccessBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | namespace DataLayer.ExtraAuthClasses 8 | { 9 | /// 10 | /// This handles the data access authorization 11 | /// 12 | public class UserDataAccessBase 13 | { 14 | public UserDataAccessBase(string userId) 15 | { 16 | UserId = userId ?? throw new ArgumentNullException(nameof(userId)); 17 | } 18 | 19 | [Key] 20 | [Required(AllowEmptyStrings = false)] 21 | [MaxLength(ExtraAuthConstants.UserIdSize)] 22 | public string UserId { get; private set; } 23 | } 24 | } -------------------------------------------------------------------------------- /DataLayer/ExtraAuthClasses/UserDataHierarchical.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.ComponentModel.DataAnnotations.Schema; 6 | using DataLayer.MultiTenantClasses; 7 | 8 | namespace DataLayer.ExtraAuthClasses 9 | { 10 | /// 11 | /// This handles a link to a hierarchical class that holds the required DataKey 12 | /// 13 | public class UserDataHierarchical : UserDataAccessBase 14 | { 15 | //This is needed by EF Core 16 | private UserDataHierarchical(string userId, int linkedTenantId) : base(userId) 17 | { 18 | LinkedTenantId = linkedTenantId; 19 | } 20 | 21 | public UserDataHierarchical(string userId, TenantBase linkedTenant) : base(userId) 22 | { 23 | Update(linkedTenant); 24 | } 25 | 26 | public void Update(TenantBase linkedTenant) 27 | { 28 | if (linkedTenant.TenantItemId == 0) 29 | throw new ApplicationException("The linkedTenant must be already in the database."); 30 | LinkedTenant = linkedTenant; 31 | } 32 | 33 | /// 34 | /// This holds the primary key of the Tenant the user is linked to 35 | /// 36 | public int LinkedTenantId { get; private set; } 37 | 38 | [ForeignKey(nameof(LinkedTenantId))] 39 | public TenantBase LinkedTenant { get; private set; } 40 | } 41 | } -------------------------------------------------------------------------------- /DataLayer/ExtraAuthClasses/UserToRole.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.ComponentModel.DataAnnotations; 6 | using System.ComponentModel.DataAnnotations.Schema; 7 | using DataLayer.EfCode; 8 | using DataLayer.ExtraAuthClasses.Support; 9 | using GenericServices; 10 | using StatusGeneric; 11 | 12 | namespace DataLayer.ExtraAuthClasses 13 | { 14 | /// 15 | /// This is a one-to-many relationship between the User (represented by the UserId) and their Roles (represented by RoleToPermissions) 16 | /// 17 | public class UserToRole : IAddRemoveEffectsUser, IChangeEffectsUser 18 | { 19 | private UserToRole() { } //needed by EF Core 20 | 21 | public UserToRole(string userId, RoleToPermissions role) 22 | { 23 | UserId = userId; 24 | Role = role; 25 | } 26 | 27 | //I use a composite key for this table: combination of UserId and RoleName 28 | //That has to be defined by EF Core's fluent API 29 | [Required(AllowEmptyStrings = false)] 30 | [MaxLength(ExtraAuthConstants.UserIdSize)] 31 | public string UserId { get; private set; } 32 | 33 | [Required(AllowEmptyStrings = false)] 34 | [MaxLength(ExtraAuthConstants.RoleNameSize)] 35 | public string RoleName { get; private set; } 36 | 37 | [ForeignKey(nameof(RoleName))] 38 | public RoleToPermissions Role { get; private set; } 39 | 40 | 41 | public static IStatusGeneric AddRoleToUser(string userId, string roleName, ExtraAuthorizeDbContext context) 42 | { 43 | if (roleName == null) throw new ArgumentNullException(nameof(roleName)); 44 | 45 | var status = new StatusGenericHandler(); 46 | if (context.Find(userId, roleName) != null) 47 | { 48 | status.AddError($"The user already has the Role '{roleName}'."); 49 | return status; 50 | } 51 | var roleToAdd = context.Find(roleName); 52 | if (roleToAdd == null) 53 | { 54 | status.AddError($"I could not find the Role '{roleName}'."); 55 | return status; 56 | } 57 | 58 | return status.SetResult(new UserToRole(userId, roleToAdd)); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /DataLayer/MultiTenantClasses/Company.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using DataLayer.EfCode; 5 | using PermissionParts; 6 | 7 | namespace DataLayer.MultiTenantClasses 8 | { 9 | /// 10 | /// This is the root entry for a multi-tenant entry. There should be only one Company per multi-tenant 11 | /// 12 | public class Company : TenantBase 13 | { 14 | private Company(string name) : base(name) { } //Needed by EF Core 15 | 16 | private Company(string name, PaidForModules allowedPaidForModules) : base(name, null) 17 | { 18 | AllowedPaidForModules = allowedPaidForModules; 19 | } 20 | 21 | public static Company AddTenantToDatabaseWithSaveChanges(string name, PaidForModules allowedPaidForModules, 22 | CompanyDbContext context) 23 | { 24 | var newTenant = new Company(name, allowedPaidForModules); 25 | TenantBase.AddTenantToDatabaseWithSaveChanges(newTenant, context); 26 | return newTenant; 27 | } 28 | 29 | /// 30 | /// This holds the modules this company have purchased 31 | /// 32 | public PaidForModules AllowedPaidForModules { get; set; } 33 | } 34 | } -------------------------------------------------------------------------------- /DataLayer/MultiTenantClasses/RetailOutlet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using DataLayer.EfCode; 6 | 7 | namespace DataLayer.MultiTenantClasses 8 | { 9 | public class RetailOutlet : TenantBase 10 | { 11 | private RetailOutlet(string name) : base(name) { } //Needed by EF Core 12 | 13 | private RetailOutlet(string name, TenantBase parent) : base(name, parent) 14 | { 15 | } 16 | 17 | public static RetailOutlet AddTenantToDatabaseWithSaveChanges(string name, TenantBase parent, CompanyDbContext context) 18 | { 19 | var newTenant = new RetailOutlet(name, parent); 20 | TenantBase.AddTenantToDatabaseWithSaveChanges(newTenant, context); 21 | return newTenant; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /DataLayer/MultiTenantClasses/ShopSale.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.ComponentModel.DataAnnotations.Schema; 6 | using DataKeyParts; 7 | using Microsoft.EntityFrameworkCore; 8 | using StatusGeneric; 9 | 10 | namespace DataLayer.MultiTenantClasses 11 | { 12 | public class ShopSale : ShopLevelDataKeyBase 13 | { 14 | private ShopSale() { } //needed by EF Core 15 | 16 | private ShopSale(int numSoldReturned, string returnReason, int shopStockId) 17 | { 18 | if (numSoldReturned == 0) throw new ArgumentException("cannot be zero", nameof(numSoldReturned)); 19 | if (numSoldReturned < 0 && returnReason == null) throw new ArgumentException("cannot be null if its a return", nameof(returnReason)); 20 | if (shopStockId == 0) throw new ArgumentException("cannot be zero", nameof(shopStockId)); 21 | 22 | NumSoldReturned = numSoldReturned; 23 | ReturnReason = returnReason; 24 | ShopStockId = shopStockId; 25 | } 26 | 27 | public int ShopSaleId { get; private set; } 28 | 29 | /// 30 | /// positive number for sale, negative number for return 31 | /// 32 | public int NumSoldReturned { get; private set; } 33 | 34 | /// 35 | /// Will be null if sale 36 | /// 37 | public string ReturnReason { get; private set; } 38 | 39 | //------------------------------------------ 40 | //relationships 41 | 42 | public int ShopStockId { get; private set; } 43 | 44 | [ForeignKey(nameof(ShopStockId))] 45 | public ShopStock StockItem { get; private set; } 46 | 47 | //---------------------------------------------- 48 | //create methods 49 | 50 | /// 51 | /// This creates a Sale entry, and also update the ShopStock number in stock 52 | /// 53 | /// 54 | /// 55 | /// 56 | /// 57 | public static IStatusGeneric CreateSellAndUpdateStock(int numBought, int shopStockId, 58 | DbContext context) 59 | { 60 | if (numBought < 0) throw new ArgumentException("must be positive", nameof(numBought)); 61 | var status = new StatusGenericHandler(); 62 | 63 | var stock = context.Find(shopStockId); 64 | if (stock == null) 65 | { 66 | status.AddError("Could not find the stock item you requested."); 67 | return status; 68 | } 69 | stock.NumInStock = stock.NumInStock - numBought; 70 | if (stock.NumInStock < 0) 71 | { 72 | status.AddError("There are not enough items of that product to sell."); 73 | return status; 74 | } 75 | var sale = new ShopSale(numBought, null, shopStockId); 76 | return status.SetResult(sale); 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /DataLayer/MultiTenantClasses/ShopStock.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using DataKeyParts; 6 | using Microsoft.EntityFrameworkCore.Metadata.Internal; 7 | 8 | namespace DataLayer.MultiTenantClasses 9 | { 10 | /// 11 | /// This contains an item stocked in the shop, and how many they have 12 | /// 13 | public class ShopStock : ShopLevelDataKeyBase 14 | { 15 | public int ShopStockId { get; set; } 16 | public string Name { get; set; } 17 | public decimal RetailPrice { get; set; } 18 | public int NumInStock { get; set; } 19 | 20 | //------------------------------------------ 21 | //relationships 22 | 23 | public int TenantItemId { get; set; } 24 | 25 | [ForeignKey(nameof(TenantItemId))] 26 | public RetailOutlet Shop { get; set; } 27 | 28 | public override string ToString() 29 | { 30 | return $"{nameof(Name)}: {Name}, {nameof(RetailPrice)}: {RetailPrice}, {nameof(NumInStock)}: {NumInStock}"; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /DataLayer/MultiTenantClasses/SubGroup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using DataLayer.EfCode; 5 | 6 | namespace DataLayer.MultiTenantClasses 7 | { 8 | public class SubGroup : TenantBase 9 | { 10 | private SubGroup(string name) : base(name) { } //Needed by EF Core 11 | 12 | private SubGroup(string name, TenantBase parent) : base(name, parent) 13 | { 14 | } 15 | 16 | public static SubGroup AddTenantToDatabaseWithSaveChanges(string name, TenantBase parent, CompanyDbContext context) 17 | { 18 | var newTenant = new SubGroup(name, parent); 19 | TenantBase.AddTenantToDatabaseWithSaveChanges(newTenant, context); 20 | return newTenant; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /FeatureAuthorize/CalcFeaturePermissions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Threading.Tasks; 7 | using DataLayer.EfCode; 8 | using Microsoft.EntityFrameworkCore; 9 | using PermissionParts; 10 | 11 | namespace FeatureAuthorize 12 | { 13 | /// 14 | /// This is the code that calculates what feature permissions a user has 15 | /// 16 | public class CalcAllowedPermissions 17 | { 18 | private readonly ExtraAuthorizeDbContext _context; 19 | 20 | public CalcAllowedPermissions(ExtraAuthorizeDbContext context) 21 | { 22 | _context = context; 23 | } 24 | 25 | /// 26 | /// This is called if the Permissions that a user needs calculating. 27 | /// It looks at what permissions the user has, and then filters out any permissions 28 | /// they aren't allowed because they haven't get access to the module that permission is linked to. 29 | /// 30 | /// 31 | /// a string containing the packed permissions 32 | public async Task CalcPermissionsForUserAsync(string userId) 33 | { 34 | //This gets all the permissions, with a distinct to remove duplicates 35 | var permissionsForUser = (await _context.UserToRoles.Where(x => x.UserId == userId) 36 | .Select(x => x.Role.PermissionsInRole) 37 | .ToListAsync()) 38 | //Because the permissions are packed we have to put these parts of the query after the ToListAsync() 39 | .SelectMany(x => x).Distinct(); 40 | 41 | //we get the modules this user is allowed to see 42 | var userModules = _context.ModulesForUsers.Find(userId) 43 | ?.AllowedPaidForModules ?? PaidForModules.None; 44 | //Now we remove permissions that are linked to modules that the user has no access to 45 | var filteredPermissions = 46 | from permission in permissionsForUser 47 | let moduleAttr = typeof(Permissions).GetMember(permission.ToString())[0] 48 | .GetCustomAttribute() 49 | where moduleAttr == null || userModules.HasFlag(moduleAttr.PaidForModule) 50 | select permission; 51 | 52 | return filteredPermissions.PackPermissionsIntoString(); 53 | } 54 | 55 | } 56 | } -------------------------------------------------------------------------------- /FeatureAuthorize/FeatureAuthorize.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /FeatureAuthorize/PermissionConstants.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | namespace FeatureAuthorize 4 | { 5 | public static class PermissionConstants 6 | { 7 | public const string PackedPermissionClaimType = "Permissions"; 8 | public const string LastPermissionsUpdatedClaimType = "PermissionUpdated"; 9 | } 10 | } -------------------------------------------------------------------------------- /FeatureAuthorize/PermissionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Security.Claims; 7 | using PermissionParts; 8 | 9 | namespace FeatureAuthorize 10 | { 11 | public static class PermissionExtensions 12 | { 13 | /// 14 | /// This returns true if the current user has the permission 15 | /// 16 | /// 17 | /// 18 | /// 19 | public static bool UserHasThisPermission(this ClaimsPrincipal user, Permissions permission) 20 | { 21 | var permissionClaim = 22 | user?.Claims.SingleOrDefault(x => x.Type == PermissionConstants.PackedPermissionClaimType); 23 | return permissionClaim?.Value.UnpackPermissionsFromString().ToArray().UserHasThisPermission(permission) == true; 24 | } 25 | 26 | public static string GetUserIdFromClaims(this IEnumerable claims) 27 | { 28 | return claims?.SingleOrDefault(x => x.Type == ClaimTypes.NameIdentifier)?.Value; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /FeatureAuthorize/PolicyCode/AuthorizationPolicyProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace FeatureAuthorize.PolicyCode 9 | { 10 | //thanks to https://www.jerriepelser.com/blog/creating-dynamic-authorization-policies-aspnet-core/ 11 | //And to GholamReza Rabbal see https://github.com/JonPSmith/PermissionAccessControl/issues/3 12 | 13 | public class AuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider 14 | { 15 | private readonly AuthorizationOptions _options; 16 | 17 | public AuthorizationPolicyProvider(IOptions options) : base(options) 18 | { 19 | _options = options.Value; 20 | } 21 | 22 | public override async Task GetPolicyAsync(string policyName) 23 | { 24 | //Unit tested shows this is quicker (and safer - see link to issue above) than the original version 25 | return await base.GetPolicyAsync(policyName) 26 | ?? new AuthorizationPolicyBuilder() 27 | .AddRequirements(new PermissionRequirement(policyName)) 28 | .Build(); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /FeatureAuthorize/PolicyCode/HasPermissionAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.Authorization; 6 | using PermissionParts; 7 | 8 | namespace FeatureAuthorize.PolicyCode 9 | { 10 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)] 11 | public class HasPermissionAttribute : AuthorizeAttribute 12 | { 13 | public HasPermissionAttribute(Permissions permission) : base(permission.ToString()) 14 | { } 15 | } 16 | } -------------------------------------------------------------------------------- /FeatureAuthorize/PolicyCode/PermissionHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Authorization; 7 | using PermissionParts; 8 | 9 | namespace FeatureAuthorize.PolicyCode 10 | { 11 | //thanks to https://www.jerriepelser.com/blog/creating-dynamic-authorization-policies-aspnet-core/ 12 | 13 | public class PermissionHandler : AuthorizationHandler 14 | { 15 | protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) 16 | { 17 | var permissionsClaim = 18 | context.User.Claims.SingleOrDefault(c => c.Type == PermissionConstants.PackedPermissionClaimType); 19 | // If user does not have the scope claim, get out of here 20 | if (permissionsClaim == null) 21 | return Task.CompletedTask; 22 | 23 | if (permissionsClaim.Value.ThisPermissionIsAllowed(requirement.PermissionName)) 24 | context.Succeed(requirement); 25 | 26 | return Task.CompletedTask; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /FeatureAuthorize/PolicyCode/PermissionRequirement.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.Authorization; 6 | 7 | namespace FeatureAuthorize.PolicyCode 8 | { 9 | public class PermissionRequirement : IAuthorizationRequirement 10 | { 11 | public PermissionRequirement(string permissionName) 12 | { 13 | PermissionName = permissionName ?? throw new ArgumentNullException(nameof(permissionName)); 14 | } 15 | 16 | public string PermissionName { get; } 17 | } 18 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jon P Smith - Selective Analytics Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PermissionAccessControl2.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | Update headers -------------------------------------------------------------------------------- /PermissionAccessControl2/AddDatabases.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using DataLayer.EfCode; 6 | using Microsoft.Data.Sqlite; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using PermissionAccessControl2.Data; 11 | 12 | namespace PermissionAccessControl2 13 | { 14 | /// 15 | /// NOTE: This class is in the ASP.NET application to get a reference to the ApplicationDbContext class 16 | /// 17 | public static class AddDatabases 18 | { 19 | public static void RegisterDatabases(this IServiceCollection services, IConfiguration configuration) 20 | { 21 | var type = configuration["DemoSetup:DatabaseSetup"]; 22 | if (type == "Permanent") 23 | { 24 | //we are dealing with real databases 25 | 26 | services.AddDbContext(options => 27 | options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"))); 28 | 29 | services.AddDbContext(options => 30 | options.UseSqlServer(configuration.GetConnectionString("DemoDatabaseConnection"))); 31 | services.AddDbContext(options => 32 | options.UseSqlServer(configuration.GetConnectionString("DemoDatabaseConnection"))); 33 | services.AddDbContext(options => 34 | options.UseSqlServer(configuration.GetConnectionString("DemoDatabaseConnection"))); 35 | } 36 | else if (type == "InMemory") 37 | { 38 | //we are dealing with in-memory databases 39 | 40 | var aspNetAuthConnection = SetupSqliteInMemoryConnection(); 41 | services.AddDbContext(options => options.UseSqlite(aspNetAuthConnection)); 42 | var appExtraConnection = SetupSqliteInMemoryConnection(); 43 | services.AddDbContext(options => options.UseSqlite(appExtraConnection)); 44 | services.AddDbContext(options => options.UseSqlite(appExtraConnection)); 45 | services.AddDbContext(options => options.UseSqlite(appExtraConnection)); 46 | } 47 | else 48 | { 49 | throw new ApplicationException("This needs a section called 'DemoSetup', with property 'DatabaseSetup' in it"); 50 | } 51 | } 52 | 53 | private static SqliteConnection SetupSqliteInMemoryConnection() 54 | { 55 | var connectionStringBuilder = new SqliteConnectionStringBuilder { DataSource = ":memory:" }; 56 | var connectionString = connectionStringBuilder.ToString(); 57 | var connection = new SqliteConnection(connectionString); 58 | connection.Open(); //see https://github.com/aspnet/EntityFramework/issues/6968 59 | return connection; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /PermissionAccessControl2/Areas/Identity/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "/Views/Shared/_Layout.cshtml"; 3 | } 4 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Controllers/CacheController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.Extensions.Caching.Distributed; 3 | using ServiceLayer.UserServices; 4 | 5 | namespace PermissionAccessControl2.Controllers 6 | { 7 | public class CacheController : Controller 8 | { 9 | private readonly ICacheRoleService _cacheRoleService; 10 | 11 | public CacheController(ICacheRoleService cacheRoleService) 12 | { 13 | _cacheRoleService = cacheRoleService; 14 | } 15 | 16 | public IActionResult Index() 17 | { 18 | return View(_cacheRoleService.ShowExistingCachePermissions(HttpContext.User.Claims)); 19 | } 20 | 21 | public IActionResult Toggle() 22 | { 23 | _cacheRoleService.ToggleCacheRole(); 24 | return RedirectToAction("Index"); 25 | } 26 | 27 | public IActionResult ShowUpdateTime() 28 | { 29 | return View(_cacheRoleService.GetFeatureLastUpdated(HttpContext.User.Claims)); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /PermissionAccessControl2/Controllers/CompanyController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using ServiceLayer.CompanyServices; 7 | using ServiceLayer.CompanyServices.Concrete; 8 | 9 | namespace PermissionAccessControl2.Controllers 10 | { 11 | public class CompanyController : Controller 12 | { 13 | public IActionResult Index([FromServices] IListCompaniesService service) 14 | { 15 | return View(service.BuildViewOfHierarchy()); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /PermissionAccessControl2/Controllers/FrontEndController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using FeatureAuthorize; 4 | using Microsoft.AspNetCore.Mvc; 5 | using PermissionParts; 6 | 7 | namespace PermissionAccessControl2.Controllers 8 | { 9 | [Route("api/[controller]")] 10 | [ApiController] 11 | public class FrontEndController : ControllerBase 12 | { 13 | /// 14 | /// This returns null (HTTP 204 - no content) if no logged in user, otherwise a array of the logged in user's permissions 15 | /// 16 | /// 17 | [HttpGet] 18 | public IEnumerable Get() 19 | { 20 | var packedPermissions = HttpContext.User?.Claims 21 | .SingleOrDefault(x => x.Type == PermissionConstants.PackedPermissionClaimType); 22 | return packedPermissions?.Value.UnpackPermissionsFromString().Select(x => x.ToString()); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using DataKeyParts; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Extensions.Options; 9 | using PermissionAccessControl2.Models; 10 | using ServiceLayer.UserServices; 11 | using ServiceLayer.UserServices.Concrete; 12 | 13 | namespace PermissionAccessControl2.Controllers 14 | { 15 | public class HomeController : Controller 16 | { 17 | public IActionResult Index([FromServices] IOptions demoSetup) 18 | { 19 | return View(demoSetup.Value); 20 | } 21 | 22 | public IActionResult Privacy() 23 | { 24 | return View(); 25 | } 26 | 27 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 28 | public IActionResult Error() 29 | { 30 | return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Controllers/ImpersonateController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using FeatureAuthorize.PolicyCode; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Mvc; 8 | using PermissionParts; 9 | using ServiceLayer.UserServices; 10 | using UserImpersonation; 11 | 12 | namespace PermissionAccessControl2.Controllers 13 | { 14 | public class ImpersonateController : Controller 15 | { 16 | //You would normally protect this with [HasPermission(Permissions.Impersonate)], but I left that off on 17 | public IActionResult Index([FromServices] IListUsersService service) 18 | { 19 | if (User.InImpersonationMode()) 20 | return RedirectToAction(nameof(Message), 21 | new { errorMessage = "You are already in impersonation mode."}); 22 | return View(service.ListUserWithRolesAndDataTenant()); 23 | } 24 | 25 | [HttpPost] 26 | [ValidateAntiForgeryToken] 27 | [HasPermission(Permissions.Impersonate)] 28 | public IActionResult StartNormal(string userId, string userName, [FromServices] IImpersonationService service) 29 | { 30 | var errorMessage = service.StartImpersonation(userId, userName, false); 31 | return RedirectToAction(nameof(Message), 32 | new {errorMessage, successMessage =$"You are now impersonating user {userName} with their permissions." }); 33 | } 34 | 35 | [HttpPost] 36 | [ValidateAntiForgeryToken] 37 | [HasPermission(Permissions.ImpersonateKeepOwnPermissions)] 38 | public IActionResult StartEnhanced(string userId, string userName, [FromServices] IImpersonationService service) 39 | { 40 | var errorMessage = service.StartImpersonation(userId, userName, true); 41 | return RedirectToAction(nameof(Message), 42 | new { errorMessage, successMessage = $"You are now impersonating user {userName} with your own permissions." }); 43 | } 44 | 45 | [Authorize] //you must be logged in 46 | //Note: anyone call call Stop, as when impersonating someone you don't know what permissions (if any) that they have 47 | public IActionResult Stop([FromServices] IImpersonationService service) 48 | { 49 | var errorMessage = service.StopImpersonation(); 50 | return RedirectToAction(nameof(Message), 51 | new { errorMessage, successMessage = $"You have stopped impersonating another user." }); 52 | } 53 | 54 | public IActionResult Message(string errorMessage, string successMessage) 55 | { 56 | return View(new Tuple(errorMessage, successMessage)); 57 | } 58 | 59 | 60 | } 61 | } -------------------------------------------------------------------------------- /PermissionAccessControl2/Controllers/ShopController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using DataLayer.EfCode; 5 | using FeatureAuthorize.PolicyCode; 6 | using GenericServices; 7 | using GenericServices.AspNetCore; 8 | using Microsoft.AspNetCore.Mvc; 9 | using PermissionParts; 10 | using ServiceLayer.Shop; 11 | 12 | namespace PermissionAccessControl2.Controllers 13 | { 14 | public class ShopController : Controller 15 | { 16 | [HttpGet] 17 | [HasPermission(Permissions.SalesSell)] 18 | public IActionResult Till([FromServices] ICrudServices service) 19 | { 20 | var dto = new SellItemDto(); 21 | dto.SetResetDto(service.ReadManyNoTracked().ToList()); 22 | return View(dto); 23 | } 24 | 25 | [HttpPost] 26 | [ValidateAntiForgeryToken] 27 | [HasPermission(Permissions.SalesSell)] 28 | public IActionResult Till([FromServices] ICrudServices service, SellItemDto dto) 29 | { 30 | if (!ModelState.IsValid) 31 | { 32 | dto.SetResetDto(service.ReadManyNoTracked().ToList()); 33 | return View(dto); 34 | } 35 | 36 | var result = service.CreateAndSave(dto); 37 | if (service.IsValid) 38 | return RedirectToAction("BuySuccess", new { message = service.Message, result.ShopSaleId }); 39 | 40 | //Error state 41 | service.CopyErrorsToModelState(ModelState, dto); 42 | dto.SetResetDto(service.ReadManyNoTracked().ToList()); 43 | return View(dto); 44 | } 45 | 46 | public IActionResult BuySuccess([FromServices] ICrudServices service, string message, int shopSaleId) 47 | { 48 | var saleInfo = service.ReadSingle(shopSaleId); 49 | return View(new Tuple(saleInfo, message)); 50 | } 51 | 52 | [HasPermission(Permissions.StockRead)] 53 | public IActionResult Stock([FromServices] ICrudServices service) 54 | { 55 | var allStock = service.ReadManyNoTracked().ToList(); 56 | var allTheSameShop = allStock.Any() && allStock.All(x => x.ShopName == allStock.First().ShopName); 57 | return View(new Tuple, bool>(allStock, allTheSameShop)); 58 | } 59 | 60 | [HasPermission(Permissions.SalesRead)] 61 | public IActionResult Sales([FromServices] ICrudServices service) 62 | { 63 | var allSales = service.ReadManyNoTracked().ToList(); 64 | var allTheSameShop = allSales.Any() && allSales.All(x => x.StockItemShopName == allSales.First().StockItemShopName); 65 | return View(new Tuple, bool>(allSales, allTheSameShop)); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /PermissionAccessControl2/Controllers/UsersController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Linq; 5 | using System.Security.Claims; 6 | using DataLayer.EfCode; 7 | using DataLayer.ExtraAuthClasses; 8 | using FeatureAuthorize; 9 | using GenericServices; 10 | using Microsoft.AspNetCore.Mvc; 11 | using PermissionParts; 12 | using ServiceLayer.UserServices; 13 | 14 | namespace PermissionAccessControl2.Controllers 15 | { 16 | public class UsersController : Controller 17 | { 18 | // GET 19 | public IActionResult Index() 20 | { 21 | return View(HttpContext.User); 22 | } 23 | 24 | public IActionResult Users([FromServices] IListUsersService service) 25 | { 26 | return View(service.ListUserWithRolesAndDataTenant()); 27 | } 28 | 29 | public IActionResult AllRoles([FromServices] ICrudServices services) 30 | { 31 | return View(services.ReadManyNoTracked().ToList()); 32 | } 33 | 34 | public IActionResult UserPermissions() 35 | { 36 | return View(HttpContext.User.Claims.PermissionsFromClaims()); 37 | } 38 | 39 | } 40 | } -------------------------------------------------------------------------------- /PermissionAccessControl2/Data/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace PermissionAccessControl2.Data 8 | { 9 | public class ApplicationDbContext : IdentityDbContext 10 | { 11 | public ApplicationDbContext(DbContextOptions options) 12 | : base(options) 13 | { 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Models/ErrorViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PermissionAccessControl2.Models 4 | { 5 | public class ErrorViewModel 6 | { 7 | public string RequestId { get; set; } 8 | 9 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 10 | } 11 | } -------------------------------------------------------------------------------- /PermissionAccessControl2/PermissionAccessControl2.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | aspnet-PermissionAccessControl2-85B8F4FB-8C2D-4BB7-82AE-49E851E29A20 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | true 36 | $(NoWarn);1591 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using DataKeyParts; 3 | using DataLayer.EfCode; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.Extensions.Options; 8 | using PermissionAccessControl2.Data; 9 | using PermissionAccessControl2.SeedDemo; 10 | 11 | namespace PermissionAccessControl2 12 | { 13 | public class Program 14 | { 15 | public static async Task Main(string[] args) 16 | { 17 | var host = CreateHostBuilder(args).Build(); 18 | //This migrates the database and adds any seed data as required 19 | await SetupDatabasesAndSeedAsync(host); 20 | await host.RunAsync(); 21 | } 22 | 23 | public static IHostBuilder CreateHostBuilder(string[] args) => 24 | Host.CreateDefaultBuilder(args) 25 | .ConfigureWebHostDefaults(webBuilder => 26 | { 27 | webBuilder.UseStartup(); 28 | }); 29 | 30 | 31 | private static async Task SetupDatabasesAndSeedAsync(IHost webHost) 32 | { 33 | using (var scope = webHost.Services.CreateScope()) 34 | { 35 | var services = scope.ServiceProvider; 36 | var demoSetupOptions = services.GetRequiredService>(); 37 | if (demoSetupOptions.Value.CreateAndSeed) 38 | { 39 | //it only creates the databases and seeds them if the DemoSetup:CreateAndSeed property is true 40 | 41 | using (var context = services.GetRequiredService()) 42 | { 43 | context.Database.EnsureCreated(); 44 | } 45 | //This creates a database which is a combination of both the ExtraAuthorizeDbContext and CompanyDbContext 46 | using (var context = services.GetRequiredService()) 47 | { 48 | context.Database.EnsureCreated(); 49 | } 50 | 51 | await webHost.Services.CheckSeedDataAndUserAsync(); 52 | await webHost.Services.CheckAddSuperAdminAsync(); 53 | } 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:65480", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "PermissionAccessControl2": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /PermissionAccessControl2/SeedDemo/Internal/AspNetUserExtension.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using System.Runtime.CompilerServices; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Identity; 9 | 10 | [assembly: InternalsVisibleTo("Test")] 11 | 12 | namespace PermissionAccessControl2.SeedDemo.Internal 13 | { 14 | internal static class AspNetUserExtension 15 | { 16 | /// 17 | /// This will add a user with the given email if they don't all ready exist 18 | /// 19 | /// 20 | /// 21 | /// 22 | /// 23 | public static async Task CheckAddNewUserAsync(this UserManager userManager, string email, string password) 24 | { 25 | var user = await userManager.FindByEmailAsync(email); 26 | if (user != null) 27 | return user; 28 | user = new IdentityUser { UserName = email, Email = email }; 29 | var result = await userManager.CreateAsync(user, password); 30 | if (!result.Succeeded) 31 | { 32 | var errorDescriptions = string.Join("\n", result.Errors.Select(x => x.Description)); 33 | throw new InvalidOperationException( 34 | $"Tried to add user {email}, but failed. Errors:\n {errorDescriptions}"); 35 | } 36 | 37 | return user; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /PermissionAccessControl2/SeedDemo/Internal/DemoUsersSetup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Runtime.CompilerServices; 8 | using System.Threading.Tasks; 9 | using DataLayer.EfCode; 10 | using DataLayer.MultiTenantClasses; 11 | using Microsoft.AspNetCore.Identity; 12 | using Microsoft.EntityFrameworkCore; 13 | using Microsoft.Extensions.DependencyInjection; 14 | using Newtonsoft.Json; 15 | using ServiceLayer.UserServices; 16 | 17 | [assembly: InternalsVisibleTo("Test")] 18 | 19 | namespace PermissionAccessControl2.SeedDemo.Internal 20 | { 21 | /// 22 | /// This is the code that sets up the demo user, with their roles and data keys. 23 | /// It uses the data from in the wwwroot/SeedData/Users.json file 24 | /// ONLY USED FOR DEMO 25 | /// 26 | internal class DemoUsersSetup 27 | { 28 | private readonly UserManager _userManager; 29 | private readonly ExtraAuthorizeDbContext _extraContext; 30 | private readonly ExtraAuthUsersSetup _extraService; 31 | 32 | public DemoUsersSetup(IServiceProvider services) 33 | { 34 | _userManager = services.GetRequiredService>(); 35 | _extraContext = services.GetRequiredService(); 36 | _extraService = new ExtraAuthUsersSetup(_extraContext); 37 | } 38 | 39 | public async Task CheckAddDemoUsersAsync(string usersJson) 40 | { 41 | var allOutlets = _extraContext.Tenants.IgnoreQueryFilters().OfType().ToList(); 42 | foreach (var userSpec in JsonConvert.DeserializeObject>(usersJson)) 43 | { 44 | if (userSpec.LinkedTenant.StartsWith("*")) 45 | { 46 | //We need to form names for outlets 47 | foreach (var retailOutlet in allOutlets.Where(x => x.Name.EndsWith(userSpec.LinkedTenant.Substring(1)))) 48 | { 49 | var email = retailOutlet.Name.Replace(' ', '-') + userSpec.Email.Substring(1); 50 | await CheckAddUser(email, userSpec.RolesCommaDelimited, retailOutlet); 51 | } 52 | } 53 | else 54 | { 55 | var foundTenant = _extraContext.Tenants.IgnoreQueryFilters() 56 | .SingleOrDefault(x => x.Name == userSpec.LinkedTenant); 57 | if (foundTenant == null) 58 | throw new ApplicationException($"Could not find a tenant named {userSpec.LinkedTenant}."); 59 | await CheckAddUser(userSpec.Email, userSpec.RolesCommaDelimited, foundTenant); 60 | } 61 | } 62 | 63 | _extraContext.SaveChanges(); 64 | } 65 | 66 | private async Task CheckAddUser(string email, string rolesCommaDelimited, TenantBase linkedTenant) 67 | { 68 | var user = await _userManager.CheckAddNewUserAsync(email, email); //password is their email 69 | foreach (var roleName in rolesCommaDelimited.Split(',').Select(x => x.Trim())) 70 | { 71 | _extraService.CheckAddRoleToUser(user.Id, roleName); 72 | } 73 | _extraService.AddUpdateDataAccessHierarchical(user.Id, linkedTenant); 74 | _extraService.CheckAddModules(user.Id, linkedTenant); 75 | } 76 | 77 | 78 | private class UserJson 79 | { 80 | public string Email { get; set; } 81 | public string RolesCommaDelimited { get; set; } 82 | public string LinkedTenant { get; set; } 83 | } 84 | 85 | } 86 | } -------------------------------------------------------------------------------- /PermissionAccessControl2/SeedDemo/Internal/HierarchicalSeeder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Runtime.CompilerServices; 7 | using DataLayer.EfCode; 8 | using DataLayer.MultiTenantClasses; 9 | using PermissionParts; 10 | 11 | [assembly: InternalsVisibleTo("Test")] 12 | 13 | namespace PermissionAccessControl2.SeedDemo.Internal 14 | { 15 | /// 16 | /// This is a extension method that converts a given format of text data (see default companyDefinitions) 17 | /// in to a set of tenants with hierarchical links between them. Just used to demo tenant data for display or unit testing 18 | /// ONLY USED FOR DEMO and UNIT TESTING 19 | /// 20 | public static class HierarchicalSeeder 21 | { 22 | public static List AddCompanyAndChildrenInDatabase(this CompanyDbContext context, params string[] companyDefinitions) 23 | { 24 | if (!companyDefinitions.Any()) 25 | companyDefinitions = new[] 26 | { 27 | "4U Inc.|West Coast|San Fran|SF Dress4U, SF Tie4U, SF Shirt4U", 28 | "4U Inc.|West Coast|LA|LA Dress4U, LA Tie4U, LA Shirt4U" 29 | }; 30 | 31 | var companyDict = new Dictionary(); 32 | var subGroupsDict = new Dictionary>(); 33 | foreach (var companyDefinition in companyDefinitions) 34 | { 35 | var hierarchyNames = companyDefinition.Split('|'); 36 | if (!companyDict.ContainsKey(hierarchyNames[0])) 37 | { 38 | companyDict[hierarchyNames[0]] = Company.AddTenantToDatabaseWithSaveChanges( 39 | hierarchyNames[0], PaidForModules.None, context); 40 | subGroupsDict.Clear(); 41 | } 42 | 43 | TenantBase parent = companyDict[hierarchyNames[0]]; 44 | 45 | for (int i = 1; i < hierarchyNames.Length; i++) 46 | { 47 | if (!subGroupsDict.ContainsKey(i)) 48 | { 49 | subGroupsDict[i] = new List(); 50 | } 51 | if (i + 1 == hierarchyNames.Length) 52 | { 53 | //End, which are shops 54 | var shopNames = hierarchyNames[i].Split(',').Select(x => x.Trim()); 55 | foreach (var shopName in shopNames) 56 | { 57 | RetailOutlet.AddTenantToDatabaseWithSaveChanges(shopName, parent, context); 58 | } 59 | } 60 | else 61 | { 62 | //Groups 63 | SubGroup subGroup = null; 64 | if (subGroupsDict[i].Any(x => x.Name == hierarchyNames[i])) 65 | { 66 | subGroup = subGroupsDict[i].Single(x => x.Name == hierarchyNames[i]); 67 | } 68 | else 69 | { 70 | subGroup = SubGroup.AddTenantToDatabaseWithSaveChanges(hierarchyNames[i], parent, context); 71 | subGroupsDict[i].Add(subGroup); 72 | } 73 | parent = subGroup; 74 | } 75 | } 76 | } 77 | 78 | return new List(companyDict.Values); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /PermissionAccessControl2/SeedDemo/Internal/SetupShopStock.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using DataLayer.EfCode; 7 | using DataLayer.MultiTenantClasses; 8 | using Microsoft.EntityFrameworkCore; 9 | 10 | namespace PermissionAccessControl2.SeedDemo.Internal 11 | { 12 | /// 13 | /// This adds stock to the shops using the data given by the wwwroot/SeedData/ShopStock.txt file 14 | /// ONLY USED FOR DEMO 15 | /// 16 | public static class SetupShopStock 17 | { 18 | public static void AddStockToShops(this CompanyDbContext context, string [] lines) 19 | { 20 | foreach (var line in lines) 21 | { 22 | var colonIndex = line.IndexOf(':'); 23 | var shopName = line.Substring(0, colonIndex); 24 | var shop = context.Tenants.IgnoreQueryFilters().OfType() 25 | .SingleOrDefault(x => x.Name == shopName); 26 | if (shop == null) 27 | throw new ApplicationException($"Could not find a shop of name '{shopName}'"); 28 | 29 | var eachStock = from stockAndPrice in line.Substring(colonIndex + 1).Split(',') 30 | let parts = stockAndPrice.Split('|').Select(x => x.Trim()).ToArray() 31 | select new {Name = parts[0], Price = decimal.Parse(parts[1])}; 32 | foreach (var stock in eachStock) 33 | { 34 | var newStock = new ShopStock {Name = stock.Name, NumInStock = 5, RetailPrice = stock.Price, Shop = shop}; 35 | newStock.SetShopLevelDataKey(shop.DataKey); 36 | context.Add(newStock); 37 | } 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /PermissionAccessControl2/SeedDemo/SuperAdminSetup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using DataLayer.EfCode; 9 | using Microsoft.AspNetCore.Identity; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using PermissionAccessControl2.SeedDemo.Internal; 13 | using PermissionParts; 14 | using ServiceLayer.UserServices; 15 | 16 | namespace PermissionAccessControl2.SeedDemo 17 | { 18 | public static class SuperAdminSetup 19 | { 20 | private const string SuperAdminRoleName = "SuperAdmin"; 21 | 22 | /// 23 | /// This ensures there is a SuperAdmin user in the system. 24 | /// It gets the SuperAdmin's email and password from the "SuperAdmin" section of the appsettings.json file 25 | /// NOTE: for security reasons I only allows one user with the RoleName of 26 | /// 27 | /// 28 | /// 29 | public static async Task CheckAddSuperAdminAsync(this IServiceProvider serviceProvider) 30 | { 31 | using (var scope = serviceProvider.CreateScope()) 32 | { 33 | var services = scope.ServiceProvider; 34 | var extraContext = services.GetRequiredService(); 35 | if (extraContext.UserToRoles.Any(x => x.RoleName == SuperAdminRoleName)) 36 | //For security reasons there can only be one user with the SuperAdminRoleName 37 | return; 38 | 39 | var userManager = services.GetRequiredService>(); 40 | 41 | var config = services.GetRequiredService(); 42 | var superSection = config.GetSection("SuperAdmin"); 43 | if (superSection == null) 44 | return; 45 | 46 | var userEmail = superSection["Email"]; 47 | var userPassword = superSection["Password"]; 48 | 49 | var superUser = await userManager.CheckAddNewUserAsync(userEmail, userPassword); 50 | 51 | using (var context = services.GetRequiredService()) 52 | { 53 | var extraService = new ExtraAuthUsersSetup(context); 54 | extraService.AddUpdateRoleToPermissions(SuperAdminRoleName, "SuperAdmin Role", new List{ Permissions.AccessAll}); 55 | extraService.CheckAddRoleToUser(superUser.Id, SuperAdminRoleName); 56 | context.SaveChanges(); 57 | } 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /PermissionAccessControl2/Views/Cache/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model IEnumerable 2 | @{ 3 | ViewData["Title"] = "Index"; 4 | } 5 | 6 |

Dynamic toggling of user's claims

7 | 8 | @if (@Model == null) 9 | { 10 |

Either no one is logged in or UpdateCookieOnChange is set to false in appsettings.json.

11 | } 12 | else 13 | { 14 |
You currently have these cache permissions in your claims
15 |
    16 | @foreach (var claim in @Model) 17 | { 18 |
  • @claim.ToString()
  • 19 | } 20 |
21 | Toggle Cache 22 | } 23 | 24 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Views/Cache/ShowUpdateTime.cshtml: -------------------------------------------------------------------------------- 1 | @model IEnumerable 2 | @{ 3 | ViewData["Title"] = "Index"; 4 | } 5 | 6 |

Show Updated Time

7 | 8 | 9 | 10 |
    11 | @foreach (var message in @Model) 12 | { 13 |
  • @message
  • 14 | } 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Views/Company/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model List 2 | @{ 3 | ViewData["Title"] = "Index"; 4 | } 5 | 6 |

Show tenant hierarchy

7 | 8 | @foreach (var html in @Model) 9 | { 10 | 11 | @html 12 |

 

13 | } 14 | 15 | 16 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model DataKeyParts.DemoSetupOptions 2 | @{ 3 | ViewData["Title"] = "Home Page"; 4 | } 5 | 6 |
7 |

Welcome to example authorization app, version 2

8 |
9 |

10 | This is a demo of a web application using my approach to feature and data authorization in an ASP.NET Core web application. 11 | It is demo of a Multi-tenant service for retail outlets which holds the stock and sales for each shop. 12 | It also allows managers to see what is happening in every shop that they are managing. 13 |

14 |

This list of articles below describe how it works.

15 | 21 | 22 |
23 |
The setup from the appsetting.json DemoSetup section is:
24 |
    25 |
  • @nameof(DataKeyParts.DemoSetupOptions.DatabaseSetup): @Model.DatabaseSetup
  • 26 |
  • @nameof(DataKeyParts.DemoSetupOptions.CreateAndSeed): @Model.CreateAndSeed
  • 27 |
  • @nameof(DataKeyParts.DemoSetupOptions.AuthVersion): @Model.AuthVersion
  • 28 |
29 |
30 | 31 |

How to use use this application

32 |

33 |

    34 |
  1. 35 | Log in using one of the demo users found here. 36 |
      37 |
    • Pick a user that has a SaleManager (good choice!) or SalesAssistant role if you want to sell something.
    • 38 |
    • Pick a user that has a AreaManger if you want to see data for all shops.
    • 39 |
    40 |
  2. 41 |
  3. 42 | Once you have logged in then a menu should have a "Shop" dropdown - pick one of the following. 43 |
      44 |
    • Till to sell something (not present if AreaManager).
    • 45 |
    • Stock to see what stock the shop has.
    • 46 |
    • Sales to see what sales the shop has done (not present if SalesAssistant).
    • 47 |
    48 |
  4. 49 |
  5. When log in you can see your claims, permissions and other user/role ifo via the "Users" menu button.
  6. 50 |
  7. Also look at Company for a list of the hierarchical data.
  8. 51 |
  9. To test refresh of users permissions go to Toggle Role to see this in action. NOTE: UpdateCookieOnChange must be set to true in appsettings.json
  10. 52 |
  11. To test user impersonation feature log in as the SuperAdmin user go to Impersonation->Pick User to see this in action. NOTE: UpdateCookieOnChange must be set to true in appsettings.json
  12. 53 |
54 |

55 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Views/Home/Privacy.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Privacy Policy"; 3 | } 4 |

@ViewData["Title"]

5 | 6 |

This is a demo system and doesn't save your data (unless you set it to do that!). It uses a cookie when you log in, but no tracking.

7 |

This code is under the MIT license.

-------------------------------------------------------------------------------- /PermissionAccessControl2/Views/Impersonate/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model List 2 | @{ 3 | ViewData["Title"] = "Users"; 4 | } 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | @foreach (var user in @Model) 15 | { 16 | 17 | 18 | 19 | 20 | 21 | 37 | 38 | } 39 |
User Name/EmailUser rolesCompanyLinked tenant
@user.UserName@user.RoleNames@user.CompanyName@user.LinkedTenantName 22 |
23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 |
36 |
40 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Views/Impersonate/Message.cshtml: -------------------------------------------------------------------------------- 1 | @model System.Tuple 2 | 3 | @{ 4 | ViewBag.Title = "Message"; 5 | } 6 | 7 |

Message

8 | @if (Model.Item1 != null) 9 | { 10 |

Error: @Model.Item1

11 | } 12 | else 13 | { 14 |

@Model.Item2

15 | } 16 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @model ErrorViewModel 2 | @{ 3 | ViewData["Title"] = "Error"; 4 | } 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (Model.ShowRequestId) 10 | { 11 |

12 | Request ID: @Model.RequestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

18 | Swapping to Development environment will display more detailed information about the error that occurred. 19 |

20 |

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

26 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Views/Shared/_CookieConsentPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Http.Features 2 | 3 | @{ 4 | var consentFeature = Context.Features.Get(); 5 | var showBanner = !consentFeature?.CanTrack ?? false; 6 | var cookieString = consentFeature?.CreateConsentCookie(); 7 | } 8 | 9 | @if (showBanner) 10 | { 11 | 17 | 25 | } 26 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Views/Shared/_LoginPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using UserImpersonation 3 | @inject SignInManager SignInManager 4 | @inject UserManager UserManager 5 | 6 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Views/Shared/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Views/Shop/BuySuccess.cshtml: -------------------------------------------------------------------------------- 1 | @model System.Tuple 2 | 3 | @{ 4 | ViewBag.Title = "title"; 5 | } 6 | 7 |

@Model.Item2

8 | 9 | 10 | 11 | @Html.DisplayFor(e => e.Item1.ShopSaleId) 12 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Views/Shop/Sales.cshtml: -------------------------------------------------------------------------------- 1 | @model Tuple, bool> 2 | @{ 3 | ViewData["Title"] = "Shop Stock"; 4 | } 5 | 6 | @if (!Model.Item1.Any()) 7 | { 8 |

No sales found

9 | } 10 | else 11 | { 12 | var displayThis = Model.Item2 ? "for shop" + Model.Item1.First().StockItemShopName : "(by shop)"; 13 | 14 |

List Stock @displayThis

15 | 16 | 17 | @if (!Model.Item2) 18 | { 19 | 20 | } 21 | 22 | 23 | 24 | 25 | @foreach (var stock in @Model.Item1) 26 | { 27 | 28 | @if (!Model.Item2) 29 | { 30 | 31 | } 32 | 33 | 34 | 35 | 36 | } 37 |
ShopStock nameNum sold/returnedTotal gain/loss
@stock.StockItemShopName@stock.StockItemName@stock.NumSoldReturned@(stock.StockItemRetailPrice * stock.NumSoldReturned)
38 | } -------------------------------------------------------------------------------- /PermissionAccessControl2/Views/Shop/Stock.cshtml: -------------------------------------------------------------------------------- 1 | @model Tuple, bool> 2 | @{ 3 | ViewData["Title"] = "Shop Stock"; 4 | } 5 | 6 | @if (!Model.Item1.Any()) 7 | { 8 |

No stock found

9 | } 10 | else 11 | { 12 | var displayThis = Model.Item2 ? "for shop" + Model.Item1.First().ShopName : "(by shop)"; 13 | 14 |

List Stock @displayThis

15 | 16 | 17 | @if (!Model.Item2) 18 | { 19 | 20 | } 21 | 22 | 23 | 24 | 25 | @foreach (var stock in @Model.Item1) 26 | { 27 | 28 | @if (!Model.Item2) 29 | { 30 | 31 | } 32 | 33 | 34 | 35 | 36 | } 37 |
ShopStock name#In stockRetail price
@stock.ShopName@stock.Name@stock.NumInStock@stock.RetailPrice
38 | } -------------------------------------------------------------------------------- /PermissionAccessControl2/Views/Shop/Till.cshtml: -------------------------------------------------------------------------------- 1 | @model ServiceLayer.Shop.SellItemDto 2 | 3 | @{ 4 | ViewBag.Title = "Till"; 5 | } 6 | 7 |

Shop Sale

8 | 9 |
10 |
11 | 12 | 13 | 14 |
15 | 16 | 20 |
21 | 22 |
23 |
24 | 25 |
26 | 27 |
28 | 29 |
30 |
31 | 32 |
33 |
34 | 35 |
36 |
37 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Views/Users/AllRoles.cshtml: -------------------------------------------------------------------------------- 1 | @model System.Collections.Generic.List 2 | 3 | @{ 4 | ViewBag.Title = "title"; 5 | } 6 | 7 |

All Roles

8 | 9 | 10 | 11 | 14 | 17 | @**@ 18 | 19 | 20 | 21 | @foreach (var item in Model) { 22 | 23 | 26 | 29 | @**@ 34 | 35 | } 36 | 37 |
12 | Role Name 13 | 15 | Permissions 16 |
24 | @Html.DisplayFor(modelItem => item.RoleName) 25 | 27 | @string.Join(", ", item.PermissionsInRole.Select(x => x.ToString())) 28 | 30 | @Html.ActionLink("Edit", "Edit", new { /* id=item.PrimaryKey */ }) | 31 | @Html.ActionLink("Details", "Details", new { /* id=item.PrimaryKey */ }) | 32 | @Html.ActionLink("Delete", "Delete", new { /* id=item.PrimaryKey */ }) 33 |
-------------------------------------------------------------------------------- /PermissionAccessControl2/Views/Users/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model System.Security.Claims.ClaimsPrincipal 2 | @{ 3 | ViewData["Title"] = "User"; 4 | } 5 | 6 |

Current logged in user claims.

7 | 8 | @if (Model?.Identity.IsAuthenticated == true) 9 | { 10 |

User '@User.Identity.Name'

11 |
    12 | 13 | @foreach (var claim in @Model.Claims) 14 | { 15 |
  • @claim.ToString()
  • 16 | } 17 |
18 | } 19 | else 20 | { 21 |

No user is logged in.

22 | } 23 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Views/Users/UserPermissions.cshtml: -------------------------------------------------------------------------------- 1 | @model IEnumerable 2 | @{ 3 | ViewData["Title"] = "User"; 4 | } 5 | 6 |

Current logged in user Permissions.

7 | 8 | @if (User?.Identity.IsAuthenticated == true) 9 | { 10 |

User '@User.Identity.Name'

11 | if (@Model == null) 12 | { 13 |

User has no permissions.

14 | } 15 | else 16 | { 17 |
    18 | @foreach (var claim in @Model) 19 | { 20 |
  • @claim.ToString()
  • 21 | } 22 |
23 | } 24 | } 25 | else 26 | { 27 |

No user is logged in.

28 | } 29 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Views/Users/Users.cshtml: -------------------------------------------------------------------------------- 1 | @model List 2 | @{ 3 | ViewData["Title"] = "Users"; 4 | } 5 | 6 |

List of demo users so you can log in to try the app

7 |

NOTE: The Email address is also the Password!

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | @foreach (var user in @Model) 16 | { 17 | 18 | 19 | 20 | 21 | 22 | 23 | } 24 |
User Name/EmailUser rolesCompanyLinked tenant
@user.UserName@user.RoleNames@user.CompanyName@user.LinkedTenantName
25 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using PermissionAccessControl2 2 | @using PermissionAccessControl2.Models 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /PermissionAccessControl2/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /PermissionAccessControl2/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /PermissionAccessControl2/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=PermissionAccessControl2-AspNetCoreIdentity;Trusted_Connection=True;MultipleActiveResultSets=true", 4 | "DemoDatabaseConnection": "Server=(localdb)\\mssqllocaldb;Database=PermissionAccessControl2-DemoDatabase;Trusted_Connection=True;MultipleActiveResultSets=true" 5 | }, 6 | "Logging": { 7 | "LogLevel": { 8 | "Default": "Warning" 9 | } 10 | }, 11 | "AllowedHosts": "*", 12 | "SuperAdmin": //This holds the information on the superuser. You must have one SuperUser setup otherwise you can't manage users 13 | { 14 | "Email": "Super@g1.com", 15 | "Password": "Super@g1.com" 16 | }, 17 | "DemoSetup": { 18 | "DatabaseSetup": "InMemory", //This can be "InMemory" or "Permanent" (a real database) database. 19 | "CreateAndSeed": true, //If this is true then it will create the dbs and ensure the data is seeded 20 | "AuthVersion": "Everything" //The options are Off, LoginPermissions, LoginPermissionsDataKey, PermissionsOnly, PermissionsDataKey, Impersonation, RefreshClaims, Everything 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /PermissionAccessControl2/wwwroot/SeedData/Companies.txt: -------------------------------------------------------------------------------- 1 | 4U Inc.|West Coast|San Fran|SF Dress4U, SF Tie4U, SF Shirt4U 2 | 4U Inc.|West Coast|LA|LA Dress4U, LA Tie4U, LA Shirt4U 3 | 4U Inc.|East Coast|NY Dress4U, Boston Shirt4U 4 | Pets2 Ltd.|London|Cats Place, Kitten Place 5 | Pets2 Ltd.|Bristol|Dogs Place, Puppy Place -------------------------------------------------------------------------------- /PermissionAccessControl2/wwwroot/SeedData/Roles.txt: -------------------------------------------------------------------------------- 1 | Director: EmployeeRead 2 | AreaManager: StockRead, SalesRead 3 | SalesAssistant: StockRead, SalesRead 4 | StoreManager: StockRead, StockAddNew, StockRemove, SalesRead, SalesSell, SalesReturn 5 | UserAdmin: UserRead, UserChange, RoleRead 6 | CacheRole: Cache1, Cache2 -------------------------------------------------------------------------------- /PermissionAccessControl2/wwwroot/SeedData/ShopStock.txt: -------------------------------------------------------------------------------- 1 | SF Dress4U: Flower dress|50, Tiny dress|22 2 | SF Tie4U: Blue tie|15, Red tie|20, Green tie|10 3 | SF Shirt4U: White shirt|40, Blue shirt|30 4 | LA Dress4U: Smart dress|90, Expensive dress|210 5 | LA Tie4U: Blue tie|15, Red tie|20, Green tie|10 6 | LA Shirt4U: White shirt|40, Blue shirt|30 7 | NY Dress4U: Modern dress|65, Nice dress|30 8 | Boston Shirt4U: White shirt|40, Blue shirt|30 9 | Cats Place: Cat food (large)|40, Cat food (small)|10 10 | Kitten Place: Scratch pole|60, Play mouse|5, Cat food (small)|12 11 | Dogs Place: Dog food (large)|25, Chew bone|13 12 | Puppy Place: Dog lead|7, Dog food (small)|9 -------------------------------------------------------------------------------- /PermissionAccessControl2/wwwroot/SeedData/Users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Email": "dir@4U.com", 4 | "RolesCommaDelimited": "Director,CacheRole", 5 | "LinkedTenant": "4U Inc." 6 | }, 7 | { 8 | "Email": "westCoast@4U.com", 9 | "RolesCommaDelimited": "AreaManager,CacheRole", 10 | "LinkedTenant": "West Coast" 11 | }, 12 | { 13 | "Email": "eastCoast@4U.com", 14 | "RolesCommaDelimited": "AreaManager,CacheRole", 15 | "LinkedTenant": "East Coast" 16 | }, 17 | { 18 | "Email": "*Boss@4U.com", 19 | "RolesCommaDelimited": "StoreManager,CacheRole", 20 | "LinkedTenant": "*4U" 21 | }, 22 | { 23 | "Email": "*Sales@4U.com", 24 | "RolesCommaDelimited": "SalesAssistant,CacheRole", 25 | "LinkedTenant": "*4U" 26 | }, 27 | //Pets2 28 | { 29 | "Email": "dir@Pets2.com", 30 | "RolesCommaDelimited": "Director,CacheRole", 31 | "LinkedTenant": "Pets2 Ltd." 32 | }, 33 | { 34 | "Email": "london@Pets2.com", 35 | "RolesCommaDelimited": "AreaManager,CacheRole", 36 | "LinkedTenant": "London" 37 | }, 38 | { 39 | "Email": "bristol@Pets2.com", 40 | "RolesCommaDelimited": "AreaManager,CacheRole", 41 | "LinkedTenant": "Bristol" 42 | }, 43 | { 44 | "Email": "*Sales@Pets2.com", 45 | "RolesCommaDelimited": "SalesAssistant,CacheRole", 46 | "LinkedTenant": "*Place" 47 | } 48 | ] 49 | -------------------------------------------------------------------------------- /PermissionAccessControl2/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | /* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | for details on configuring this project to bundle and minify static web assets. */ 3 | 4 | a.navbar-brand { 5 | white-space: normal; 6 | text-align: center; 7 | word-break: break-all; 8 | } 9 | 10 | /* Sticky footer styles 11 | -------------------------------------------------- */ 12 | html { 13 | font-size: 14px; 14 | } 15 | @media (min-width: 768px) { 16 | html { 17 | font-size: 16px; 18 | } 19 | } 20 | 21 | .border-top { 22 | border-top: 1px solid #e5e5e5; 23 | } 24 | .border-bottom { 25 | border-bottom: 1px solid #e5e5e5; 26 | } 27 | 28 | .box-shadow { 29 | box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); 30 | } 31 | 32 | button.accept-policy { 33 | font-size: 1rem; 34 | line-height: inherit; 35 | } 36 | 37 | /* Sticky footer styles 38 | -------------------------------------------------- */ 39 | html { 40 | position: relative; 41 | min-height: 100%; 42 | } 43 | 44 | body { 45 | /* Margin bottom by footer height */ 46 | margin-bottom: 60px; 47 | } 48 | .footer { 49 | position: absolute; 50 | bottom: 0; 51 | width: 100%; 52 | white-space: nowrap; 53 | /* Set the fixed height of the footer here */ 54 | height: 60px; 55 | line-height: 60px; /* Vertically center the text there */ 56 | } 57 | -------------------------------------------------------------------------------- /PermissionAccessControl2/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonPSmith/PermissionAccessControl2/c858a932c6c08e94ad36667bca1262ec889c241e/PermissionAccessControl2/wwwroot/favicon.ico -------------------------------------------------------------------------------- /PermissionAccessControl2/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | // for details on configuring this project to bundle and minify static web assets. 3 | 4 | // Write your JavaScript code. 5 | -------------------------------------------------------------------------------- /PermissionAccessControl2/wwwroot/lib/bootstrap/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2018 Twitter, Inc. 4 | Copyright (c) 2011-2018 The Bootstrap Authors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /PermissionAccessControl2/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) .NET Foundation. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | these files except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /PermissionAccessControl2/wwwroot/lib/jquery-validation/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright Jörn Zaefferer 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /PermissionAccessControl2/wwwroot/lib/jquery/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors, https://js.foundation/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules and external directories are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. 37 | -------------------------------------------------------------------------------- /PermissionParts/LinkedToModuleAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace PermissionParts 7 | { 8 | [AttributeUsage(AttributeTargets.Field)] 9 | public class LinkedToModuleAttribute : Attribute 10 | { 11 | public PaidForModules PaidForModule { get; private set; } 12 | 13 | public LinkedToModuleAttribute(PaidForModules paidForModule) 14 | { 15 | PaidForModule = paidForModule; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /PermissionParts/PaidForModules.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace PermissionParts 7 | { 8 | /// 9 | /// This is an example of how you would manage what optional parts of your system a user can access 10 | /// NOTE: You can add Display attributes (as done on Permissions) to give more information about a module 11 | /// 12 | [Flags] 13 | public enum PaidForModules : long 14 | { 15 | None = 0, 16 | Feature1 = 1, 17 | Feature2 = 2, 18 | Feature3 = 4 19 | } 20 | } -------------------------------------------------------------------------------- /PermissionParts/PermissionChecker.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.ComponentModel; 7 | using System.Linq; 8 | 9 | namespace PermissionParts 10 | { 11 | public static class PermissionChecker 12 | { 13 | /// 14 | /// This is used by the policy provider to check the permission name string 15 | /// 16 | /// 17 | /// 18 | /// 19 | public static bool ThisPermissionIsAllowed(this string packedPermissions, string permissionName) 20 | { 21 | var usersPermissions = packedPermissions.UnpackPermissionsFromString().ToArray(); 22 | 23 | if (!Enum.TryParse(permissionName, true, out Permissions permissionToCheck)) 24 | throw new InvalidEnumArgumentException($"{permissionName} could not be converted to a {nameof(Permissions)}."); 25 | 26 | return usersPermissions.UserHasThisPermission(permissionToCheck); 27 | } 28 | 29 | /// 30 | /// This is the main checker of whether a user permissions allows them to access something with the given permission 31 | /// 32 | /// 33 | /// 34 | /// 35 | public static bool UserHasThisPermission(this Permissions[] usersPermissions, Permissions permissionToCheck) 36 | { 37 | return usersPermissions.Contains(permissionToCheck) || usersPermissions.Contains(Permissions.AccessAll); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /PermissionParts/PermissionDisplay.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.ComponentModel.DataAnnotations; 7 | using System.Reflection; 8 | 9 | namespace PermissionParts 10 | { 11 | public class PermissionDisplay 12 | { 13 | public PermissionDisplay(string groupName, string name, string description, Permissions permission, 14 | string moduleName) 15 | { 16 | Permission = permission; 17 | GroupName = groupName; 18 | ShortName = name ?? throw new ArgumentNullException(nameof(name)); 19 | Description = description ?? throw new ArgumentNullException(nameof(description)); 20 | ModuleName = moduleName; 21 | } 22 | 23 | /// 24 | /// GroupName, which groups permissions working in the same area 25 | /// 26 | public string GroupName { get; private set; } 27 | /// 28 | /// ShortName of the permission - often says what it does, e.g. Read 29 | /// 30 | public string ShortName { get; private set; } 31 | /// 32 | /// Long description of what action this permission allows 33 | /// 34 | public string Description { get; private set; } 35 | /// 36 | /// Gives the actual permission 37 | /// 38 | public Permissions Permission { get; private set; } 39 | /// 40 | /// Contains an optional paidForModule that this feature is linked to 41 | /// 42 | public string ModuleName { get; private set; } 43 | 44 | 45 | /// 46 | /// This returns 47 | /// 48 | /// 49 | public static List GetPermissionsToDisplay(Type enumType) 50 | { 51 | var result = new List(); 52 | foreach (var permissionName in Enum.GetNames(enumType)) 53 | { 54 | var member = enumType.GetMember(permissionName); 55 | //This allows you to obsolete a permission and it won't be shown as a possible option, but is still there so you won't reuse the number 56 | var obsoleteAttribute = member[0].GetCustomAttribute(); 57 | if (obsoleteAttribute != null) 58 | continue; 59 | //If there is no DisplayAttribute then the Enum is not used 60 | var displayAttribute = member[0].GetCustomAttribute(); 61 | if (displayAttribute == null) 62 | continue; 63 | 64 | //Gets the optional PaidForModule that a permission can be linked to 65 | var moduleAttribute = member[0].GetCustomAttribute(); 66 | 67 | var permission = (Permissions)Enum.Parse(enumType, permissionName, false); 68 | 69 | result.Add(new PermissionDisplay(displayAttribute.GroupName, displayAttribute.Name, 70 | displayAttribute.Description, permission, moduleAttribute?.PaidForModule.ToString())); 71 | } 72 | 73 | return result; 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /PermissionParts/PermissionPackers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Globalization; 7 | using System.Linq; 8 | 9 | namespace PermissionParts 10 | { 11 | public static class PermissionPackers 12 | { 13 | public static string PackPermissionsIntoString(this IEnumerable permissions) 14 | { 15 | return permissions.Aggregate("", (s, permission) => s + (char)permission); 16 | } 17 | 18 | public static IEnumerable UnpackPermissionsFromString(this string packedPermissions) 19 | { 20 | if (packedPermissions == null) 21 | throw new ArgumentNullException(nameof(packedPermissions)); 22 | foreach (var character in packedPermissions) 23 | { 24 | yield return ((Permissions) character); 25 | } 26 | } 27 | 28 | public static Permissions? FindPermissionViaName(this string permissionName) 29 | { 30 | return Enum.TryParse(permissionName, out Permissions permission) 31 | ? (Permissions?) permission 32 | : null; 33 | } 34 | 35 | } 36 | } -------------------------------------------------------------------------------- /PermissionParts/PermissionParts.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /PermissionParts/Permissions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | namespace PermissionParts 8 | { 9 | public enum Permissions : short 10 | { 11 | NotSet = 0, //error condition 12 | 13 | //Here is an example of very detailed control over something 14 | [Display(GroupName = "Stock", Name = "Read", Description = "Can read stock")] 15 | StockRead = 10, 16 | [Display(GroupName = "Stock", Name = "Add new", Description = "Can add a new stock item")] 17 | StockAddNew = 13, 18 | [Display(GroupName = "Stock", Name = "Remove", Description = "Can read sales data")] 19 | StockRemove = 14, 20 | 21 | [Display(GroupName = "Sales", Name = "Read", Description = "Can delete a stock item")] 22 | SalesRead = 20, 23 | [Display(GroupName = "Sales", Name = "Sell", Description = "Can sell items from stock")] 24 | SalesSell = 21, 25 | [Display(GroupName = "Sales", Name = "Return", Description = "Can return an item to stock")] 26 | SalesReturn = 22, 27 | 28 | [Display(GroupName = "Employees", Name = "Read", Description = "Can read company employees")] 29 | EmployeeRead = 30, 30 | 31 | [Display(GroupName = "UserAdmin", Name = "Read users", Description = "Can list User")] 32 | UserRead = 40, 33 | //This is an example of grouping multiple actions under one permission 34 | [Display(GroupName = "UserAdmin", Name = "Alter user", Description = "Can do anything to the User")] 35 | UserChange = 41, 36 | 37 | [Display(GroupName = "UserAdmin", Name = "Read Roles", Description = "Can list Role")] 38 | RoleRead = 50, 39 | [Display(GroupName = "UserAdmin", Name = "Change Role", Description = "Can create, update or delete a Role")] 40 | RoleChange = 51, 41 | 42 | [Display(GroupName = "CacheTest", Name = "Cache1", Description = "Base permission to update permission test")] 43 | Cache1 = 60, 44 | [Display(GroupName = "CacheTest", Name = "Cache2", Description = "Permission to toggle for update permission test")] 45 | Cache2 = 61, 46 | 47 | [Display(GroupName = "Impersonation", Name = "Impersonate - straight", Description = "Impersonate user using their permissions")] 48 | Impersonate = 70, 49 | [Display(GroupName = "Impersonation", Name = "Impersonate - enhanced", Description = "Impersonate user using current permissions")] 50 | ImpersonateKeepOwnPermissions = 71, 51 | 52 | //This is an example of what to do with permission you don't used anymore. 53 | //You don't want its number to be reused as it could cause problems 54 | //Just mark it as obsolete and the PermissionDisplay code won't show it 55 | [Obsolete] 56 | [Display(GroupName = "Old", Name = "Not used", Description = "example of old permission")] 57 | OldPermissionNotUsed = 100, 58 | 59 | //This is an example of a permission linked to a optional (paid for?) feature 60 | //The code that turns roles to permissions can 61 | //remove this permission if the user isn't allowed to access this feature 62 | [LinkedToModule(PaidForModules.Feature1)] 63 | [Display(GroupName = "Features", Name = "Feature1", Description = "Can access feature1")] 64 | Feature1Access = 1000, 65 | [LinkedToModule(PaidForModules.Feature2)] 66 | [Display(GroupName = "Features", Name = "Feature2", Description = "Can access feature2")] 67 | Feature2Access = 1001, 68 | 69 | [Display(GroupName = "SuperAdmin", Name = "AccessAll", Description = "This allows the user to access every feature")] 70 | AccessAll = Int16.MaxValue, 71 | } 72 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonPSmith/PermissionAccessControl2/c858a932c6c08e94ad36667bca1262ec889c241e/README.md -------------------------------------------------------------------------------- /RefreshClaimsParts/AuthChanges.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Runtime.CompilerServices; 6 | 7 | [assembly: InternalsVisibleTo("Test")] 8 | 9 | namespace RefreshClaimsParts 10 | { 11 | public class AuthChanges : IAuthChanges 12 | { 13 | /// 14 | /// This returns true if ticksToCompareString is null, or if its value is lower than the value in the TimeStore 15 | /// 16 | /// 17 | /// 18 | /// 19 | /// 20 | public bool IsOutOfDateOrMissing(string cacheKey, string ticksToCompareString, ITimeStore timeStore) 21 | { 22 | if (ticksToCompareString == null) 23 | //if there is no time claim then you do need to reset the claims 24 | return true; 25 | 26 | var ticksToCompare = long.Parse(ticksToCompareString); 27 | return IsOutOfDate(cacheKey, ticksToCompare, timeStore); 28 | } 29 | 30 | private bool IsOutOfDate(string cacheKey, long ticksToCompare, ITimeStore timeStore) 31 | { 32 | var cachedTicks = timeStore.GetValueFromStore(cacheKey); 33 | if (cachedTicks == null) 34 | throw new ApplicationException( 35 | $"You must seed the database with a cache value for the key {cacheKey}."); 36 | 37 | return ticksToCompare < cachedTicks; 38 | } 39 | 40 | public void AddOrUpdate(ITimeStore timeStore) 41 | { 42 | timeStore.AddUpdateValue(AuthChangesConsts.FeatureCacheKey, DateTime.UtcNow.Ticks); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /RefreshClaimsParts/AuthChangesConsts.cs: -------------------------------------------------------------------------------- 1 | namespace RefreshClaimsParts 2 | { 3 | public static class AuthChangesConsts 4 | { 5 | public const int CacheKeyMaxSize = 36; 6 | 7 | public const string FeatureCacheKey = "8FA8E7A8-0ADD-433C-B063-3BD725254C9B"; 8 | } 9 | } -------------------------------------------------------------------------------- /RefreshClaimsParts/IAuthChanges.cs: -------------------------------------------------------------------------------- 1 | namespace RefreshClaimsParts 2 | { 3 | public interface IAuthChanges 4 | { 5 | /// 6 | /// This returns true if there is no ticksToCompare or the ticksToCompare is earlier than the AuthLastUpdated time 7 | /// 8 | /// 9 | /// 10 | /// Link to the DbContext that managers the cache store 11 | /// 12 | bool IsOutOfDateOrMissing(string cacheKey, string ticksToCompareString, ITimeStore timeStore); 13 | 14 | /// 15 | /// This adds or updates the TimeStore entry with the cacheKey with the cachedValue (datetime as ticks) 16 | /// 17 | /// 18 | void AddOrUpdate(ITimeStore timeStore); 19 | } 20 | } -------------------------------------------------------------------------------- /RefreshClaimsParts/ITimeStore.cs: -------------------------------------------------------------------------------- 1 | namespace RefreshClaimsParts 2 | { 3 | /// 4 | /// Access to the TimeStore part of the ExtraAuthorizeDbContext 5 | /// 6 | public interface ITimeStore 7 | { 8 | /// 9 | /// This reads the TimeStore entry with the given key. 10 | /// 11 | /// the cache key 12 | /// DateTime ticks value, or null if not set. 13 | long? GetValueFromStore(string key); 14 | 15 | /// 16 | /// This adds or updates the TimeStore entry defined by the key 17 | /// 18 | /// the cache key 19 | /// the new DateTime ticks value 20 | void AddUpdateValue(string key, long ticks); 21 | } 22 | } -------------------------------------------------------------------------------- /RefreshClaimsParts/RefreshClaimsParts.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ServiceLayer/AppStart/StartupExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Reflection; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using NetCore.AutoRegisterDi; 7 | 8 | namespace ServiceLayer.AppStart 9 | { 10 | public static class StartupExtensions 11 | { 12 | public static void ServiceLayerRegister(this IServiceCollection services) 13 | { 14 | //This registers the classes in the current assembly that end in "Service" and have a public interface 15 | services.RegisterAssemblyPublicNonGenericClasses(Assembly.GetExecutingAssembly()) 16 | .Where(c => c.Name.EndsWith("Service")) 17 | .AsPublicImplementedInterfaces(); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /ServiceLayer/CompanyServices/Concrete/ListCompaniesService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using DataLayer.EfCode; 8 | using DataLayer.MultiTenantClasses; 9 | using Microsoft.AspNetCore.Html; 10 | using Microsoft.EntityFrameworkCore; 11 | 12 | namespace ServiceLayer.CompanyServices.Concrete 13 | { 14 | /// 15 | /// This displays a list of the Company hierarchies in a simple way 16 | /// ONLY USED FOR DEMO 17 | /// 18 | public class ListCompaniesService : IListCompaniesService 19 | { 20 | private readonly CompanyDbContext _context; 21 | 22 | public ListCompaniesService(CompanyDbContext context) 23 | { 24 | _context = context; 25 | } 26 | 27 | public List BuildViewOfHierarchy() 28 | { 29 | var result = new List(); 30 | foreach (var company in _context.Tenants.IgnoreQueryFilters().OfType() 31 | .Include(x => x.Children) 32 | .ThenInclude(x => x.Children) 33 | .ThenInclude(x => x.Children) 34 | .ThenInclude(x => x.Children) 35 | .ThenInclude(x => x.Children)) 36 | { 37 | var sb = new StringBuilder(""); 38 | sb.Append(HtmlDisplayTenant(company, 0)); 39 | ShowChildren(company.Children, sb, 1); 40 | sb.Append("
"); 41 | result.Add(new HtmlString(sb.ToString())); 42 | } 43 | 44 | return result; 45 | } 46 | 47 | private void ShowChildren(IEnumerable children, StringBuilder sb, int indent) 48 | { 49 | foreach (var tenant in children) 50 | { 51 | sb.Append(HtmlDisplayTenant(tenant, indent)); 52 | if (tenant.Children.Any()) 53 | ShowChildren(tenant.Children, sb, indent+1); 54 | } 55 | } 56 | 57 | private string HtmlDisplayTenant(TenantBase tenant, int indent) 58 | { 59 | var result = ""; 60 | for (int i = 0; i < indent; i++) 61 | { 62 | result += ""; 63 | } 64 | result += $"{tenant.GetType().Name}" + 65 | $"{tenant.Name}" + 66 | $"DataKey = {tenant.DataKey}" + 67 | ""; 68 | 69 | return result; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /ServiceLayer/CompanyServices/IListCompaniesService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using Microsoft.AspNetCore.Html; 6 | 7 | namespace ServiceLayer.CompanyServices 8 | { 9 | public interface IListCompaniesService 10 | { 11 | List BuildViewOfHierarchy(); 12 | } 13 | } -------------------------------------------------------------------------------- /ServiceLayer/ServiceLayer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /ServiceLayer/Shop/ListSalesDto.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using DataLayer.MultiTenantClasses; 5 | using GenericServices; 6 | 7 | namespace ServiceLayer.Shop 8 | { 9 | public class ListSalesDto : ILinkToEntity 10 | { 11 | public int ShopSaleId { get; set; } 12 | 13 | public string StockItemName { get; set; } 14 | 15 | public decimal StockItemRetailPrice { get; set; } 16 | 17 | /// 18 | /// positive number for sale, negative number for return 19 | /// 20 | public int NumSoldReturned { get; set; } 21 | 22 | /// 23 | /// Will be null if sale 24 | /// 25 | public string ReturnReason { get; set; } 26 | 27 | public string StockItemShopName { get; set; } 28 | } 29 | } -------------------------------------------------------------------------------- /ServiceLayer/Shop/ListStockDto.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using DataLayer.MultiTenantClasses; 5 | using GenericServices; 6 | 7 | namespace ServiceLayer.Shop 8 | { 9 | public class ListStockDto : ILinkToEntity 10 | { 11 | public int ShopStockId { get; set; } 12 | public string Name { get; set; } 13 | public decimal RetailPrice { get; set; } 14 | public int NumInStock { get; set; } 15 | 16 | public string ShopName { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /ServiceLayer/Shop/SellItemDto.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using DataLayer.MultiTenantClasses; 7 | using GenericServices; 8 | 9 | namespace ServiceLayer.Shop 10 | { 11 | public class SellItemDto : ILinkToEntity 12 | { 13 | public List DropdownData { get; set; } 14 | 15 | public int ShopStockId { get; set; } 16 | 17 | public int NumBought { get; set; } = 1; 18 | 19 | public int TenantItemId { get; set; } 20 | 21 | /// 22 | /// This holds the PK of the created ShopSale 23 | /// 24 | public int ShopSaleId { get; set; } 25 | 26 | public void SetResetDto(List stockList) 27 | { 28 | DropdownData = stockList; 29 | TenantItemId = stockList.FirstOrDefault()?.TenantItemId ?? 0; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /ServiceLayer/Shop/StockSelectDto.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using DataLayer.MultiTenantClasses; 5 | using GenericServices; 6 | 7 | namespace ServiceLayer.Shop 8 | { 9 | public class StockSelectDto : ILinkToEntity 10 | { 11 | public int ShopStockId { get; set; } 12 | public string Name { get; set; } 13 | public decimal RetailPrice { get; set; } 14 | public int NumInStock { get; set; } 15 | 16 | public int TenantItemId { get; set; } 17 | 18 | public string DisplayText => $"{Name}, {RetailPrice:C} ({NumInStock} left)"; 19 | } 20 | } -------------------------------------------------------------------------------- /ServiceLayer/UserServices/Concrete/CacheRoleService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Security.Claims; 5 | using DataLayer.EfCode; 6 | using DataLayer.ExtraAuthClasses; 7 | using FeatureAuthorize; 8 | using PermissionParts; 9 | using RefreshClaimsParts; 10 | 11 | namespace ServiceLayer.UserServices.Concrete 12 | { 13 | /// 14 | /// This is used to test that the dynamic update of a logged-in user happens if the UpdateCookieOnChange is true. 15 | /// It does this by changing the permissions in the Role called , 16 | /// which always has the permission , and can have permission 17 | /// 18 | public class CacheRoleService : ICacheRoleService 19 | { 20 | //NOTE: If you change this you need to change the Roles.txt file in wwwroot/SeedData 21 | public const string CacheRoleName = "CacheRole"; 22 | 23 | private readonly ExtraAuthorizeDbContext _context; 24 | 25 | public CacheRoleService(ExtraAuthorizeDbContext context) 26 | { 27 | _context = context; 28 | } 29 | 30 | /// 31 | /// This just shows the user's permissions which start with the string "Cache" 32 | /// 33 | /// 34 | /// 35 | public IEnumerable ShowExistingCachePermissions(IEnumerable usersClaims) 36 | { 37 | return usersClaims.PermissionsFromClaims()?.Where(x => x.ToString().StartsWith("Cache")); 38 | } 39 | 40 | /// 41 | /// This toggles whether the permission is in the . 42 | /// This causes the to update the TimeStore with the time this change happens. 43 | /// Then the will compare the users lastUpdated time which will 44 | /// cause a recalc of the logged-in user's permission claim. 45 | /// 46 | public void ToggleCacheRole() 47 | { 48 | var hasCache2Permission = _context.Find(CacheRoleName) 49 | .PermissionsInRole.Any(x => x == Permissions.Cache2); 50 | var updatedPermissions = new List {Permissions.Cache1}; 51 | if (!hasCache2Permission) 52 | updatedPermissions.Add(Permissions.Cache2); 53 | 54 | var authUserHelper = new ExtraAuthUsersSetup(_context); 55 | authUserHelper.UpdateRole(CacheRoleName, $"Has {updatedPermissions.Count} permissions.", updatedPermissions); 56 | _context.SaveChanges(); 57 | } 58 | 59 | /// 60 | /// This allows us to see the time the last times 61 | /// 1. The last time the Role/Permissions were changes (database value) 62 | /// 2. The last time the current user's permissions were calculated (user claim) 63 | /// 64 | /// 65 | /// 66 | public IEnumerable GetFeatureLastUpdated(IEnumerable usersClaims) 67 | { 68 | var databaseValue = _context.Find(AuthChangesConsts.FeatureCacheKey)?.LastUpdatedTicks; 69 | yield return databaseValue == null 70 | ? "No database value present" 71 | : $"Database: {new DateTime((long)databaseValue):F}"; 72 | 73 | var claimsValue = usersClaims 74 | .SingleOrDefault(x => x.Type == PermissionConstants.LastPermissionsUpdatedClaimType)?.Value; 75 | yield return claimsValue == null 76 | ? "No claim value present" 77 | : $"User Claim: {new DateTime(long.Parse(claimsValue)):F}"; 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /ServiceLayer/UserServices/Concrete/ListUsersService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using DataLayer.EfCode; 7 | using DataLayer.ExtraAuthClasses; 8 | using DataLayer.MultiTenantClasses; 9 | using Microsoft.AspNetCore.Identity; 10 | 11 | namespace ServiceLayer.UserServices.Concrete 12 | { 13 | public class ListUsersService : IListUsersService 14 | { 15 | private readonly UserManager _userManager; 16 | private readonly ExtraAuthorizeDbContext _extraContext; 17 | 18 | public ListUsersService(UserManager userManager, ExtraAuthorizeDbContext extraContext) 19 | { 20 | _userManager = userManager; 21 | _extraContext = extraContext; 22 | } 23 | 24 | public List ListUserWithRolesAndDataTenant() 25 | { 26 | var result = new List(); 27 | foreach (var user in _userManager.Users) 28 | { 29 | var userRoleNames = _extraContext.UserToRoles.Where(x => x.UserId == user.Id).Select(x => x.RoleName); 30 | var dataEntry = _extraContext.Find(user.Id); 31 | string tenantName = "no linked tenant"; 32 | string companyName = null; 33 | if (dataEntry != null) 34 | { 35 | var linkedTenant = _extraContext.Find(dataEntry.LinkedTenantId); 36 | tenantName = linkedTenant?.Name ?? "tenant not found"; 37 | if (linkedTenant != null) 38 | companyName = _extraContext.Find(linkedTenant.ExtractCompanyId())?.Name; 39 | } 40 | 41 | result.Add(new ListUsersDto(user.Id, user.UserName, string.Join(", ", userRoleNames), companyName, tenantName)); 42 | } 43 | 44 | return result; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /ServiceLayer/UserServices/ICacheRoleService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Security.Claims; 3 | using PermissionParts; 4 | 5 | namespace ServiceLayer.UserServices 6 | { 7 | public interface ICacheRoleService 8 | { 9 | IEnumerable ShowExistingCachePermissions(IEnumerable usersClaims); 10 | void ToggleCacheRole(); 11 | IEnumerable GetFeatureLastUpdated(IEnumerable usersClaims); 12 | } 13 | } -------------------------------------------------------------------------------- /ServiceLayer/UserServices/IListUsersService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace ServiceLayer.UserServices 7 | { 8 | public interface IListUsersService 9 | { 10 | List ListUserWithRolesAndDataTenant(); 11 | } 12 | } -------------------------------------------------------------------------------- /ServiceLayer/UserServices/ListUsersDto.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace ServiceLayer.UserServices 7 | { 8 | public class ListUsersDto 9 | { 10 | public ListUsersDto(string userId, string userName, string roleNames, string companyName, string linkedTenantName) 11 | { 12 | UserId = userId; 13 | UserName = userName ?? throw new ArgumentNullException(nameof(userName)); 14 | RoleNames = roleNames ?? throw new ArgumentNullException(nameof(roleNames)); 15 | CompanyName = companyName; 16 | LinkedTenantName = linkedTenantName ?? throw new ArgumentNullException(nameof(linkedTenantName)); 17 | } 18 | 19 | public string UserId { get; } 20 | public string UserName { get; } 21 | public string RoleNames { get; } 22 | public string CompanyName { get; } 23 | public string LinkedTenantName { get; } 24 | } 25 | } -------------------------------------------------------------------------------- /ServiceLayer/UserServices/UserExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Security.Claims; 4 | using FeatureAuthorize; 5 | using PermissionParts; 6 | 7 | namespace ServiceLayer.UserServices 8 | { 9 | public static class UserExtensions 10 | { 11 | /// 12 | /// This gets the permissions for the currently logged in user (or null if no claim) 13 | /// 14 | /// 15 | /// 16 | public static IEnumerable PermissionsFromClaims(this IEnumerable usersClaims) 17 | { 18 | var permissionsClaim = 19 | usersClaims?.SingleOrDefault(c => c.Type == PermissionConstants.PackedPermissionClaimType); 20 | return permissionsClaim?.Value.UnpackPermissionsFromString(); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Test/DiConfigHelpers/MockHostingEnvironment.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.FileProviders; 6 | 7 | namespace Test.DiConfigHelpers 8 | { 9 | internal class MockHostingEnvironment : IWebHostEnvironment 10 | { 11 | public string EnvironmentName { get; set; } = "Development"; 12 | 13 | public string ApplicationName { get; set; } 14 | 15 | public string WebRootPath { get; set; } 16 | 17 | public IFileProvider WebRootFileProvider { get; set; } 18 | 19 | public string ContentRootPath { get; set; } 20 | 21 | public IFileProvider ContentRootFileProvider { get; set; } 22 | } 23 | } -------------------------------------------------------------------------------- /Test/EfHelpers/AuthRoleServiceHelpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using DataLayer.EfCode; 7 | using DataLayer.ExtraAuthClasses; 8 | using PermissionParts; 9 | using ServiceLayer.UserServices.Concrete; 10 | using Xunit.Extensions.AssertExtensions; 11 | 12 | namespace Test.EfHelpers 13 | { 14 | public static class AuthRoleServiceHelpers 15 | { 16 | public static void SeedUserWithDefaultPermissions(this ExtraAuthorizeDbContext context, 17 | PaidForModules modules = PaidForModules.None, string userId = "userId") 18 | { 19 | var defaultPermissions = new List {Permissions.StockRead, Permissions.Feature1Access}; 20 | 21 | var roleStatus = RoleToPermissions.CreateRoleWithPermissions( 22 | "TestRole1", "TestRole1", defaultPermissions, context); 23 | roleStatus.IsValid.ShouldBeTrue(roleStatus.GetAllErrors()); 24 | context.Add(roleStatus.Result); 25 | 26 | var moduleUser = new ModulesForUser(userId, modules); 27 | context.Add(moduleUser); 28 | 29 | var userStatus = UserToRole.AddRoleToUser(userId, "TestRole1", context); 30 | userStatus.IsValid.ShouldBeTrue(roleStatus.GetAllErrors()); 31 | context.Add(userStatus.Result); 32 | 33 | context.SaveChanges(); 34 | } 35 | 36 | public static void SeedUserWithTwoRoles(this ExtraAuthorizeDbContext context, string userId = "userId") 37 | { 38 | var userStatus = RoleToPermissions.CreateRoleWithPermissions( 39 | "TestRole1", "TestRole1", new List { Permissions.StockRead}, context); 40 | userStatus.IsValid.ShouldBeTrue(userStatus.GetAllErrors()); 41 | context.Add(userStatus.Result); 42 | userStatus = RoleToPermissions.CreateRoleWithPermissions( 43 | "TestRole2", "TestRole1", new List { Permissions.SalesSell}, context); 44 | userStatus.IsValid.ShouldBeTrue(userStatus.GetAllErrors()); 45 | context.Add(userStatus.Result); 46 | 47 | var roleStatus = UserToRole.AddRoleToUser(userId, "TestRole1", context); 48 | roleStatus.IsValid.ShouldBeTrue(userStatus.GetAllErrors()); 49 | context.Add(roleStatus.Result); 50 | roleStatus = UserToRole.AddRoleToUser(userId, "TestRole2", context); 51 | roleStatus.IsValid.ShouldBeTrue(userStatus.GetAllErrors()); 52 | context.Add(roleStatus.Result); 53 | 54 | context.SaveChanges(); 55 | } 56 | 57 | public static void SeedCacheRole(this ExtraAuthorizeDbContext context, bool hasCache2, string userId = "userId") 58 | { 59 | var permissions = new List { Permissions.Cache1 }; 60 | if (hasCache2) 61 | permissions.Add(Permissions.Cache2); 62 | var userStatus = RoleToPermissions.CreateRoleWithPermissions( 63 | CacheRoleService.CacheRoleName, CacheRoleService.CacheRoleName, permissions, context); 64 | userStatus.IsValid.ShouldBeTrue(userStatus.GetAllErrors()); 65 | context.Add(userStatus.Result); 66 | 67 | context.SaveChanges(); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /Test/EfHelpers/CheckEntitiesHelpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Reflection; 6 | using DataKeyParts; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.EntityFrameworkCore.Metadata; 9 | 10 | namespace Test.EfHelpers 11 | { 12 | public static class CheckEntitiesHelpers 13 | { 14 | public static IEnumerable CheckEntitiesHasAQueryFilter(this IEnumerable entityTypes) 15 | { 16 | foreach (var entityType in entityTypes) 17 | { 18 | if (entityType.GetQueryFilter() == null 19 | && entityType.BaseType == null //not a TPH subclass 20 | && entityType.ClrType.GetCustomAttribute() == null //not an owned type 21 | && entityType.ClrType.GetCustomAttribute() == null) //Not marked as global 22 | yield return $"The entity class {entityType.Name} does not have a query filter"; 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /Test/FakesAndMocks/CookieRequestHelpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNetCore.DataProtection; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Http.Features; 7 | using Microsoft.Extensions.Primitives; 8 | using Microsoft.Net.Http.Headers; 9 | 10 | namespace Test.FakesAndMocks 11 | { 12 | public static class CookieRequestHelpers 13 | { 14 | 15 | public static void AddRequestCookie(this HttpContext httpContext, string key, string value) 16 | { 17 | httpContext.Request.Cookies = MockRequestCookieCollection(key, value); 18 | } 19 | 20 | //see https://stackoverflow.com/a/63132794/1434764 21 | public static IRequestCookieCollection MockRequestCookieCollection(string key, string value) 22 | { 23 | var requestFeature = new HttpRequestFeature(); 24 | var featureCollection = new FeatureCollection(); 25 | 26 | requestFeature.Headers = new HeaderDictionary(); 27 | requestFeature.Headers.Add(HeaderNames.Cookie, new StringValues(key + "=" + value)); 28 | 29 | featureCollection.Set(requestFeature); 30 | 31 | var cookiesFeature = new RequestCookiesFeature(featureCollection); 32 | 33 | return cookiesFeature.Cookies; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /Test/FakesAndMocks/FakeAuthChanges.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using RefreshClaimsParts; 6 | 7 | namespace Test.FakesAndMocks 8 | { 9 | public class FakeAuthChanges : IAuthChanges 10 | { 11 | public long CachedValue { get; private set; } = -1; 12 | public bool CacheValueSet { get; private set; } 13 | 14 | public bool IsOutOfDateOrMissing(string cacheKey, string ticksToCompareString, ITimeStore timeStore) 15 | { 16 | throw new System.NotImplementedException(); 17 | } 18 | 19 | public bool IsLowerThan(string cacheKey, long ticksToCompare) 20 | { 21 | throw new System.NotImplementedException(); 22 | } 23 | 24 | public void AddOrUpdate(ITimeStore timeStore) 25 | { 26 | CacheValueSet = true; 27 | } 28 | 29 | public void Clear() 30 | { 31 | CachedValue = -1; 32 | CacheValueSet = false; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /Test/FakesAndMocks/FakeGetClaimsProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using DataKeyParts; 5 | 6 | namespace Test.FakesAndMocks 7 | { 8 | public class FakeGetClaimsProvider : IGetClaimsProvider 9 | { 10 | public FakeGetClaimsProvider(string dataKey) 11 | { 12 | DataKey = dataKey; 13 | } 14 | 15 | public string DataKey { get; } 16 | } 17 | } -------------------------------------------------------------------------------- /Test/FakesAndMocks/FakeTimeStore.cs: -------------------------------------------------------------------------------- 1 | using RefreshClaimsParts; 2 | 3 | namespace Test.FakesAndMocks 4 | { 5 | public class FakeTimeStore : ITimeStore 6 | { 7 | public FakeTimeStore(string key, long? value) 8 | { 9 | Key = key; 10 | Value = value; 11 | } 12 | 13 | public string Key { get; private set; } 14 | public long? Value { get; private set; } 15 | 16 | public long? GetValueFromStore(string key) 17 | { 18 | Key = key; 19 | return Value; 20 | } 21 | 22 | public void AddUpdateValue(string key, long value) 23 | { 24 | Key = key; 25 | Value = value; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Test/Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | PreserveNewest 19 | 20 | 21 | PreserveNewest 22 | 23 | 24 | PreserveNewest 25 | 26 | 27 | PreserveNewest 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | all 44 | runtime; build; native; contentfiles; analyzers 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /Test/TestData/SeedData/Companies.txt: -------------------------------------------------------------------------------- 1 | 4U Inc.|West Coast|LA Tie4U 2 | 4U Inc.|East Coast|NY Dress4U 3 | Pets2 Ltd.|Cats Place, Kitten Place -------------------------------------------------------------------------------- /Test/TestData/SeedData/Roles.txt: -------------------------------------------------------------------------------- 1 | Director: EmployeeRead 2 | AreaManager: StockRead, SalesRead 3 | SalesAssistant: StockRead, SalesSell 4 | StoreManager: StockRead, StockAddNew, StockRemove, SalesRead, SalesSell, SalesReturn 5 | UserAdmin: UserRead, UserChange, RoleRead -------------------------------------------------------------------------------- /Test/TestData/SeedData/ShopStock.txt: -------------------------------------------------------------------------------- 1 | LA Tie4U: Blue tie|15, Red tie|20, Green tie|10 2 | NY Dress4U: Modern dress|65, Nice dress|30 3 | Cats Place: Cat food (large)|40, Cat food (small)|10 4 | Kitten Place: Scratch pole|60, Play mouse|5, Cat food (small)|12 -------------------------------------------------------------------------------- /Test/TestData/SeedData/Users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Email": "dir@4U.com", 4 | "RolesCommaDelimited": "Director", 5 | "LinkedTenant": "4U Inc." 6 | }, 7 | { 8 | "Email": "westCoast@4U.com", 9 | "RolesCommaDelimited": "AreaManager", 10 | "LinkedTenant": "West Coast" 11 | }, 12 | { 13 | "Email": "eastCoast@4U.com", 14 | "RolesCommaDelimited": "AreaManager", 15 | "LinkedTenant": "East Coast" 16 | }, 17 | { 18 | "Email": "*Boss@4U.com", 19 | "RolesCommaDelimited": "StoreManager", 20 | "LinkedTenant": "*4U" 21 | }, 22 | { 23 | "Email": "*Sales@4U.com", 24 | "RolesCommaDelimited": "SalesAssistant", 25 | "LinkedTenant": "*4U" 26 | }, 27 | //Pets2 28 | { 29 | "Email": "dir@Pets2.com", 30 | "RolesCommaDelimited": "Director", 31 | "LinkedTenant": "Pets2 Ltd." 32 | }, 33 | { 34 | "Email": "*Sales@Pets2.com", 35 | "RolesCommaDelimited": "SalesAssistant", 36 | "LinkedTenant": "*Place" 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /Test/UnitCommands/DeleteAllUnitTestDatabases.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using TestSupport.Attributes; 5 | using TestSupport.EfHelpers; 6 | using Xunit.Abstractions; 7 | 8 | namespace Test.UnitCommands 9 | { 10 | public class DeleteAllUnitTestDatabases 11 | { 12 | private readonly ITestOutputHelper _output; 13 | 14 | public DeleteAllUnitTestDatabases(ITestOutputHelper output) 15 | { 16 | _output = output; 17 | } 18 | 19 | //Run this method to wipe ALL the test database for the current branch 20 | //You need to run it in debug mode - that stops it being run when you "run all" unit tests 21 | [RunnableInDebugOnly] 22 | public void DeleteAllTestDatabasesOk() 23 | { 24 | var numDeleted = DatabaseTidyHelper.DeleteAllUnitTestDatabases(); 25 | _output.WriteLine( "This deleted {0} databases.", numDeleted); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Test/UnitTests/DataAuthorizeTests/TestHierarchicalFiltering.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Linq; 5 | using DataLayer.EfCode; 6 | using PermissionAccessControl2.SeedDemo.Internal; 7 | using Test.FakesAndMocks; 8 | using TestSupport.EfHelpers; 9 | using Xunit; 10 | using Xunit.Abstractions; 11 | using Xunit.Extensions.AssertExtensions; 12 | 13 | namespace Test.UnitTests.DataAuthorizeTests 14 | { 15 | public class TestHierarchicalFiltering 16 | { 17 | private readonly ITestOutputHelper _output; 18 | 19 | [Fact] 20 | public void TestCreateValidDatabaseOk() 21 | { 22 | //SETUP 23 | var options = SqliteInMemory.CreateOptions(); 24 | using (var context = new CompanyDbContext(options, new FakeGetClaimsProvider("accessKey"))) 25 | { 26 | //ATTEMPT 27 | context.Database.EnsureCreated(); 28 | 29 | //VERIFY 30 | } 31 | } 32 | 33 | public TestHierarchicalFiltering(ITestOutputHelper output) 34 | { 35 | _output = output; 36 | } 37 | 38 | [Theory] 39 | [InlineData("1|", 10)] 40 | [InlineData("1|2|", 9)] 41 | [InlineData("1|2|3|", 4)] 42 | [InlineData("1|2|3|6*", 1)] 43 | public void TestFilterTenantsOk(string dataKey, int expectedCount) 44 | { 45 | //SETUP 46 | var options = SqliteInMemory.CreateOptions(); 47 | using (var context = new CompanyDbContext(options, new FakeGetClaimsProvider(dataKey))) 48 | { 49 | context.Database.EnsureCreated(); 50 | context.AddCompanyAndChildrenInDatabase(); 51 | 52 | //ATTEMPT 53 | var tenants = context.Tenants.ToList(); 54 | 55 | //VERIFY 56 | //foreach (var line in tenants) 57 | //{ 58 | // _output.WriteLine($"\"{line}\","); 59 | //} 60 | tenants.Count.ShouldEqual(expectedCount); 61 | } 62 | } 63 | 64 | 65 | } 66 | } -------------------------------------------------------------------------------- /Test/UnitTests/FeatureAuthorizeTests/TestAuthChanges.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using RefreshClaimsParts; 6 | using Test.FakesAndMocks; 7 | using Xunit; 8 | using Xunit.Extensions.AssertExtensions; 9 | 10 | namespace Test.UnitTests.FeatureAuthorizeTests 11 | { 12 | public class TestAuthChanges 13 | { 14 | 15 | [Theory] 16 | [InlineData("test", "123", true)] 17 | [InlineData("test", "1234", false)] 18 | [InlineData("test", null, true)] 19 | public void TestIsOutOfDateOrMissing(string key, string ticksToTry, bool expectedResult ) 20 | { 21 | //SETUP 22 | var fakeTimeStore = new FakeTimeStore("test", 200); 23 | var authChange = new AuthChanges(); 24 | 25 | //ATTEMPT 26 | var isOutOfDate = authChange.IsOutOfDateOrMissing(key, ticksToTry, fakeTimeStore); 27 | 28 | //VERIFY 29 | isOutOfDate.ShouldEqual(expectedResult); 30 | } 31 | 32 | [Fact] 33 | public void TestIsOutOfDateOrMissingNoOriginalValue() 34 | { 35 | //SETUP 36 | var fakeTimeStore = new FakeTimeStore("test", null); 37 | var authChange = new AuthChanges(); 38 | 39 | //ATTEMPT 40 | var ex = Assert.Throws(() => authChange.IsOutOfDateOrMissing("test", "100", fakeTimeStore)); 41 | 42 | //VERIFY 43 | ex.Message.ShouldStartWith("You must seed the database with a cache value for the key "); 44 | } 45 | 46 | 47 | [Fact] 48 | public void TestAddOrUpdateDatabaseUpdate() 49 | { 50 | //SETUP 51 | var fakeTimeStore = new FakeTimeStore("test", 200); 52 | var authChange = new AuthChanges(); 53 | 54 | //ATTEMPT 55 | authChange.AddOrUpdate(fakeTimeStore); 56 | 57 | //VERIFY 58 | fakeTimeStore.Key.ShouldEqual(AuthChangesConsts.FeatureCacheKey); 59 | fakeTimeStore.Value.ShouldNotEqual((long)200); 60 | } 61 | 62 | [Fact] 63 | public void TestAddOrUpdateDatabaseAdd() 64 | { 65 | //SETUP 66 | var fakeTimeStore = new FakeTimeStore(AuthChangesConsts.FeatureCacheKey, 200); 67 | var authChange = new AuthChanges(); 68 | 69 | //ATTEMPT 70 | authChange.AddOrUpdate(fakeTimeStore); 71 | 72 | //VERIFY 73 | fakeTimeStore.Key.ShouldEqual(AuthChangesConsts.FeatureCacheKey); 74 | fakeTimeStore.Value.ShouldNotEqual((long)200); 75 | } 76 | 77 | } 78 | } -------------------------------------------------------------------------------- /Test/UnitTests/FeatureAuthorizeTests/TestCacheRoleService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Runtime.InteropServices; 7 | using System.Security.Claims; 8 | using DataLayer.EfCode; 9 | using DataLayer.ExtraAuthClasses; 10 | using FeatureAuthorize; 11 | using PermissionParts; 12 | using ServiceLayer.UserServices.Concrete; 13 | using Test.EfHelpers; 14 | using Test.FakesAndMocks; 15 | using TestSupport.EfHelpers; 16 | using Xunit; 17 | using Xunit.Extensions.AssertExtensions; 18 | 19 | namespace Test.UnitTests.FeatureAuthorizeTests 20 | { 21 | public class TestCacheRoleService 22 | { 23 | [Theory] 24 | [InlineData(true)] 25 | [InlineData(false)] 26 | public void TestSeedCacheRole(bool hasCache2) 27 | { 28 | //SETUP 29 | var fakeAuthChanges = new FakeAuthChanges(); 30 | var options = SqliteInMemory.CreateOptions(); 31 | using (var context = new ExtraAuthorizeDbContext(options, fakeAuthChanges)) 32 | { 33 | context.Database.EnsureCreated(); 34 | 35 | //ATTEMPT 36 | context.SeedCacheRole(hasCache2); 37 | } 38 | using (var context = new ExtraAuthorizeDbContext(options, fakeAuthChanges)) 39 | { 40 | //VERIFY 41 | var role = context.RolesToPermissions.Single(); 42 | role.RoleName.ShouldEqual(CacheRoleService.CacheRoleName); 43 | role.PermissionsInRole.Count().ShouldEqual(hasCache2 ? 2 : 1); 44 | } 45 | } 46 | 47 | [Fact] 48 | public void TestToggleCacheRole() 49 | { 50 | //SETUP 51 | var fakeAuthChanges = new FakeAuthChanges(); 52 | var options = SqliteInMemory.CreateOptions(); 53 | using (var context = new ExtraAuthorizeDbContext(options, fakeAuthChanges)) 54 | { 55 | context.Database.EnsureCreated(); 56 | context.SeedCacheRole(true); 57 | 58 | var cacheRoleService = new CacheRoleService(context); 59 | var claims = new List 60 | { 61 | new Claim(PermissionConstants.PackedPermissionClaimType, 62 | new List {Permissions.Cache1, Permissions.Cache2}.PackPermissionsIntoString()) 63 | }; 64 | 65 | //ATTEMPT 66 | cacheRoleService.ToggleCacheRole(); 67 | 68 | //VERIFY 69 | var role = context.RolesToPermissions.Single(); 70 | role.RoleName.ShouldEqual(CacheRoleService.CacheRoleName); 71 | role.PermissionsInRole.Count().ShouldEqual(1); 72 | fakeAuthChanges.CacheValueSet.ShouldBeTrue(); 73 | } 74 | } 75 | 76 | } 77 | } -------------------------------------------------------------------------------- /Test/UnitTests/FeatureAuthorizeTests/TestCalcAllowedPermissions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | using DataLayer.EfCode; 7 | using FeatureAuthorize; 8 | using PermissionParts; 9 | using Test.EfHelpers; 10 | using Test.FakesAndMocks; 11 | using TestSupport.EfHelpers; 12 | using Xunit; 13 | using Xunit.Extensions.AssertExtensions; 14 | 15 | namespace Test.UnitTests.FeatureAuthorizeTests 16 | { 17 | public class TestCalcAllowedPermissions 18 | { 19 | [Fact] 20 | public async Task TestCalcPermissionsForUserAsync() 21 | { 22 | //SETUP 23 | var fakeAuthChanges = new FakeAuthChanges(); 24 | var options = SqliteInMemory.CreateOptions(); 25 | using (var context = new ExtraAuthorizeDbContext(options, fakeAuthChanges)) 26 | { 27 | context.Database.EnsureCreated(); 28 | context.SeedUserWithDefaultPermissions(); 29 | 30 | var calc = new CalcAllowedPermissions(context); 31 | //ATTEMPT 32 | var packedP = await calc.CalcPermissionsForUserAsync("userId"); 33 | 34 | //VERIFY 35 | packedP.UnpackPermissionsFromString().ShouldEqual(new List { Permissions.StockRead}); 36 | } 37 | } 38 | 39 | [Fact] 40 | public async Task TestCalcPermissionsForUserAsyncWithModule() 41 | { 42 | //SETUP 43 | var fakeAuthChanges = new FakeAuthChanges(); 44 | var options = SqliteInMemory.CreateOptions(); 45 | using (var context = new ExtraAuthorizeDbContext(options, fakeAuthChanges)) 46 | { 47 | context.Database.EnsureCreated(); 48 | context.SeedUserWithDefaultPermissions(PaidForModules.Feature1); 49 | 50 | var calc = new CalcAllowedPermissions(context); 51 | //ATTEMPT 52 | var packedP = await calc.CalcPermissionsForUserAsync("userId"); 53 | 54 | //VERIFY 55 | packedP.UnpackPermissionsFromString().ShouldEqual(new List { Permissions.StockRead, Permissions.Feature1Access }); 56 | } 57 | } 58 | 59 | } 60 | } -------------------------------------------------------------------------------- /Test/UnitTests/FeatureAuthorizeTests/TestPermissionsChangedDatabase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using DataLayer.EfCode; 8 | using DataLayer.ExtraAuthClasses; 9 | using Microsoft.EntityFrameworkCore; 10 | using PermissionParts; 11 | using RefreshClaimsParts; 12 | using Test.FakesAndMocks; 13 | using TestSupport.EfHelpers; 14 | using Xunit; 15 | using Xunit.Extensions.AssertExtensions; 16 | 17 | namespace Test.UnitTests.FeatureAuthorizeTests 18 | { 19 | public class TestPermissionsChangedDatabase 20 | { 21 | [Fact] 22 | public void TestAddRoleNotTrigger() 23 | { 24 | //SETUP 25 | var options = SqliteInMemory.CreateOptions(); 26 | using (var context = new ExtraAuthorizeDbContext(options, new AuthChanges())) 27 | { 28 | context.Database.EnsureCreated(); 29 | 30 | //ATTEMPT 31 | var rolToPer = RoleToPermissions.CreateRoleWithPermissions 32 | ("test", "test", new List { Permissions.AccessAll }, context).Result; 33 | context.Add(rolToPer); 34 | context.SaveChanges(); 35 | 36 | //VERIFY 37 | context.TimeStores.Count().ShouldEqual(0); 38 | context.RolesToPermissions.Count().ShouldEqual(1); 39 | } 40 | } 41 | 42 | [Fact] 43 | public void TestUpdateRoleTrigger() 44 | { 45 | //SETUP 46 | var options = SqliteInMemory.CreateOptions(); 47 | using (var context = new ExtraAuthorizeDbContext(options, new AuthChanges())) 48 | { 49 | context.Database.EnsureCreated(); 50 | var rolToPer = RoleToPermissions.CreateRoleWithPermissions 51 | ("test", "test", new List { Permissions.AccessAll }, context).Result; 52 | context.Add(rolToPer); 53 | context.SaveChanges(); 54 | 55 | //ATTEMPT 56 | rolToPer.Update("test", new List { Permissions.EmployeeRead }); 57 | context.SaveChanges(); 58 | 59 | //VERIFY 60 | context.TimeStores.Count().ShouldEqual(1); 61 | context.RolesToPermissions.Count().ShouldEqual(1); 62 | } 63 | } 64 | 65 | [Fact] 66 | public async Task TestAddRoleToUseTrigger() 67 | { 68 | //SETUP 69 | var options = SqliteInMemory.CreateOptions(); 70 | using (var context = new ExtraAuthorizeDbContext(options, new AuthChanges())) 71 | { 72 | context.Database.EnsureCreated(); 73 | var rolToPer = RoleToPermissions.CreateRoleWithPermissions 74 | ("test", "test", new List { Permissions.AccessAll }, context).Result; 75 | context.Add(rolToPer); 76 | context.SaveChanges(); 77 | 78 | //ATTEMPT 79 | var userToRole = new UserToRole("test", rolToPer); 80 | context.Add(userToRole); 81 | await context.SaveChangesAsync(); 82 | 83 | //VERIFY 84 | context.TimeStores.Count().ShouldEqual(1); 85 | context.UserToRoles.Count().ShouldEqual(1); 86 | } 87 | } 88 | 89 | } 90 | } -------------------------------------------------------------------------------- /Test/UnitTests/FeatureAuthorizeTests/TestPermissionsChangedFakeDatabase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using DataLayer.EfCode; 8 | using DataLayer.ExtraAuthClasses; 9 | using PermissionParts; 10 | using Test.EfHelpers; 11 | using Test.FakesAndMocks; 12 | using TestSupport.EfHelpers; 13 | using Xunit; 14 | using Xunit.Extensions.AssertExtensions; 15 | 16 | namespace Test.UnitTests.FeatureAuthorizeTests 17 | { 18 | public class TestPermissionsChangedFakeDatabase 19 | { 20 | [Fact] 21 | public void TestAddRoleNotTrigger() 22 | { 23 | //SETUP 24 | var fakeAuthChanges = new FakeAuthChanges(); 25 | var options = SqliteInMemory.CreateOptions(); 26 | using (var context = new ExtraAuthorizeDbContext(options, fakeAuthChanges)) 27 | { 28 | context.Database.EnsureCreated(); 29 | 30 | //ATTEMPT 31 | var rolToPer = RoleToPermissions.CreateRoleWithPermissions 32 | ("test", "test", new List { Permissions.AccessAll }, context).Result; 33 | context.Add(rolToPer); 34 | context.SaveChanges(); 35 | 36 | //VERIFY 37 | fakeAuthChanges.CacheValueSet.ShouldBeFalse(); 38 | context.RolesToPermissions.Count().ShouldEqual(1); 39 | } 40 | } 41 | 42 | [Fact] 43 | public void TestUpdateRoleTrigger() 44 | { 45 | //SETUP 46 | var fakeAuthChanges = new FakeAuthChanges(); 47 | var options = SqliteInMemory.CreateOptions(); 48 | using (var context = new ExtraAuthorizeDbContext(options, fakeAuthChanges)) 49 | { 50 | context.Database.EnsureCreated(); 51 | var rolToPer = RoleToPermissions.CreateRoleWithPermissions 52 | ("test", "test", new List { Permissions.AccessAll }, context).Result; 53 | context.Add(rolToPer); 54 | context.SaveChanges(); 55 | 56 | //ATTEMPT 57 | rolToPer.Update("updated", new List { Permissions.EmployeeRead }); 58 | context.SaveChanges(); 59 | 60 | //VERIFY 61 | fakeAuthChanges.CacheValueSet.ShouldBeTrue(); 62 | context.RolesToPermissions.Count().ShouldEqual(1); 63 | } 64 | } 65 | 66 | [Fact] 67 | public async Task TestAddRoleToUseTrigger() 68 | { 69 | //SETUP 70 | var fakeAuthChanges = new FakeAuthChanges(); 71 | var options = SqliteInMemory.CreateOptions(); 72 | using (var context = new ExtraAuthorizeDbContext(options, fakeAuthChanges)) 73 | { 74 | context.Database.EnsureCreated(); 75 | var rolToPer = RoleToPermissions.CreateRoleWithPermissions 76 | ("test", "test", new List { Permissions.AccessAll }, context).Result; 77 | context.Add(rolToPer); 78 | context.SaveChanges(); 79 | fakeAuthChanges.Clear(); 80 | 81 | //ATTEMPT 82 | var userToRole = new UserToRole("test", rolToPer); 83 | context.Add(userToRole); 84 | await context.SaveChangesAsync(); 85 | 86 | //VERIFY 87 | fakeAuthChanges.CacheValueSet.ShouldBeTrue(); 88 | context.UserToRoles.Count().ShouldEqual(1); 89 | } 90 | } 91 | 92 | 93 | 94 | } 95 | } -------------------------------------------------------------------------------- /Test/UnitTests/SecurityChecks/CheckEntitiesAreSecure.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Linq; 5 | using DataLayer.EfCode; 6 | using Test.EfHelpers; 7 | using Test.FakesAndMocks; 8 | using TestSupport.EfHelpers; 9 | using Xunit; 10 | using Xunit.Extensions.AssertExtensions; 11 | 12 | namespace Test.UnitTests.SecurityChecks 13 | { 14 | public class CheckEntitiesAreSecure 15 | { 16 | [Fact] 17 | public void CheckQueryFiltersAreAppliedToEntityClassesOk() 18 | { 19 | //SETUP 20 | var options = SqliteInMemory.CreateOptions(); 21 | using (var context = new CompanyDbContext(options, new FakeGetClaimsProvider("accessKey"))) 22 | { 23 | var entities = context.Model.GetEntityTypes().ToList(); 24 | 25 | //ATTEMPT 26 | var queryFilterErrs = entities.CheckEntitiesHasAQueryFilter().ToList(); 27 | 28 | //VERIFY 29 | queryFilterErrs.Any().ShouldBeFalse(string.Join('\n', queryFilterErrs)); 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Test/UnitTests/SecurityChecks/CheckPermissions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using PermissionParts; 7 | using Xunit; 8 | using Xunit.Extensions.AssertExtensions; 9 | 10 | namespace Test.UnitTests.SecurityChecks 11 | { 12 | public class CheckPermissions 13 | { 14 | //Its VERY important that you don't have duplicate Permission numbers as it could cause a security breach 15 | [Fact] 16 | public void CheckPermissionsHaveUniqueNumberOk() 17 | { 18 | //SETUP 19 | 20 | //ATTEMPT 21 | var nums = Enum.GetValues(typeof(Permissions)).Cast().Select(x => (int)x).ToList(); 22 | 23 | //VERIFY 24 | nums.Count.ShouldEqual(nums.Distinct().Count()); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Test/UnitTests/ShopServices/TestShopServices.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Linq; 5 | using DataLayer.EfCode; 6 | using DataLayer.MultiTenantClasses; 7 | using GenericServices.PublicButHidden; 8 | using GenericServices.Setup; 9 | using Microsoft.EntityFrameworkCore; 10 | using PermissionParts; 11 | using ServiceLayer.Shop; 12 | using Test.FakesAndMocks; 13 | using TestSupport.EfHelpers; 14 | using Xunit; 15 | using Xunit.Extensions.AssertExtensions; 16 | 17 | namespace Test.UnitTests.ShopServices 18 | { 19 | public class TestShopServices 20 | { 21 | 22 | 23 | [Fact] 24 | public void TestQueryFilterWorksOnShopStock() 25 | { 26 | //SETUP 27 | var options = SqliteInMemory.CreateOptions(); 28 | using (var context = new CompanyDbContext(options, new FakeGetClaimsProvider("accessKey*"))) 29 | { 30 | context.Database.EnsureCreated(); 31 | var company = Company.AddTenantToDatabaseWithSaveChanges("TestCompany", PaidForModules.None, context); 32 | var shop = RetailOutlet.AddTenantToDatabaseWithSaveChanges("TestShop", company, context); 33 | var stock = new ShopStock {Name = "dress", NumInStock = 5, Shop = shop}; 34 | context.Add(stock); 35 | context.SaveChanges(); 36 | 37 | var utData = context.SetupSingleDtoAndEntities(); 38 | var service = new CrudServices(context, utData.ConfigAndMapper); 39 | 40 | //ATTEMPT 41 | var dto = new SellItemDto 42 | { 43 | TenantItemId = shop.TenantItemId, 44 | ShopStockId = stock.ShopStockId, 45 | NumBought = 1 46 | }; 47 | var shopSale = service.CreateAndSave(dto); 48 | 49 | //VERIFY 50 | service.IsValid.ShouldBeTrue(service.GetAllErrors()); 51 | context.ShopSales.Count().ShouldEqual(1); 52 | context.ShopStocks.Single().NumInStock.ShouldEqual(4); 53 | } 54 | } 55 | 56 | 57 | } 58 | } -------------------------------------------------------------------------------- /Test/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "UnitTestConnection": "Server=(localdb)\\mssqllocaldb;Database=PermissionAccessControl2-Test;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Test/demosettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "SuperAdmin": //This holds the information on the superuser. You must have one SuperUser setup otherwise you can't manage users 3 | { 4 | "Email": "Super@g1.com", 5 | "Password": "Super@g1.com" 6 | } 7 | } -------------------------------------------------------------------------------- /UserImpersonation/AppStart/StartupExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.Extensions.DependencyInjection; 5 | using NetCore.AutoRegisterDi; 6 | 7 | namespace UserImpersonation.AppStart 8 | { 9 | public static class StartupExtensions 10 | { 11 | public static void UserImpersonationRegister(this IServiceCollection services) 12 | { 13 | //This registers the classes in the current assembly that end in "Service" and have a public interface 14 | services.RegisterAssemblyPublicNonGenericClasses() 15 | .Where(c => c.Name.EndsWith("Service")) 16 | .AsPublicImplementedInterfaces(); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /UserImpersonation/Concrete/AuthCookieSignout.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authentication.Cookies; 6 | 7 | namespace UserImpersonation.Concrete 8 | { 9 | public class AuthCookieSigningOut 10 | { 11 | /// 12 | /// This will ensure any impersonation cookie is deleted when a user signs out 13 | /// 14 | /// 15 | /// 16 | public Task SigningOutAsync(CookieSigningOutContext context) 17 | { 18 | var cookie = new ImpersonationCookie(context.HttpContext, null); 19 | cookie.Delete(); 20 | 21 | return Task.CompletedTask; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /UserImpersonation/Concrete/ImpersonationCookie.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Runtime.CompilerServices; 6 | using Microsoft.AspNetCore.DataProtection; 7 | using Microsoft.AspNetCore.Http; 8 | 9 | [assembly: InternalsVisibleTo("Test")] 10 | 11 | namespace UserImpersonation.Concrete 12 | { 13 | public class ImpersonationCookie 14 | { 15 | private const string CookieName = "UserImpersonation"; 16 | 17 | private readonly HttpContext _httpContext; 18 | private readonly IDataProtectionProvider _protectionProvider; 19 | private readonly CookieOptions _options; 20 | 21 | public string EncryptPurpose { get; private set; } 22 | 23 | public ImpersonationCookie(HttpContext httpContext, IDataProtectionProvider protectionProvider) 24 | { 25 | _httpContext = httpContext ?? throw new ArgumentNullException(nameof(httpContext)); 26 | _protectionProvider = protectionProvider; //Can be null 27 | EncryptPurpose = "hffhegse432!&2!jbK!K3wqqqagg3bbassdewdsgfedgbfdewe13c"; 28 | _options = new CookieOptions 29 | { 30 | Secure = false, //In real life you would want this to be true, but for this demo I allow http 31 | HttpOnly = true, //Not used by JavaScript 32 | IsEssential = true, 33 | //These two make it a session cookie, i.e. it disappears when the browser is closed 34 | Expires = null, 35 | MaxAge = null 36 | }; 37 | } 38 | 39 | public void AddUpdateCookie(string data) 40 | { 41 | if (_protectionProvider == null) 42 | throw new NullReferenceException( 43 | $"The {nameof(IDataProtectionProvider)} was null, which means impersonation is turned off."); 44 | 45 | var protector = _protectionProvider.CreateProtector(EncryptPurpose); 46 | var encryptedString = protector.Protect(data); 47 | _httpContext.Response.Cookies.Append(CookieName, encryptedString, _options); 48 | } 49 | 50 | public bool Exists(IRequestCookieCollection cookiesIn) 51 | { 52 | return cookiesIn[CookieName] != null; 53 | } 54 | 55 | public string GetCookieInValue() 56 | { 57 | if (_protectionProvider == null) 58 | throw new NullReferenceException( 59 | $"The {nameof(IDataProtectionProvider)} was null, which means impersonation is turned off."); 60 | 61 | var cookieData = _httpContext.Request.Cookies[CookieName]; 62 | if (string.IsNullOrEmpty(cookieData)) 63 | return null; 64 | 65 | var protector = _protectionProvider.CreateProtector(EncryptPurpose); 66 | string decrypt = null; 67 | try 68 | { 69 | decrypt = protector.Unprotect(cookieData); 70 | } 71 | catch (Exception e) 72 | { 73 | //_logger.LogError(e, "Error decoding a cookie. Have deleted cookie to stop any further problems."); 74 | Delete(); 75 | throw; 76 | } 77 | 78 | return decrypt; 79 | } 80 | 81 | public void Delete() 82 | { 83 | _httpContext.Response.Cookies.Delete(CookieName, _options); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /UserImpersonation/Concrete/ImpersonationData.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace UserImpersonation.Concrete 7 | { 8 | /// 9 | /// This holds the data that will be put into the ImpersonationCookie 10 | /// 11 | public class ImpersonationData 12 | { 13 | /// 14 | /// UserId of the user you are impersonating 15 | /// 16 | public string UserId { get; } 17 | 18 | /// 19 | /// UserName of the impersonated user - used to show who you are impersonating 20 | /// 21 | public string UserName { get; } 22 | 23 | /// 24 | /// If true then when impersonating another user you will keep your original permissions 25 | /// 26 | public bool KeepOwnPermissions { get; } 27 | 28 | public ImpersonationData(string userId, string userName, bool keepOwnPermissions) 29 | { 30 | UserId = userId ?? throw new ArgumentNullException(nameof(userId)); 31 | UserName = userName ?? throw new ArgumentNullException(nameof(userName)); 32 | KeepOwnPermissions = keepOwnPermissions; 33 | } 34 | 35 | public ImpersonationData(string packedString) 36 | { 37 | var split = packedString.Split(','); 38 | if (split.Length != 3) 39 | throw new ArgumentException("The string didn't unpack to three items"); 40 | 41 | UserId = split[0]; 42 | KeepOwnPermissions = bool.Parse(split[1]); 43 | UserName = split[2]; 44 | } 45 | 46 | public string GetPackImpersonationData() 47 | { 48 | return $"{UserId},{KeepOwnPermissions},{UserName}"; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /UserImpersonation/Concrete/ImpersonationService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using FeatureAuthorize; 5 | using Microsoft.AspNetCore.DataProtection; 6 | using Microsoft.AspNetCore.Http; 7 | 8 | namespace UserImpersonation.Concrete 9 | { 10 | public class ImpersonationService : IImpersonationService 11 | { 12 | private readonly HttpContext _httpContext; 13 | private readonly ImpersonationCookie _cookie; 14 | 15 | public ImpersonationService(IHttpContextAccessor httpContextAccessor, IDataProtectionProvider protectionProvider) 16 | { 17 | _httpContext = httpContextAccessor.HttpContext; 18 | _cookie = protectionProvider != null //If protectionProvider is null then impersonation is turned off 19 | ? new ImpersonationCookie(_httpContext, protectionProvider) 20 | : null; 21 | } 22 | 23 | /// 24 | /// This creates an user impersonation cookie, which starts the user impersonation via the AuthCookie ValidateAsync event 25 | /// 26 | /// This must be the userId of the user you want to impersonate 27 | /// 28 | /// 29 | /// Error message, or null if OK. 30 | public string StartImpersonation(string userId, string userName, bool keepOwnPermissions) 31 | { 32 | if (_cookie == null) 33 | return "Impersonation is turned off in this application."; 34 | if (!_httpContext.User.Identity.IsAuthenticated) 35 | return "You must be logged in to impersonate a user."; 36 | if (_httpContext.User.Claims.GetUserIdFromClaims() == userId) 37 | return "You cannot impersonate yourself."; 38 | if (_httpContext.User.InImpersonationMode()) 39 | return "You are already in impersonation mode."; 40 | if (userId == null) 41 | return "You must provide a userId string"; 42 | if (userName == null) 43 | return "You must provide a username string"; 44 | 45 | _cookie.AddUpdateCookie(new ImpersonationData(userId, userName, keepOwnPermissions).GetPackImpersonationData()); 46 | return null; 47 | } 48 | 49 | /// 50 | /// This will delete the user impersonation cookie, which causes the AuthCookie ValidateAsync event to revert to the original user 51 | /// 52 | /// error message, or null if OK 53 | public string StopImpersonation() 54 | { 55 | if (!_httpContext.User.InImpersonationMode()) 56 | return "You aren't in impersonation mode."; 57 | 58 | _cookie.Delete(); 59 | return null; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /UserImpersonation/IImpersonationService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | namespace UserImpersonation 4 | { 5 | public interface IImpersonationService 6 | { 7 | /// 8 | /// This creates an user impersonation cookie, which starts the user impersonation via the AuthCookie ValidateAsync event 9 | /// 10 | /// This must be the userId of the user you want to impersonate 11 | /// the name to show as being impersonated 12 | /// true if the original user's permissions should be used 13 | /// error message, or null if OK 14 | string StartImpersonation(string userId, string userName, bool keepOwnPermissions); 15 | 16 | /// 17 | /// This will delete the user impersonation cookie, which causes the AuthCookie ValidateAsync event to revert to the original user 18 | /// 19 | /// error message, or null if OK 20 | string StopImpersonation(); 21 | } 22 | } -------------------------------------------------------------------------------- /UserImpersonation/ImpersonateExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Linq; 5 | using System.Security.Claims; 6 | using Microsoft.AspNetCore.Html; 7 | using UserImpersonation.Concrete; 8 | 9 | namespace UserImpersonation 10 | { 11 | public static class ImpersonateExtensions 12 | { 13 | public static bool InImpersonationMode(this ClaimsPrincipal claimsPrincipal) 14 | { 15 | return claimsPrincipal.Claims.Any(x => x.Type == ImpersonationHandler.ImpersonationClaimType); 16 | } 17 | 18 | public static string GetImpersonatedUserNameMode(this ClaimsPrincipal claimsPrincipal) 19 | { 20 | return claimsPrincipal.Claims.SingleOrDefault(x => x.Type == ImpersonationHandler.ImpersonationClaimType)?.Value; 21 | } 22 | 23 | public static HtmlString GetCurrentUserNameAsHtml(this ClaimsPrincipal claimsPrincipal) 24 | { 25 | var impersonalisedName = claimsPrincipal.GetImpersonatedUserNameMode(); 26 | var nameToShow = impersonalisedName ?? 27 | claimsPrincipal.Claims.SingleOrDefault(x => x.Type == ClaimTypes.Name)?.Value ?? 28 | "not logged in"; 29 | 30 | return new HtmlString( 31 | "Impersonating " : ">Hello ") 32 | + $"{nameToShow}"); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /UserImpersonation/UserImpersonation.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | --------------------------------------------------------------------------------