├── .github └── FUNDING.yml ├── .gitignore ├── HeimGuard.sln ├── LICENSE.txt ├── README.md └── src └── HeimGuard ├── AutoPolicy ├── HeimGuardAuthorizationPolicyProvider.cs └── PermissionHandler.cs ├── HeimGuard.csproj ├── HeimGuardBuilder.cs ├── HeimGuardClient.cs ├── HeimGuardServiceRegistration.cs └── IUserPolicyHandler.cs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [pdevito3] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.build/ 2 | /global.json 3 | QueryBaseline.cs 4 | 5 | ## Ignore Visual Studio temporary files, build results, and 6 | ## files generated by popular Visual Studio add-ons. 7 | ## 8 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 9 | 10 | # User-specific files 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | *.user.sln* 16 | /test.ps1 17 | *.stackdump 18 | 19 | # User-specific files (MonoDevelop/Xamarin Studio) 20 | *.userprefs 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | 34 | # Visual Studio 2015 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # BenchmarkDotNet Results 40 | [Bb]enchmarkDotNet.Artifacts/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUNIT 47 | *.VisualState.xml 48 | TestResult.xml 49 | 50 | # Build Results of an ATL Project 51 | [Dd]ebugPS/ 52 | [Rr]eleasePS/ 53 | dlldata.c 54 | 55 | # .NET Core 56 | project.lock.json 57 | project.fragment.lock.json 58 | artifacts/ 59 | #**/Properties/launchSettings.json 60 | 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.pch 68 | *.pdb 69 | *.pgc 70 | *.pgd 71 | *.rsp 72 | *.sbr 73 | *.tlb 74 | *.tli 75 | *.tlh 76 | *.tmp 77 | *.tmp_proj 78 | *.log 79 | *.vspscc 80 | *.vssscc 81 | .builds 82 | *.pidb 83 | *.svclog 84 | *.scc 85 | 86 | # Chutzpah Test files 87 | _Chutzpah* 88 | 89 | # Visual C++ cache files 90 | ipch/ 91 | *.aps 92 | *.ncb 93 | *.opendb 94 | *.opensdf 95 | *.sdf 96 | *.cachefile 97 | *.VC.db 98 | *.VC.VC.opendb 99 | 100 | # Visual Studio profiler 101 | *.psess 102 | *.vsp 103 | *.vspx 104 | *.sap 105 | 106 | # TFS 2012 Local Workspace 107 | $tf/ 108 | 109 | # Guidance Automation Toolkit 110 | *.gpState 111 | 112 | # ReSharper is a .NET coding add-in 113 | _ReSharper*/ 114 | *.[Rr]e[Ss]harper 115 | *.DotSettings.user 116 | 117 | # JustCode is a .NET coding add-in 118 | .JustCode 119 | 120 | # TeamCity is a build add-in 121 | _TeamCity* 122 | 123 | # DotCover is a Code Coverage Tool 124 | *.dotCover 125 | 126 | # Visual Studio code coverage results 127 | *.coverage 128 | *.coveragexml 129 | 130 | # NCrunch 131 | _NCrunch_* 132 | .*crunch*.local.xml 133 | nCrunchTemp_* 134 | 135 | # MightyMoose 136 | *.mm.* 137 | AutoTest.Net/ 138 | 139 | # Web workbench (sass) 140 | .sass-cache/ 141 | 142 | # Installshield output folder 143 | [Ee]xpress/ 144 | 145 | # DocProject is a documentation generator add-in 146 | DocProject/buildhelp/ 147 | DocProject/Help/*.HxT 148 | DocProject/Help/*.HxC 149 | DocProject/Help/*.hhc 150 | DocProject/Help/*.hhk 151 | DocProject/Help/*.hhp 152 | DocProject/Help/Html2 153 | DocProject/Help/html 154 | 155 | # Click-Once directory 156 | publish/ 157 | 158 | # Publish Web Output 159 | *.[Pp]ublish.xml 160 | *.azurePubxml 161 | # TODO: Comment the next line if you want to checkin your web deploy settings 162 | # but database connection strings (with potential passwords) will be unencrypted 163 | *.pubxml 164 | *.publishproj 165 | 166 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 167 | # checkin your Azure Web App publish settings, but sensitive information contained 168 | # in these scripts will be unencrypted 169 | PublishScripts/ 170 | 171 | # NuGet Packages 172 | *.nupkg 173 | # The packages folder can be ignored because of Package Restore 174 | **/packages/* 175 | # except build/, which is used as an MSBuild target. 176 | !**/packages/build/ 177 | # Uncomment if necessary however generally it will be regenerated when needed 178 | #!**/packages/repositories.config 179 | # NuGet v3's project.json files produces more ignorable files 180 | *.nuget.props 181 | *.nuget.targets 182 | 183 | # Microsoft Azure Build Output 184 | csx/ 185 | *.build.csdef 186 | 187 | # Microsoft Azure Emulator 188 | ecf/ 189 | rcf/ 190 | 191 | # Windows Store app package directories and files 192 | AppPackages/ 193 | BundleArtifacts/ 194 | Package.StoreAssociation.xml 195 | _pkginfo.txt 196 | 197 | # Visual Studio cache files 198 | # files ending in .cache can be ignored 199 | *.[Cc]ache 200 | # but keep track of directories ending in .cache 201 | !*.[Cc]ache/ 202 | 203 | # Others 204 | ClientBin/ 205 | ~$* 206 | *~ 207 | *.dbmdl 208 | *.dbproj.schemaview 209 | *.jfm 210 | *.pfx 211 | *.publishsettings 212 | orleans.codegen.cs 213 | 214 | # Since there are multiple workflows, uncomment next line to ignore bower_components 215 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 216 | #bower_components/ 217 | 218 | # RIA/Silverlight projects 219 | Generated_Code/ 220 | 221 | # Backup & report files from converting an old project file 222 | # to a newer Visual Studio version. Backup files are not needed, 223 | # because we have git ;-) 224 | _UpgradeReport_Files/ 225 | Backup*/ 226 | UpgradeLog*.XML 227 | UpgradeLog*.htm 228 | 229 | # SQL Server files 230 | *.mdf 231 | *.ldf 232 | 233 | # Business Intelligence projects 234 | *.rdl.data 235 | *.bim.layout 236 | *.bim_*.settings 237 | 238 | # Microsoft Fakes 239 | FakesAssemblies/ 240 | 241 | # GhostDoc plugin setting file 242 | *.GhostDoc.xml 243 | 244 | # Node.js Tools for Visual Studio 245 | .ntvs_analysis.dat 246 | node_modules/ 247 | 248 | # Typescript v1 declaration files 249 | typings/ 250 | 251 | # Visual Studio 6 build log 252 | *.plg 253 | 254 | # Visual Studio 6 workspace options file 255 | *.opt 256 | 257 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 258 | *.vbw 259 | 260 | # Visual Studio LightSwitch build output 261 | **/*.HTMLClient/GeneratedArtifacts 262 | **/*.DesktopClient/GeneratedArtifacts 263 | **/*.DesktopClient/ModelManifest.xml 264 | **/*.Server/GeneratedArtifacts 265 | **/*.Server/ModelManifest.xml 266 | _Pvt_Extensions 267 | 268 | # Paket dependency manage 269 | .paket/paket.exe 270 | paket-files/ 271 | 272 | # FAKE - F# Make 273 | .fake/ 274 | 275 | # JetBrains Rider 276 | .idea/ 277 | *.sln.iml 278 | 279 | # CodeRush 280 | .cr/ 281 | 282 | # Python Tools for Visual Studio (PTVS) 283 | __pycache__/ 284 | *.pyc 285 | 286 | # Cake - Uncomment if you are using it 287 | # tools/** 288 | # !tools/packages.config 289 | 290 | # Telerik's JustMock configuration file 291 | *.jmconfig 292 | 293 | # BizTalk build output 294 | *.btp.cs 295 | *.btm.cs 296 | *.odx.cs 297 | *.xsd.cs 298 | 299 | #DS Store 300 | .DS_Store 301 | 302 | #ENV 303 | .env 304 | 305 | -------------------------------------------------------------------------------- /HeimGuard.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{89045AE9-2215-4B51-96D9-0EF49A0CF6B7}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HeimGuard", "src\HeimGuard\HeimGuard.csproj", "{57C6135E-802F-42BF-AED5-DD3B56BC9DE2}" 6 | EndProject 7 | Global 8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 9 | Debug|Any CPU = Debug|Any CPU 10 | Release|Any CPU = Release|Any CPU 11 | EndGlobalSection 12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 13 | {57C6135E-802F-42BF-AED5-DD3B56BC9DE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 14 | {57C6135E-802F-42BF-AED5-DD3B56BC9DE2}.Debug|Any CPU.Build.0 = Debug|Any CPU 15 | {57C6135E-802F-42BF-AED5-DD3B56BC9DE2}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | {57C6135E-802F-42BF-AED5-DD3B56BC9DE2}.Release|Any CPU.Build.0 = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(NestedProjects) = preSolution 19 | {57C6135E-802F-42BF-AED5-DD3B56BC9DE2} = {89045AE9-2215-4B51-96D9-0EF49A0CF6B7} 20 | EndGlobalSection 21 | EndGlobal 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Paul DeVito 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Latest Release 3 | License 4 |

