From cf651aa65c2c6c67433bf9f6ec4eb3d4f0ff46f7 Mon Sep 17 00:00:00 2001 From: iammukeshm Date: Tue, 3 Nov 2026 22:11:48 +0530 Subject: [PATCH 1/2] docs(identity): group + endpoint permission gates stack; profile PUT has no email field (#1429, #1431) --- src/content/docs/changelog/index.mdx | 2 ++ src/content/docs/modules/identity.mdx | 4 ++-- src/content/docs/security/authorization.mdx | 14 +++++++++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/content/docs/changelog/index.mdx b/src/content/docs/changelog/index.mdx index 233ea7f8..bca47240 100644 --- a/src/content/docs/changelog/index.mdx +++ b/src/content/docs/changelog/index.mdx @@ -13,6 +13,8 @@ Notable changes to the kit, newest first. ## 2026-11-03 +- **Authorization: a route group's `.RequirePermission()` still applies when an endpoint inside it adds its own (fix).** `RequiredPermissionAuthorizationHandler` read only the most specific permission metadata, so `.RequirePermission(b)` on an endpoint replaced `.RequirePermission(a)` on its route group and a user holding only `b` got through. The handler now combines every permission gate on the endpoint and requires all of them, the same all-of rule as `.RequirePermission(a, b)`. A blank gate anywhere in the chain denies the request. No kit endpoint gates a route group today, so nothing changes for the shipped API; your own group-gated endpoints become stricter. See [Authorization](/docs/security/authorization/#applying-a-gate), [#1429](https://github.com/fullstackhero/dotnet-starter-kit/issues/1429) and [#1452](https://github.com/fullstackhero/dotnet-starter-kit/pull/1452). +- **Identity: `PUT /api/v1/identity/profile` no longer has an `email` field (breaking, fix).** `UpdateUserCommand.Email` was validated as an e-mail address but never saved, so a client that sent a new address got a `200` and nothing changed. Changing an e-mail needs a confirmation flow, so the field is removed from the command and its validator rather than wired through. The API's JSON options skip unknown properties, so a client that still sends `email` gets the same `200` and unchanged address as before, but a malformed address is no longer rejected with a `400`. The dashboard's profile save stopped sending it. See [Identity](/docs/modules/identity/) and [#1431](https://github.com/fullstackhero/dotnet-starter-kit/issues/1431). - **Frontend containers: the inactivity timeout is configurable from the environment (fix).** Both apps read `inactivityIdleMs` and `inactivityWarningMs` from `/config.json`, but the container's `config.json.template` never carried them, so an operator could not change the auto sign-out time without rebuilding the image. The template now renders them from `FSH_INACTIVITY_IDLE_MS` and `FSH_INACTIVITY_WARNING_MS`. Unset or empty keeps the built-in timings (admin 10 minutes, dashboard 20, each with a 60-second warning), so existing deployments behave as before; a value that is not a positive integer stops the container at start with a clear error rather than rendering invalid JSON. `deploy/docker` exposes them as `FSH_ADMIN_INACTIVITY_IDLE_MS`, `FSH_DASHBOARD_INACTIVITY_IDLE_MS` and `FSH_INACTIVITY_WARNING_MS` in `.env`. The Terraform static-site `runtime_config` and the Aspire dev setup are unchanged. See [Frontend development](/docs/frontend/development/#the-container-pattern) and [#1442](https://github.com/fullstackhero/dotnet-starter-kit/pull/1442). - **Dashboard: the overview welcomes the tenant by its name (fix).** The first-run card on a tenant with no subscription read "Welcome to" followed by the tenant identifier (for example `qpb2edshbj`), and the header caption showed the same identifier, because both came from the token's `tenant` claim. Both now use the tenant name from `GET /api/v1/tenants/me/status`, which the overview already loads for its "Valid for" card, and fall back to the identifier while that call is loading or has failed. No backend change. See [Tenant dashboard](/docs/frontend/dashboard/#page-inventory) and [#1437](https://github.com/fullstackhero/dotnet-starter-kit/pull/1437). - **Both apps: non-ASCII claims in the access token now render correctly (fix).** `decodeJwt` decoded the token payload with a bare `atob`, which returns one character per byte, while the payload is UTF-8. A display name like "Comitê de Ética" therefore showed as "Comitê de Ética" in the user menu and the overview greeting, even though the token and the database were correct. The payload bytes are now decoded as UTF-8 in both the dashboard and the admin console. No configuration change. See PR [#1439](https://github.com/fullstackhero/dotnet-starter-kit/pull/1439). diff --git a/src/content/docs/modules/identity.mdx b/src/content/docs/modules/identity.mdx index ac95cf91..beb3d8d2 100644 --- a/src/content/docs/modules/identity.mdx +++ b/src/content/docs/modules/identity.mdx @@ -1,6 +1,6 @@ --- title: Identity module -lastUpdated: 2026-10-02 +lastUpdated: 2026-11-03 description: JWT bearer + refresh tokens, ASP.NET Identity with roles + permissions, user groups, operator impersonation, two-factor TOTP, sessions, and password-policy enforcement. sidebar: label: Identity @@ -189,7 +189,7 @@ Auth e-mail links resolve through `FrontendOptions` (a dedicated config, separat | POST | `/token/issue` | Login | | POST | `/token/refresh` | Rotate refresh token | | GET | `/profile` | Current user profile (returns a strong `ETag`) | -| PUT | `/profile` | Update own profile (honours `If-Match`, answers `412` on a stale token) | +| PUT | `/profile` | Update own profile (honours `If-Match`, answers `412` on a stale token). There is no e-mail field: an `email` in the body is ignored | | PUT | `/profile/image` | Set / clear profile image | | GET | `/permissions` | Current user permissions | | GET | `/permissions/catalog` | All registered permissions (catalog) | diff --git a/src/content/docs/security/authorization.mdx b/src/content/docs/security/authorization.mdx index e31957dd..a4977511 100644 --- a/src/content/docs/security/authorization.mdx +++ b/src/content/docs/security/authorization.mdx @@ -1,6 +1,6 @@ --- title: Authorization -lastUpdated: 2026-10-02 +lastUpdated: 2026-11-03 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 @@ -123,6 +123,18 @@ endpoints.MapPost("/products/{id}/stock", handler) 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. +Gates stack across route groups. A `.RequirePermission()` on a group and another on an endpoint inside it are both enforced, so the caller needs every permission from both: + +```csharp +var stock = endpoints.MapGroup("/stock") + .RequirePermission(CatalogPermissions.Products.Update); + +stock.MapPost("/adjust", handler) + .RequirePermission(CatalogPermissions.Products.AdjustStock); // needs Update AND AdjustStock +``` + +Before [#1429](https://github.com/fullstackhero/dotnet-starter-kit/issues/1429) the handler read only the most specific metadata entry, so the endpoint's gate replaced the group's and a user holding only `AdjustStock` got through. + ## 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. From 2c40693c57bdb8b1e95a92f9765386f8e1bbdae6 Mon Sep 17 00:00:00 2001 From: iammukeshm Date: Tue, 3 Nov 2026 22:17:25 +0530 Subject: [PATCH 2/2] docs(changelog): link the profile email PR (#1454) --- src/content/docs/changelog/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/docs/changelog/index.mdx b/src/content/docs/changelog/index.mdx index bca47240..0db40041 100644 --- a/src/content/docs/changelog/index.mdx +++ b/src/content/docs/changelog/index.mdx @@ -14,7 +14,7 @@ Notable changes to the kit, newest first. ## 2026-11-03 - **Authorization: a route group's `.RequirePermission()` still applies when an endpoint inside it adds its own (fix).** `RequiredPermissionAuthorizationHandler` read only the most specific permission metadata, so `.RequirePermission(b)` on an endpoint replaced `.RequirePermission(a)` on its route group and a user holding only `b` got through. The handler now combines every permission gate on the endpoint and requires all of them, the same all-of rule as `.RequirePermission(a, b)`. A blank gate anywhere in the chain denies the request. No kit endpoint gates a route group today, so nothing changes for the shipped API; your own group-gated endpoints become stricter. See [Authorization](/docs/security/authorization/#applying-a-gate), [#1429](https://github.com/fullstackhero/dotnet-starter-kit/issues/1429) and [#1452](https://github.com/fullstackhero/dotnet-starter-kit/pull/1452). -- **Identity: `PUT /api/v1/identity/profile` no longer has an `email` field (breaking, fix).** `UpdateUserCommand.Email` was validated as an e-mail address but never saved, so a client that sent a new address got a `200` and nothing changed. Changing an e-mail needs a confirmation flow, so the field is removed from the command and its validator rather than wired through. The API's JSON options skip unknown properties, so a client that still sends `email` gets the same `200` and unchanged address as before, but a malformed address is no longer rejected with a `400`. The dashboard's profile save stopped sending it. See [Identity](/docs/modules/identity/) and [#1431](https://github.com/fullstackhero/dotnet-starter-kit/issues/1431). +- **Identity: `PUT /api/v1/identity/profile` no longer has an `email` field (breaking, fix).** `UpdateUserCommand.Email` was validated as an e-mail address but never saved, so a client that sent a new address got a `200` and nothing changed. Changing an e-mail needs a confirmation flow, so the field is removed from the command and its validator rather than wired through. The API's JSON options skip unknown properties, so a client that still sends `email` gets the same `200` and unchanged address as before, but a malformed address is no longer rejected with a `400`. The dashboard's profile save stopped sending it. See [Identity](/docs/modules/identity/), [#1431](https://github.com/fullstackhero/dotnet-starter-kit/issues/1431) and [#1454](https://github.com/fullstackhero/dotnet-starter-kit/pull/1454). - **Frontend containers: the inactivity timeout is configurable from the environment (fix).** Both apps read `inactivityIdleMs` and `inactivityWarningMs` from `/config.json`, but the container's `config.json.template` never carried them, so an operator could not change the auto sign-out time without rebuilding the image. The template now renders them from `FSH_INACTIVITY_IDLE_MS` and `FSH_INACTIVITY_WARNING_MS`. Unset or empty keeps the built-in timings (admin 10 minutes, dashboard 20, each with a 60-second warning), so existing deployments behave as before; a value that is not a positive integer stops the container at start with a clear error rather than rendering invalid JSON. `deploy/docker` exposes them as `FSH_ADMIN_INACTIVITY_IDLE_MS`, `FSH_DASHBOARD_INACTIVITY_IDLE_MS` and `FSH_INACTIVITY_WARNING_MS` in `.env`. The Terraform static-site `runtime_config` and the Aspire dev setup are unchanged. See [Frontend development](/docs/frontend/development/#the-container-pattern) and [#1442](https://github.com/fullstackhero/dotnet-starter-kit/pull/1442). - **Dashboard: the overview welcomes the tenant by its name (fix).** The first-run card on a tenant with no subscription read "Welcome to" followed by the tenant identifier (for example `qpb2edshbj`), and the header caption showed the same identifier, because both came from the token's `tenant` claim. Both now use the tenant name from `GET /api/v1/tenants/me/status`, which the overview already loads for its "Valid for" card, and fall back to the identifier while that call is loading or has failed. No backend change. See [Tenant dashboard](/docs/frontend/dashboard/#page-inventory) and [#1437](https://github.com/fullstackhero/dotnet-starter-kit/pull/1437). - **Both apps: non-ASCII claims in the access token now render correctly (fix).** `decodeJwt` decoded the token payload with a bare `atob`, which returns one character per byte, while the payload is UTF-8. A display name like "Comitê de Ética" therefore showed as "Comitê de Ética" in the user menu and the overview greeting, even though the token and the database were correct. The payload bytes are now decoded as UTF-8 in both the dashboard and the admin console. No configuration change. See PR [#1439](https://github.com/fullstackhero/dotnet-starter-kit/pull/1439).