Since the last lesson, the Kladde knows who is signed in. Now it decides what this person may do. There are two jobs in the office: the office maintains contacts and appointments, and the manager may additionally delete contacts for good. These jobs become roles, the roles come from Entra ID, and the Kladde checks them at every endpoint and every page where it matters.
On top of that comes a change that's more important than any single role: from this lesson on, in the Kladde everything is protected that isn't explicitly public. Anyone who builds a new page and forgets about permissions builds a protected page, not an open one.
Requirements, policies, roles
ASP.NET Core makes permission decisions with three building blocks:
- A requirement is a single condition: “is signed in,” “has the role Manager,” “has the claim
kladde/departmentwith the value Sales.” - A policy is a named list of requirements. It's met when all the requirements are met.
- A role is a claim of type
ClaimTypes.Role(lesson 2.6.1).RequireRoleturns it into a requirement.
You create policies at startup, with AddAuthorizationBuilder:
builder.Services.AddAuthorizationBuilder()
.AddPolicy("Manager", policy => policy.RequireRole("Manager"))
.AddPolicy("Team", policy => policy.RequireRole("Office", "Manager"));
They're used by their name: on an endpoint with .RequireAuthorization("Manager"), on a page with
@attribute [Authorize(Policy = "Manager")]. There's also the short form [Authorize(Roles = "Manager")] without a policy. The Kladde
still uses policies: if the rule changes, say to “manager, but only with a second factor,” you change one spot in Program.cs and not
every attribute.
You need to know two rules to read policies:
- Several requirements in one policy apply together (and).
RequireRole("Manager").RequireClaim("kladde/department", "Sales")demands both. - Several roles in one
RequireRoleapply as alternatives (or). WithRequireRole("Office", "Manager"), one of the two is enough.
For everything that can't be written that way there's RequireAssertion with a condition of your own in C#. For rules that need data,
such as “only whoever created the appointment may move it,” there are requirements of your own with an AuthorizationHandler; that's
material for K4.
The following program checks four made-up people against four policies, entirely without HTTP. For that it asks the service
IAuthorizationService, which the middleware uses too. In k2-practice/2-6, create the file policies.cs:
// policies.cs: who meets which policy
#:sdk Microsoft.NET.Sdk.Web
#:property PublishAot=false
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.SetMinimumLevel(LogLevel.Warning);
builder.Services.AddAuthorizationBuilder()
.AddPolicy("Manager", policy => policy.RequireRole("Manager"))
.AddPolicy("Team", policy => policy.RequireRole("Office", "Manager"))
.AddPolicy("Sales", policy => policy.RequireAuthenticatedUser().RequireClaim("kladde/department", "Sales"))
.AddPolicy("ManagerInSales", policy => policy.RequireRole("Manager").RequireClaim("kladde/department", "Sales"));
await using WebApplication app = builder.Build();
var authorization = app.Services.GetRequiredService<IAuthorizationService>();
ClaimsPrincipal[] people =
[
Person("Ada Sample", ["Office"], null),
Person("Tom Wieland", ["Manager"], "Sales"),
Person("Ilse Kranich", ["Office"], "Sales"),
Person("Guest", [], null),
];
string[] policies = ["Manager", "Team", "Sales", "ManagerInSales"];
Console.WriteLine($"{"",-14}{string.Join("", policies.Select(p => p.PadRight(19)))}");
foreach (ClaimsPrincipal person in people)
{
Console.Write($"{person.Identity!.Name,-14}");
foreach (string policy in policies)
{
AuthorizationResult result = await authorization.AuthorizeAsync(person, policy);
Console.Write((result.Succeeded ? "yes" : "no").PadRight(19));
}
Console.WriteLine();
}
static ClaimsPrincipal Person(string name, string[] roles, string? department)
{
List<Claim> claims = [new(ClaimTypes.Name, name), .. roles.Select(r => new Claim(ClaimTypes.Role, r))];
if (department is not null)
{
claims.Add(new Claim("kladde/department", department));
}
return new ClaimsPrincipal(new ClaimsIdentity(claims, "Demo"));
}
WebApplication serves here only as a convenient way to a finished service container with everything authorization needs; no server is
started. Don't run the program yet. The exercise comes first.