5 | 6 | ## What is HeimGuard? 7 | 8 | HeimGuard is a simple library, inspired by [PolicyServer](https://policyserver.io/) and [this talk](https://www.youtube.com/watch?v=Dlrf85NTuAU) by Dominick Baier, that allows you to easily manage permissions in your .NET projects. 9 | 10 | 11 | ## Quickstart 12 | 13 | Thankfully for us, .NET makes it very easy to protect a controller using a specific policy using the `Authorize` attribute, so let's start there: 14 | 15 | ```csharp 16 | using Microsoft.AspNetCore.Authorization; 17 | using Microsoft.AspNetCore.Mvc; 18 | 19 | [ApiController] 20 | [Route("recipes")] 21 | [Authorize(Policy = "RecipesFullAccess")] 22 | public class RecipesController : ControllerBase 23 | { 24 | [HttpGet] 25 | public IActionResult Get() 26 | { 27 | return Ok(); 28 | } 29 | } 30 | ``` 31 | 32 | Next, I'm going to put my user's role in my `ClaimPrincipal`. This isn't required for HeimGuard to work, but is what we'll use for this example. 33 | 34 | ```json 35 | { 36 | "sub": "145hfy662", 37 | "name": "John Smith", 38 | "aud": ["api1", "api2"], 39 | "role": ["Chef"] 40 | } 41 | ``` 42 | 43 | Now I'm going to implement an interface from HeimGuard called `IUserPolicyHandler`. This handler is responsible for implementing your permissions lookup for your user. It should return an `IEnumerable` that stores all of the permissions that your user has available to them. 44 | 45 | **HeimGuard doesn't care how you store permissions and how you access them.** For simplicity sake in the example below, I'm just grabbing a static list, but this could just as easily come from a database or some external administration boundary and could be in whatever shape you want. 46 | 47 | ```csharp 48 | using System.Security.Claims; 49 | using HeimGuard; 50 | using Services; 51 | 52 | public class Permission 53 | { 54 | public string Name { get; set; } 55 | public List Roles { get; set; } 56 | } 57 | 58 | public static class DummyPermissionStore 59 | { 60 | public static List GetPermissions() 61 | { 62 | return new List() 63 | { 64 | new() 65 | { 66 | Name = "RecipesFullAccess", 67 | Roles = new List() { "Chef" } 68 | } 69 | }; 70 | } 71 | } 72 | 73 | public class SimpleUserPolicyHandler : IUserPolicyHandler 74 | { 75 | private readonly IHttpContextAccessor _httpContextAccessor; 76 | 77 | public UserPolicyHandler(IHttpContextAccessor httpContextAccessor) 78 | { 79 | _httpContextAccessor = httpContextAccessor; 80 | } 81 | 82 | public async Task> GetUserPermissions() 83 | { 84 | var user = _httpContextAccessor.HttpContext?.User; 85 | if (user == null) throw new ArgumentNullException(nameof(user)); 86 | 87 | // this gets the user's role(s) from their ClaimsPrincipal 88 | var roles = user.Claims 89 | .Where(c => c.Type == ClaimTypes.Role) 90 | .Select(r => r.Value) 91 | .ToArray(); 92 | 93 | // this gets their permissions based on their roles. in this example, it's just using a static list 94 | var permissions = DummyPermissionStore.GetPermissions() 95 | .Where(p => p.Roles.Any(r => roles.Contains(r))) 96 | .Select(p => p.Name) 97 | .ToArray(); 98 | 99 | return await Task.FromResult(permissions.Distinct()); 100 | } 101 | } 102 | ``` 103 | 104 | Now, all we have to do is register our `SimpleUserPolicyHandler` with `AddHeimGuard` and we're good to go: 105 | 106 | ```c# 107 | public void ConfigureServices(IServiceCollection services) 108 | { 109 | //... other services 110 | services.AddHeimGuard() 111 | .AutomaticallyCheckPermissions() 112 | .MapAuthorizationPolicies(); 113 | } 114 | ``` 115 | 116 | You'll notice two other methods extending `AddHeimGuard`. Nether are required, but they do make your life easier. For more details, check out [HeimGuard Enhancements](#heimguard-enhancements). 117 | 118 | ## Introduction 119 | 120 | Let's start by differentiating 3 different levels of permissions: 121 | 122 | - **Application access**: these are generally configured in your auth server and passed along in your token (e.g. using audience (`aud`) claim to determine what apis a token can be used in). 123 | - **Feature access**: permission specific check in a particular application boundary (e.g. can a user perform some action). 124 | - **Application logic**: custom business logic specific to your application (e.g. given this certain set of criteria, can a user perform some action). 125 | 126 | The goal with HeimGuard is to easily manage user’s permissions around the feature access scope of permissions in your .NET apps using the built in .NET policies you’re familiar with. 127 | 128 | Out of the box with .NET, we can easily decorate our controllers like this `[Authorize(Policy = "RecipesFullAccess")]` and register it in `AddAuthorization`, but there's a gap here, **how do we check if the user has that claim?** 129 | 130 | One of the most common solutions to this is to load up your policies in your security token. 131 | 132 | Identity is the input to your permissions that, together, determine a user's permissions. 133 | 134 | ```json 135 | { 136 | "sub": "145hfy662", 137 | "name": "John Smith", 138 | "aud": ["api1", "api2"], 139 | "permission": [ 140 | "ManageRecipe", 141 | "CreateNewRecipe", 142 | "UpdateIngredients" 143 | ] 144 | } 145 | ``` 146 | 147 | This can work, but but there are some downsides here: 148 | 149 | - Your JWT gets quickly overloaded, potentially to the point of being too big to even put into a cookie. Ideally, your token is only passing along user identity information only. 150 | - You don't have boundary permission context. Let's look at a couple examples: 151 | - As mentioned above, we generally use the `aud` claim (or maybe some custom one) to determine what apis your security token can be used in. So in the example above we have ` "aud": ["api1", "api2"],` and one of my permissions is `ManageRecipe`. What if I am allowed to manage recipes in `api1` but not `api2`? You could prefix them with something like `api1.ManageRecipe`, but that adds coupling, domain logic, and becomes a huge multipler in the amount of claims being passed around. 152 | - Say I have a permission `CanDrinkAlcohol` but depending on where I’m at in the world it may or may not be true based on my age. I could tag it with something like `US.CanDrink`, `UK.CanDrink`, etc. but this would be far from ideal for a variety of reasons. 153 | - Tokens are only given at authentication time, so if you need to update permissions, you need to invalidate all the issued tokens every time you make an update. You could also make token lifetimes very short to get more up to date info more often, but that is not ideal either and still has coupling of identity and permissions. 154 | 155 | So, what do we do? Well we can still get identity state from our identity server like we usually do. Usually, that should include some kind of role or set of roles that the user has been assigned to. These roles can then be mapped to permissions and used as a reference to a group of permissions. 156 | 157 | > It’s important to note that these roles should be identity based and make sense across your whole domain, not just a particular boundary. For instance, something like `InventoryManager` would be better than something like `Approver`. 158 | 159 | So we have our user and their identity roles from our auth token, but how do we know what permissions go with our roles? Well, this can be done in a variety of ways to whatever suits your needs best for your api. 160 | 161 | If you have a simple API or an API that rarely has modified permissions, maybe you just want keep a static list of role to permissions mappings in a class in your project or in your appsettings. More commonly, you'll probably want to persist them in a database somewhere. This could be in your boundary/application database or it could be in a separate administration boundary. Maybe you have both and use eventual consistency to keep them in sync. You could even add a caching layer on top of this as well and reference that. 162 | 163 | At the end of the day, you can store your permission to role mappings anywhere you want, but you still need a way to easily access them and integrate them into your permissions pipeline. This is where HeimGuard comes in. 164 | 165 | ## Getting Started 166 | 167 | ### Prerequisites 168 | 169 | Before you get HeimGuard set up, make sure that your authorization policies are set up properly. There are two important items here: 170 | 171 | 1. Add an authorization attribute (e.g. `[Authorize(Policy = "RecipesFullAccess")]`) to your controller so HeimGuard knows what policy to check against. 172 | 173 | 2. Reigster your policy 174 | 175 | ```C# 176 | services.AddAuthorization(options => 177 | { 178 | options.AddPolicy("RecipesFullAccess", 179 | policy => policy.RequireClaim("permission", "RecipesFullAccess")); 180 | }); 181 | ``` 182 | 183 | > 🎉 Note that #2 isn't required if you are using [MapAuthorizationPolicies](#mapauthorizationpolicies). 184 | 185 | So for this example, let's say we have a controller like so: 186 | 187 | ```c# 188 | using HeimGuard; 189 | using Microsoft.AspNetCore.Authorization; 190 | using Microsoft.AspNetCore.Mvc; 191 | 192 | [ApiController] 193 | [Route("recipes")] 194 | [Authorize(Policy = "RecipesFullAccess")] 195 | public class RecipesController : ControllerBase 196 | { 197 | [HttpGet] 198 | public IActionResult Get() 199 | { 200 | return Ok() 201 | } 202 | } 203 | ``` 204 | 205 | 206 | 207 | ### Setting Up a Permissions Store 208 | 209 | To start out, you're going to set up whatever store you want to use for your roles. This could take pretty much whatever structure you want, the only requirement here is that **a permission must be able to be narrowed down to a string that can be used in our authorization attribute.** 210 | 211 | Let's look at a couple different examples of how we might store our permissions. 212 | 213 | > 🔮 The examples below are mapping permissions to roles, but this isn't a requirement. You could just as easily associate permissions to users or even apply permissions to users as well as roles. 214 | 215 | #### Simple Static Class Store 216 | 217 | As shown in the quickstart, maybe we have a really simple policy that we just want to store in our project. We could just make a `Permission` class that has some roles associated to it and a store to access it. 218 | 219 | You could also make static strings that get used here and throughout your app to prevent spelling issues. Again, lots of flexibility here. 220 | 221 | ```c# 222 | public class Permission 223 | { 224 | public string Name { get; set; } 225 | public List Roles { get; set; } 226 | } 227 | 228 | public static class SimplePermissionStore 229 | { 230 | public static List GetPermissions() 231 | { 232 | return new List() 233 | { 234 | new() 235 | { 236 | Name = "RecipesFullAccess", 237 | Roles = new List() { "Chef" } 238 | } 239 | }; 240 | } 241 | } 242 | ``` 243 | 244 | #### Database Store 245 | 246 | We could also have some entities that we are storing in our application database or maybe in a separate administration boundary. Notice here how our permissions have a Guid as their key, but we can still get a string out of it using `Name` for our authorization attribute. 247 | 248 | ```c# 249 | using System.ComponentModel.DataAnnotations; 250 | 251 | public class Role 252 | { 253 | [Key] 254 | public Guid Id { get; set; } 255 | public string Name { get; set; } 256 | } 257 | 258 | public class Permission 259 | { 260 | [Key] 261 | public Guid Id { get; set; } 262 | public string Name { get; set; } 263 | } 264 | 265 | public class RolePermission 266 | { 267 | [Key] 268 | public Guid Id { get; set; } 269 | 270 | [JsonIgnore] 271 | [IgnoreDataMember] 272 | [ForeignKey("Role")] 273 | public Guid RoleId { get; set; } 274 | public Role Role { get; set; } 275 | 276 | [JsonIgnore] 277 | [IgnoreDataMember] 278 | [ForeignKey("Permission")] 279 | public Guid PermissionId { get; set; } 280 | public Permission Permission { get; set; } 281 | } 282 | ``` 283 | 284 | 285 | 286 | ### Implementing a Policy Handler 287 | 288 | Now that we have a store set up, we need to determine how we get our final list of permissions for a given user. 289 | 290 | To do this, we are going to create a class that inherits from HeimGaurd's `IUserPolicyHandler` and implements a method called `GetUserPermissions`. This method will do whatever logic you need to perform to get the permissions for you user. It could do any of, but not limited to the following: 291 | 292 | 1. Check a static file or database for permissions assigned to a user 293 | 2. Get a user's roles and then reach 294 | 3. ping a database and 295 | 296 | Again, the goal here is to get a list of permissions for my user, particularly as an `IEnumerable`. 297 | 298 | #### Simple Static Class IUserPolicyHandler Example 299 | 300 | For our [Simple Static Class Store](#simple-static-class-store) example above, we have 3 main steps: 301 | 302 | 1. Get out user from our `ClaimsPrincipal` using `IHttpContextAccessor`. You could inject a `CurrentUserService` or whatever else here to accomplish this. 303 | 2. Get the given roles for that user from their token. Again, these roles could instead be stored in a database or static file as well. You could not even use roles at all and map permissions directly to a user. 304 | 3. Get the permissions assigned to that role from our static list. 305 | 306 | ```c# 307 | 308 | public class SimpleUserPolicyHandler : IUserPolicyHandler 309 | { 310 | private readonly IHttpContextAccessor _httpContextAccessor; 311 | 312 | public UserPolicyHandler(IHttpContextAccessor httpContextAccessor) 313 | { 314 | _httpContextAccessor = httpContextAccessor; 315 | } 316 | 317 | public async Task> GetUserPermissions() 318 | { 319 | var user = _httpContextAccessor.HttpContext?.User; 320 | if (user == null) throw new ArgumentNullException(nameof(user)); 321 | 322 | // this gets the user's role(s) from their ClaimsPrincipal 323 | var roles = user 324 | .Claims.Where(c => c.Type == ClaimTypes.Role) 325 | .Select(r => r.Value) 326 | .ToArray(); 327 | 328 | // this gets their permissions based on their roles. in this example, it's just using a static list 329 | var permissions = SimplePermissionStore.GetPermissions() 330 | .Where(p => p.Roles.Any(r => roles.Contains(r))) 331 | .Select(p => p.Name) 332 | .ToArray(); 333 | 334 | return await Task.FromResult(permissions.Distinct()); 335 | } 336 | } 337 | ``` 338 | 339 | #### Database Static Class IUserPolicyHandler Example 340 | 341 | For our [Database Store](#database-store) example above, we have the same 3 steps, just implemented slightly differently to accomodate our database. As a matter of fact, the only difference here is the `var permissions` assignment and injecting my `DbContext`. This is only because I have a similar pattern for both stores, yours could look very different depending on your schema. You could also use a repo, built in method, etc to perform this action and make it more testable. 342 | 343 | **However it is implemented, the only thing that matters here is returning a list of strings as your final permissions.** 344 | 345 | ```c# 346 | public class DatabaseUserPolicyHandler : IUserPolicyHandler 347 | { 348 | private readonly RecipesDbContext _dbContext; 349 | private readonly IHttpContextAccessor _httpContextAccessor; 350 | 351 | public UserPolicyHandler(RecipesDbContext dbContext, IHttpContextAccessor httpContextAccessor) 352 | { 353 | _dbContext = dbContext; 354 | _httpContextAccessor = httpContextAccessor; 355 | } 356 | 357 | public async Task> GetUserPermissions() 358 | { 359 | var user = _httpContextAccessor.HttpContext?.User; 360 | if (user == null) throw new ArgumentNullException(nameof(user)); 361 | 362 | var roles = user.Claims 363 | .Where(c => c.Type == ClaimTypes.Role) 364 | .Select(r => r.Value) 365 | .ToArray(); 366 | 367 | var permissions = await _dbContext.RolePermissions 368 | .Where(rp => roles.Contains(rp.Role.Name)) 369 | .Select(rp => rp.Permission.Name) 370 | .ToArrayAsync(); 371 | 372 | return await Task.FromResult(permissions.Distinct()); 373 | } 374 | } 375 | ``` 376 | 377 | #### Enhancing Your `HasPermission` Checks 378 | 379 | The method `GetUserPermissions()` method requires all permissions for a user to be returned. When using a database, you might want to enhance the permformance of this call. You can do this by having your db use an `Exists` operation. To do this, you can implement the `HasPermission` method on your `UserPolicyHandler`. For example: 380 | 381 | ```csharp 382 | 383 | public class DatabaseUserPolicyHandler : IUserPolicyHandler 384 | { 385 | private readonly RecipesDbContext _dbContext; 386 | private readonly IHttpContextAccessor _httpContextAccessor; 387 | 388 | public UserPolicyHandler(RecipesDbContext dbContext, IHttpContextAccessor httpContextAccessor) 389 | { 390 | _dbContext = dbContext; 391 | _httpContextAccessor = httpContextAccessor; 392 | } 393 | 394 | public async Task> GetUserPermissions() 395 | { 396 | // ... 397 | } 398 | 399 | public async Task HasPermission(string permission) 400 | { 401 | var roles = await GetRoles(); 402 | 403 | // super admins can do everything 404 | if (roles.Contains(Role.SuperAdmin().Value)) 405 | return true; 406 | 407 | return await _dbContext.RolePermissions 408 | .Where(rp => roles.Contains(rp.Role.Name)) 409 | .Select(rp => rp.Permission.Name) 410 | .AnyAsync(x => x == permission); 411 | } 412 | } 413 | ``` 414 | 415 | 416 | 417 | ### Registering HeimGuard 418 | 419 | Once you have your `IUserPolicyHandler` implementation set up, just go to your service builder and register HeimGuard like so: 420 | 421 | ```c# 422 | public void ConfigureServices(IServiceCollection services) 423 | { 424 | //... 425 | services.AddHeimGuard() 426 | .AutomaticallyCheckPermissions() 427 | .MapAuthorizationPolicies(); 428 | // OR... 429 | services.AddHeimGuard() 430 | .AutomaticallyCheckPermissions() 431 | .MapAuthorizationPolicies(); 432 | } 433 | ``` 434 | 435 | And that's it! I've added a couple extension methods on here as they are recommended by default, but they are not required. For more details, check out [HeimGuard Enhancements](#heimguard-enhancements). 436 | 437 | ## HeimGuard Enhancements 438 | 439 | There are currently two extensions on HeimGuard that are both optional, but depending on your workflow, may save you a lot of manual work. 440 | 441 | ### AutomaticallyCheckPermissions 442 | 443 | - `AutomaticallyCheckPermissions` will automatically checks user permissions when an authorization attribute is used. Again, this is optional, but without this, we would need 444 | - to add something like this to our controller or service/handler that it calls: 445 | 446 | ```csharp 447 | using HeimGuard; 448 | using Microsoft.AspNetCore.Authorization; 449 | using Microsoft.AspNetCore.Mvc; 450 | 451 | [ApiController] 452 | [Route("recipes")] 453 | [Authorize] 454 | public class RecipesController : ControllerBase 455 | { 456 | private readonly IHeimGuardClient _heimGuard; 457 | 458 | public RecipesController(IHeimGuardClient heimGuard) 459 | { 460 | _heimGuard = heimGuard; 461 | } 462 | 463 | [HttpGet] 464 | public IActionResult Get() 465 | { 466 | return _heimGuard.HasPermissionAsync("RecipesFullAccess") 467 | ? Ok() 468 | : Forbidden(); 469 | 470 | // OR... 471 | 472 | await _heimGuard.MustHavePermission(Permissions.CanAddRecipe); 473 | return Ok(); 474 | } 475 | } 476 | ``` 477 | 478 | ### MapAuthorizationPolicies 479 | 480 | - `MapAuthorizationPolicies` will automatically map authorization attributes to ASP.NET Core authorization policies that haven't already been mapped. That means you don't have to do something like this for all your policies: 481 | 482 | ```csharp 483 | services.AddAuthorization(options => 484 | { 485 | options.AddPolicy("RecipesFullAccess", 486 | policy => policy.RequireClaim("permission", "RecipesFullAccess")); 487 | }); 488 | ``` 489 | 490 | > 🧳 Note that if you manually register anything in here it will take presidence over the dynamically added policy. 491 | 492 | ## Custom Policies 493 | 494 | Custom policies can still be written and used as they normally would be in .NET. **Be careful here in that these can get to the grey area of business logic vs authorization.** 495 | 496 | * [Microsoft's docs for one requirement](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-6.0#use-a-handler-for-one-requirement) 497 | * [Microsoft's docs for multiple requirements](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-6.0#use-a-handler-for-multiple-requirements) 498 | 499 | Generally: 500 | 501 | 1. Write a custom requirement that extends Microsoft's `IAuthorizationRequirement` 502 | 2. Write a handler for that requirement so that any invoked policy that has the custom requirement in it will leverage it. 503 | * You can use HeimGuard DI in these handlers to easily check if the given user has the permission at all and then perform your custom requirement checks. 504 | 3. Register that handler in startup 505 | 4. Set up your controller 506 | 507 | ### ❗️ Important Note 508 | 509 | It's important to note that custom policies can not be automatically resolved with `AutomaticallyCheckPermissions`. That doesn't mean that you have to remove `AutomaticallyCheckPermissions` if you use any custom policies, but you'll need to be deliberate with how you set up your controllers. Sepcifically, you can still add the `Authorize` attribute, but you won't pass it a policy like you normally would. Instead, you'll build the custom requirement and involk your custom handler, which could (and likely should) leverage HeimGuard with DI. 510 | 511 | ## Tenants 512 | 513 | When working in a multitenant app, you might end up having different roles across different tenants. For example, say I am an `Admin` in Organization 1, but a `User` in Organization 2. The `Admin` role will likely add a lot of permissions that the user role wouldnt have, but how do we check what organization the user is in for that particular request? 514 | 515 | If your token is configured to have only your current tenant context (e.g. when I logged in my token only got populated with my roles for `Organization 1`, even though I have access ti other organiations), you can grab that claim from your token and use it in your `IUserPolicyHandler` implementation. 516 | 517 | Many times this won't be the case though. If you don't know what your tenant context until later in the process, it will generally be easiest to check permissions without using the `Authorize` attribute at all and strictly checking using this method as a stand alone option. Otherwise, you don't have the context to know what tenant you are working with. 518 | 519 | For example, you could add a method to you `IUserPolicyHandler` (or a new service) that can take in a user and get their permissions based on their tenant (i.e. organization). 520 | 521 | ```c# 522 | public bool GetUserPermissionsByTenant(Guid tenantId) 523 | { 524 | var userId = _currentUserService.GetUserId(); 525 | if (userId == null) throw new ArgumentNullException(nameof(userId)); 526 | 527 | var roles = _dbContext.UserTenantRoles 528 | .Where(utr => utr.TenantId == tenantId && utr.UserId == userId) 529 | .Select(utr => utr.Role.Name) 530 | .ToArray(); 531 | 532 | var permissions = await _dbContext.RolePermissions 533 | .Where(rp => roles.Contains(rp.Role.Name)) 534 | .Select(rp => rp.Permission.Name) 535 | .ToArrayAsync(); 536 | 537 | return await Task.FromResult(permissions.Distinct()); 538 | } 539 | ``` 540 | 541 | Then you could call this inside of your controller or in your CQRS handler. 542 | 543 | > 🧢 It's worth noting that at the end of the day, this approach isn't leveraging anything in HeimGuard, so if you need something like this throughout your whole app, then it's probably not even worth bothering with HeimGuard. 544 | 545 | ## Caching 546 | 547 | A potentially downside to this approach of permission mapping is that it can get chatty. If this is causing performance issues for you, one option might be to use a redis cache in your `IUserPolicyHandler` implementation. 548 | 549 | ## Multiple Policies Per Attribute 550 | 551 | What if you want to assign multiple policies to a single authorization attribute? At that point, your going to want to build a [custom policy assertion](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies) using a function. 552 | 553 | ```cs 554 | options.AddPolicy("ThisThingOrThatThing", policy => 555 | policy.RequireAssertion(context => 556 | context.User.HasClaim(c => 557 | (c.Type == "ThisThing" || 558 | c.Type == "ThatThing")))); 559 | ``` 560 | 561 | Alternatively, you can use the just have the `Authorize` attribute on a controller and manually check for permissions in your controller, handler, service, etc. like so. 562 | ```csharp 563 | public async Task Handle(AddRecipeCommand request, CancellationToken cancellationToken) 564 | { 565 | if(!await _heimGuard.HasPermissionAsync(Permissions.ThisThing) && !await _heimGuard.HasPermissionAsync(Permissions.ThisThing)) 566 | throw new ForbiddenAccessException(); 567 | 568 | var recipe = Recipe.Create(request.RecipeToAdd); 569 | await _recipeRepository.Add(recipe, cancellationToken); 570 | 571 | await _unitOfWork.CommitChanges(cancellationToken); 572 | 573 | var recipeAdded = await _recipeRepository.GetById(recipe.Id, cancellationToken: cancellationToken); 574 | return _mapper.Map(recipeAdded); 575 | } 576 | 577 | ``` 578 | 579 | 580 | ## Example 581 | Check out [this example project](https://github.com/pdevito3/HeimGuardExamplePermissions) for one of many options for setting up HeimGuard 582 | -------------------------------------------------------------------------------- /src/HeimGuard/AutoPolicy/HeimGuardAuthorizationPolicyProvider.cs: -------------------------------------------------------------------------------- 1 | namespace HeimGuard.AutoPolicy 2 | { 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.Extensions.Options; 6 | 7 | public class HeimGuardAuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider 8 | { 9 | /// 10 | /// Initializes a new instance of the class. 11 | /// 12 | /// The options. 13 | public HeimGuardAuthorizationPolicyProvider(IOptions options) : base(options) 14 | { 15 | } 16 | 17 | /// 18 | /// Gets a from the given 19 | /// 20 | /// The policy name to retrieve. 21 | /// 22 | /// The named . 23 | /// 24 | public override async Task GetPolicyAsync(string policyName) 25 | { 26 | // check static policies and add it if it isn't already there 27 | var policy = await base.GetPolicyAsync(policyName) ?? new AuthorizationPolicyBuilder() 28 | .AddRequirements(new PermissionRequirement(policyName)) 29 | .Build(); 30 | 31 | return policy; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/HeimGuard/AutoPolicy/PermissionHandler.cs: -------------------------------------------------------------------------------- 1 | namespace HeimGuard.AutoPolicy 2 | { 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Authorization; 5 | 6 | internal class PermissionRequirement : IAuthorizationRequirement 7 | { 8 | public PermissionRequirement(string name) 9 | { 10 | Name = name; 11 | } 12 | public string Name { get; private set; } 13 | } 14 | 15 | internal class PermissionHandler : AuthorizationHandler 16 | { 17 | private readonly IHeimGuardClient _guardClient; 18 | 19 | public PermissionHandler(IHeimGuardClient guardClient) 20 | { 21 | _guardClient = guardClient; 22 | } 23 | 24 | /// 25 | /// Uses HeimGuard permission check to automatically confirm that a user has the appropriate permissions given a particular context. 26 | /// 27 | protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) 28 | { 29 | // no cancellation token available on AuthorizationHandler: https://github.com/aspnet/Security/issues/1598 30 | if (await _guardClient.HasPermissionAsync(requirement.Name)) 31 | { 32 | context.Succeed(requirement); 33 | } 34 | } 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/HeimGuard/HeimGuard.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0 5 | 6 | 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 | 36 | 1.1.0 37 | 1.1.0 38 | 1.1.0 39 | MIT 40 | https://github.com/pdevito3/heimguard 41 | git 42 | ./nupkg 43 | false 44 | Paul DeVito 45 | A small and simple library that allows you to easily manage permissions in your .NET projects. 46 | true 47 | HeimGuard 48 | HeimGuard 49 | https://github.com/pdevito3/heimguard 50 | permission permissions authorization policy policies role roles 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/HeimGuard/HeimGuardBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace HeimGuard 2 | { 3 | using AutoPolicy; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | /// 8 | /// Builder for HeimGuard DI 9 | /// 10 | public class HeimGuardBuilder 11 | { 12 | public IServiceCollection Services { get; } 13 | 14 | public HeimGuardBuilder(IServiceCollection services) 15 | { 16 | Services = services; 17 | } 18 | 19 | /// 20 | /// Automatically maps authorization attributes to ASP.NET Core authorization policies that haven't already been 21 | /// added. 22 | /// 23 | public HeimGuardBuilder MapAuthorizationPolicies() 24 | { 25 | Services.AddAuthorizationCore(); 26 | Services.AddTransient(); 27 | 28 | return this; 29 | } 30 | 31 | /// 32 | /// Automatically checks user permissions when an authorization attribute is used. 33 | /// 34 | public HeimGuardBuilder AutomaticallyCheckPermissions() 35 | { 36 | Services.AddHttpContextAccessor(); 37 | Services.AddTransient(); 38 | 39 | return this; 40 | } 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/HeimGuard/HeimGuardClient.cs: -------------------------------------------------------------------------------- 1 | namespace HeimGuard 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Security.Claims; 7 | using System.Threading.Tasks; 8 | 9 | /// 10 | /// The handler that interacts with the policy handler to expose guard methods for your user permissions. 11 | /// 12 | public interface IHeimGuardClient 13 | { 14 | /// 15 | /// Checks if the user has a particular permission. 16 | /// 17 | /// 18 | /// A task that represents the asynchronous permission check. The task result contains 19 | /// the true if the user has the given permission and false if they do not. 20 | Task HasPermissionAsync(string permission); 21 | 22 | /// 23 | /// Guards against users without the given permission. Throws an Exception of type 24 | /// if the user does not have the given permission. Does nothing if the 25 | /// user has the given permission. 26 | /// 27 | /// The name of the permission the user must have access to. 28 | /// The Exception that should be thrown if a user does not have the given permission. 29 | /// User does not have the given permission. 30 | Task MustHavePermission(string permission) 31 | where TException : Exception, new(); 32 | } 33 | 34 | /// 35 | public class HeimGuardClient : IHeimGuardClient 36 | { 37 | private readonly IUserPolicyHandler _userPolicyHandler; 38 | 39 | /// 40 | /// Initializes a new instance of the class. 41 | /// 42 | public HeimGuardClient(IUserPolicyHandler userPolicyHandler) 43 | { 44 | _userPolicyHandler = userPolicyHandler; 45 | } 46 | 47 | /// 48 | public async Task HasPermissionAsync(string permission) 49 | => await _userPolicyHandler.HasPermission(permission); 50 | 51 | /// 52 | public async Task MustHavePermission(string permission) 53 | where TException : Exception, new() 54 | { 55 | if (!await HasPermissionAsync(permission)) 56 | { 57 | try 58 | { 59 | throw Activator.CreateInstance(typeof(TException), permission) as TException; 60 | } 61 | catch (MissingMethodException) 62 | { 63 | throw new TException(); 64 | } 65 | } 66 | } 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/HeimGuard/HeimGuardServiceRegistration.cs: -------------------------------------------------------------------------------- 1 | namespace HeimGuard 2 | { 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | public static class HeimGuardServiceRegistration 6 | { 7 | /// 8 | /// Adds HeimGuard service to apply permissions based on the current user's policy. 9 | /// 10 | /// The services. 11 | /// 12 | public static HeimGuardBuilder AddHeimGuard(this IServiceCollection services) 13 | where TUserPolicyHandler : class, IUserPolicyHandler 14 | { 15 | services.AddScoped(); 16 | services.AddTransient(); 17 | 18 | return new HeimGuardBuilder(services); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/HeimGuard/IUserPolicyHandler.cs: -------------------------------------------------------------------------------- 1 | namespace HeimGuard 2 | { 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | /// 9 | /// Provides an abstraction for handling the role:permission mappings for a given user in am IEnumerable of strings. 10 | /// 11 | public interface IUserPolicyHandler 12 | { 13 | /// 14 | /// Returns an IEnumerable of strings that represents a distinct list of a given user's permissions. 15 | /// 16 | /// that represents the asynchronous operation, containing the list of permissions for the current user. 17 | Task> GetUserPermissions(); 18 | 19 | /// 20 | /// Returns a boolean value indicating whether the current user has the specified permission. 21 | /// 22 | /// that represents the asynchronous operation, containing a boolean indicating whether the current user has the specified permission. 23 | async Task HasPermission(string permission) => (await GetUserPermissions()).Contains(permission); 24 | } 25 | } 26 | --------------------------------------------------------------------------------