diff --git a/src/content/docs/building-blocks/shared.mdx b/src/content/docs/building-blocks/shared.mdx index d0ed60d5..31910dc9 100644 --- a/src/content/docs/building-blocks/shared.mdx +++ b/src/content/docs/building-blocks/shared.mdx @@ -1,6 +1,6 @@ --- title: Shared building block -lastUpdated: 2026-06-11 +lastUpdated: 2026-10-02 description: Multi-tenancy types, permission registry, claim/role/action/resource constants, audit attributes, and shared DTOs used by every module. sidebar: label: Shared @@ -43,7 +43,7 @@ New permission? Add it to `PermissionConstants`. New claim type? Add it to `Clai - **`ClaimsPrincipalExtensions`** - extracts user id, tenant, email, etc. from a `ClaimsPrincipal`. Used in middleware and handlers. - **`RequiredPermissionAttribute`** - implements **`IRequiredPermissionMetadata`** (a `HashSet RequiredPermissions`). This interface is what the authorization handler keys off - it must exist exactly once, in `FSH.Framework.Shared.Identity.Authorization`. -- **`EndpointExtensions.RequirePermission(permission, params additionalPermissions)`** - the canonical fluent helper for minimal-API endpoints; it attaches a `RequiredPermissionAttribute` as endpoint metadata. Permissions are plain strings (modules expose them as `const string`s in their contracts). +- **`EndpointExtensions.RequirePermission(permission, params additionalPermissions)`** - the canonical fluent helper for minimal-API endpoints; it attaches a `RequiredPermissionAttribute` as endpoint metadata. When it lists several permissions the user needs every one of them. Permissions are plain strings (modules expose them as `const string`s in their contracts). ### Persistence shared diff --git a/src/content/docs/changelog/index.mdx b/src/content/docs/changelog/index.mdx index dcd82bf1..e6d98e83 100644 --- a/src/content/docs/changelog/index.mdx +++ b/src/content/docs/changelog/index.mdx @@ -13,6 +13,7 @@ Notable changes to the kit, newest first. ## 2026-10-02 +- **Authorization: `.RequirePermission(a, b)` now requires every listed permission (fix).** `RequiredPermissionAuthorizationHandler` checked only the first permission in the endpoint metadata, so a user holding `a` passed a gate that also demanded `b`. It now checks each one and succeeds only when the user holds all of them. A permission metadata entry with no permissions left (for example `.RequirePermission("")`) now denies the request; before, the handler threw `InvalidOperationException`. No kit endpoint lists more than one permission today, so nothing changes for the shipped API; your own multi-permission endpoints become stricter. See [Authorization](/docs/security/authorization/#applying-a-gate), [#1415](https://github.com/fullstackhero/dotnet-starter-kit/issues/1415) and [#1424](https://github.com/fullstackhero/dotnet-starter-kit/pull/1424). - **Mailing & Docker Compose: e-mail works out of the box, and SMTP connection security is configurable (fix).** `SmtpMailService` always connected with STARTTLS, so any server that does not offer it was refused before a single envelope was sent, and the compose stack inherited the `smtp.ethereal.email` host with empty credentials: every confirmation, password-reset and welcome e-mail failed, and a user registered by an operator could not sign in until someone confirmed the address by hand. A new **`MailOptions:Smtp:Security`** setting (MailKit `SecureSocketOptions`, by name: `None`, `Auto`, `SslOnConnect`, `StartTls`, `StartTlsWhenAvailable`) chooses the mode; it defaults to `StartTls`, so existing configuration behaves exactly as before, and an unknown name fails the options binding instead of guessing. `deploy/docker/docker-compose.yml` now reads the SMTP target from `.env` (`FSH_SMTP_HOST`, `FSH_SMTP_PORT`, `FSH_SMTP_SECURITY`, `FSH_SMTP_USERNAME`, `FSH_SMTP_PASSWORD`), defaulting to a pinned [Mailpit](https://mailpit.axllent.org) catcher (`axllent/mailpit:v1.31.3`, `Security` set to `None`) that runs only under the `mail-catcher` compose profile, which `.env.example` enables with `COMPOSE_PROFILES=mail-catcher`. With that default, e-mail is caught, not delivered. SMTP stays on the compose network and the inbox UI is published on the host loopback only, at `http://localhost:8025` (`FSH_MAILPIT_PORT`), because it holds live reset and confirmation links. For real delivery, edit `.env` only: set `FSH_SMTP_*` to your provider, set `FSH_MAIL_FROM` to a sender it accepts (the compose default is `no-reply@fsh.local`, because `appsettings.Production.json` leaves `MailOptions:From` blank), and delete the `COMPOSE_PROFILES` line. See [Mailing](/docs/building-blocks/mailing/) and [#1409](https://github.com/fullstackhero/dotnet-starter-kit/pull/1409). - **Docker Compose: no more `libgssapi_krb5.so.2` errors in the API and migrator logs (fix).** The chiseled .NET images do not ship the Kerberos library, and Npgsql tries GSS encryption by default (`GSS Encryption Mode=Prefer`), so the first connection logged `Cannot load library libgssapi_krb5.so.2` on every start. Both compose connection strings now append `GSS Encryption Mode=Disable`; keep it when you swap in your own Postgres unless the server uses Kerberos. See [Troubleshooting](/docs/getting-started/troubleshooting/) and [#1409](https://github.com/fullstackhero/dotnet-starter-kit/pull/1409). diff --git a/src/content/docs/security/authorization.mdx b/src/content/docs/security/authorization.mdx index eaec1779..e31957dd 100644 --- a/src/content/docs/security/authorization.mdx +++ b/src/content/docs/security/authorization.mdx @@ -1,6 +1,6 @@ --- title: Authorization -lastUpdated: 2026-06-11 +lastUpdated: 2026-10-02 description: Permission-based authorization - fine-grained gates on every endpoint via .RequirePermission() with a flat registry, default + fallback policy wiring, and the silent-no-op gotcha that catches every team once. sidebar: label: Authorization @@ -114,6 +114,15 @@ The equivalent attribute form: Both produce the same metadata - `.RequirePermission()` is literally `WithMetadata(new RequiredPermissionAttribute(...))`. `RequiredPermissionAuthorizationHandler` reads it from the endpoint's metadata via the `IRequiredPermissionMetadata` interface and checks the user's effective permission set. Endpoints with **no** permission metadata pass the handler (authentication is still enforced by the policy itself). +Passing more than one permission requires **all** of them (AND, not OR): + +```csharp +endpoints.MapPost("/products/{id}/stock", handler) + .RequirePermission(CatalogPermissions.Products.Update, CatalogPermissions.Products.AdjustStock); +``` + +A user holding only one of the two gets `403`. Before [#1415](https://github.com/fullstackhero/dotnet-starter-kit/issues/1415) the handler checked only the first permission and ignored the rest. Blank permission strings are dropped from the metadata; if nothing is left (for example `.RequirePermission("")`), the gate denies every request instead of opening. + ## Roles, groups, and aggregation - **Roles** are flat (no inheritance hierarchy). A user can be in multiple roles; the user's effective permission set is the **union** of every role's permission claims.