Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/content/docs/building-blocks/shared.mdx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<string> 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

Expand Down
1 change: 1 addition & 0 deletions src/content/docs/changelog/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
11 changes: 10 additions & 1 deletion src/content/docs/security/authorization.mdx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand Down