Compare commits
5 Commits
docs/prose-audit
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5df2ef0d1e | |||
| e5785fd769 | |||
| 22370ca4da | |||
| e0a3fbf35b | |||
| 161ed6f80d |
@@ -100,7 +100,7 @@ When source code changes, build and test the affected component before reporting
|
|||||||
## Design Sources To Consult Before Non-Trivial Changes
|
## Design Sources To Consult Before Non-Trivial Changes
|
||||||
|
|
||||||
- `gateway.md` — top-level architecture, command/event surface, IPC envelope, STA thread model, fault handling.
|
- `gateway.md` — top-level architecture, command/event surface, IPC envelope, STA thread model, fault handling.
|
||||||
- `glauth.md` — local LDAP server (GLAuth on `localhost:3893`, base DN `dc=zb,dc=local`) used for dev authn. Pre-provisioned users (`admin/admin123`, `readonly/readonly123`, etc.) and the role→capability mapping live there.
|
- `glauth.md` — shared GLAuth LDAP server (`10.100.0.35:3893`, base DN `dc=zb,dc=local`, source of truth `scadaproj/infra/glauth/`) used for dev authn. Dashboard test users (`multi-role`/`password` = Administrator, `gw-viewer`/`password` = Viewer) and the role→capability mapping live there.
|
||||||
- `docs/DesignDecisions.md` — v1 choices (MXAccess COM target `LMXProxyServerClass` from `C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll`, API-key-in-SQLite auth, fail-fast event backpressure, etc.).
|
- `docs/DesignDecisions.md` — v1 choices (MXAccess COM target `LMXProxyServerClass` from `C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll`, API-key-in-SQLite auth, fail-fast event backpressure, etc.).
|
||||||
- `docs/GatewayProcessDesign.md`, `docs/MxAccessWorkerInstanceDesign.md`, `docs/WorkerFrameProtocol.md`, `docs/WorkerProcessLauncher.md` — detailed component designs.
|
- `docs/GatewayProcessDesign.md`, `docs/MxAccessWorkerInstanceDesign.md`, `docs/WorkerFrameProtocol.md`, `docs/WorkerProcessLauncher.md` — detailed component designs.
|
||||||
- `docs/GatewayConfiguration.md` — full `MxGateway:*` options bound by `GatewayOptions` and validated at startup by `GatewayOptionsValidator`.
|
- `docs/GatewayConfiguration.md` — full `MxGateway:*` options bound by `GatewayOptions` and validated at startup by `GatewayOptionsValidator`.
|
||||||
|
|||||||
@@ -1,27 +1,36 @@
|
|||||||
# GLAuth — LDAP authn reference for mxaccessgw
|
# GLAuth — LDAP authn reference for mxaccessgw
|
||||||
|
|
||||||
GLAuth is a lightweight LDAP server installed on this dev box at
|
> **UPDATED 2026-06-04 — mxaccessgw no longer uses a per-box GLAuth at `C:\publish\glauth`.
|
||||||
`C:\publish\glauth\` and run as a Windows service via NSSM. It already
|
> Dev/test LDAP is now the SHARED GLAuth on `10.100.0.35:3893` (`dc=zb,dc=local`);
|
||||||
backs the LmxOpcUa OPC UA server's UserName-token authn and the LmxOpcUa
|
> the single source of truth is `scadaproj/infra/glauth/` (`config.toml` + `README`).
|
||||||
Admin UI's cookie login; this doc captures everything mxaccessgw needs
|
> The localhost/NSSM/`glauth.cfg` procedures below are RETIRED, kept for reference/rollback.**
|
||||||
to consume the same directory so a single set of dev credentials covers
|
|
||||||
both stacks.
|
|
||||||
|
|
||||||
The authoritative copy of LmxOpcUa's reference lives at
|
GLAuth is a lightweight LDAP server. It already backs all three sister apps (MxAccessGateway,
|
||||||
`C:\publish\glauth\auth.md`. This doc is a redistilled view tailored to
|
OtOpcUa, ScadaBridge) through a **shared container** (`zb-shared-glauth`) running on the Linux
|
||||||
mxaccessgw — what users + groups are already provisioned, how to bind
|
docker host at **`10.100.0.35:3893`**. This doc captures everything mxaccessgw needs to consume
|
||||||
against them, and what's needed to add a gw-specific role.
|
that directory so a single set of dev credentials covers all stacks.
|
||||||
|
|
||||||
|
~~GLAuth is installed on this dev box at `C:\publish\glauth\` and run as a Windows service via
|
||||||
|
NSSM.~~ *(RETIRED — the per-box Windows service has been stopped and set to Manual startup;
|
||||||
|
kept only as a rollback option. Do not edit or restart it for new work.)*
|
||||||
|
|
||||||
|
The single source of truth for the shared GLAuth is
|
||||||
|
**`~/Desktop/scadaproj/infra/glauth/config.toml`** (deploy/verify runbook:
|
||||||
|
`scadaproj/infra/glauth/README.md`). This doc is a redistilled view tailored to mxaccessgw —
|
||||||
|
what users + groups are provisioned, how to bind against them, and what's needed to add a
|
||||||
|
gw-specific role.
|
||||||
|
|
||||||
## Connection details
|
## Connection details
|
||||||
|
|
||||||
| Setting | Value |
|
| Setting | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Protocol | LDAP (unencrypted) |
|
| Protocol | LDAP (unencrypted) |
|
||||||
| Host | `localhost` |
|
| Host | **`10.100.0.35`** (shared docker host — ~~`localhost`~~ retired) |
|
||||||
| Port | `3893` |
|
| Port | `3893` |
|
||||||
| LDAPS | disabled in dev (set `[ldaps]` block to enable) |
|
| LDAPS | disabled in dev (`Transport=None`, `AllowInsecure=true`) |
|
||||||
| Base DN | `dc=zb,dc=local` |
|
| Base DN | `dc=zb,dc=local` |
|
||||||
| Bind DN format | `cn={username},dc=zb,dc=local` |
|
| Bind DN format | `cn={username},dc=zb,dc=local` |
|
||||||
|
| Service account DN | `cn=serviceaccount,dc=zb,dc=local` / `serviceaccount123` |
|
||||||
| Group OU | `ou=<groupname>,ou=groups,dc=zb,dc=local` |
|
| Group OU | `ou=<groupname>,ou=groups,dc=zb,dc=local` |
|
||||||
| Failed-bind throttle | 3 fails → 10-minute IP lockout (per `[behaviors]`) |
|
| Failed-bind throttle | 3 fails → 10-minute IP lockout (per `[behaviors]`) |
|
||||||
|
|
||||||
@@ -59,13 +68,13 @@ For mxaccessgw dev, `admin` covers every gw-side capability test;
|
|||||||
`readonly` is the right "negative" case for proving Browse-OK /
|
`readonly` is the right "negative" case for proving Browse-OK /
|
||||||
Write-denied.
|
Write-denied.
|
||||||
|
|
||||||
The gateway dashboard adds one role beyond this LmxOpcUa taxonomy:
|
The gateway dashboard uses two gateway-specific groups beyond the LmxOpcUa taxonomy:
|
||||||
`GwAdmin`. `LdapOptions.RequiredGroup` defaults to `GwAdmin`, so the
|
`GwAdmin` (gid 5610 → role `Administrator`) and `GwReader` (gid 5611 → role `Viewer`).
|
||||||
dashboard login and `DashboardLdapLiveTests` require `admin` to be a
|
These are already provisioned in the shared `scadaproj/infra/glauth/config.toml`.
|
||||||
member of a `GwAdmin` group. `GwAdmin` is **not** in the baseline
|
The dashboard test users are **`multi-role`/`password`** (Administrator) and
|
||||||
GLAuth config — it must be provisioned before dashboard authn or the
|
**`gw-viewer`/`password`** (Viewer). `LdapOptions.RequiredGroup` defaults to `GwAdmin`.
|
||||||
LDAP live tests work. See [Provisioning the GwAdmin
|
See [Provisioning the GwAdmin group](#provisioning-the-gwadmin-group) below for the
|
||||||
group](#provisioning-the-gwadmin-group) below.
|
(now-retired) per-box procedure and for the shared-config equivalent.
|
||||||
|
|
||||||
> **Dashboard role value (Task 1.7):** the LDAP `GwAdmin` group now maps to
|
> **Dashboard role value (Task 1.7):** the LDAP `GwAdmin` group now maps to
|
||||||
> the canonical dashboard role **`Administrator`** (was `Admin`); `GwReader`
|
> the canonical dashboard role **`Administrator`** (was `Admin`); `GwReader`
|
||||||
@@ -118,7 +127,7 @@ record:
|
|||||||
```yaml
|
```yaml
|
||||||
ldap:
|
ldap:
|
||||||
enabled: true
|
enabled: true
|
||||||
server: localhost
|
server: 10.100.0.35 # shared GLAuth on docker host (was localhost)
|
||||||
port: 3893
|
port: 3893
|
||||||
useTls: false
|
useTls: false
|
||||||
allowInsecureLdap: true # dev only
|
allowInsecureLdap: true # dev only
|
||||||
@@ -143,13 +152,29 @@ look that up in `groupToRole`.
|
|||||||
|
|
||||||
## Provisioning the GwAdmin group
|
## Provisioning the GwAdmin group
|
||||||
|
|
||||||
|
> **UPDATED 2026-06-04 — RETIRED per-box procedure.** `GwAdmin` (gid 5610) and `GwReader`
|
||||||
|
> (gid 5611) are already present in the shared GLAuth. To add or modify users/groups,
|
||||||
|
> edit **`~/Desktop/scadaproj/infra/glauth/config.toml`** on host `10.100.0.35` and run:
|
||||||
|
>
|
||||||
|
> ```bash
|
||||||
|
> cd ~/Desktop/scadaproj/infra/glauth
|
||||||
|
> docker compose up -d --force-recreate
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> The per-box `C:\publish\glauth\glauth.cfg` + NSSM procedure below is kept for
|
||||||
|
> rollback reference only — do not use it for new provisioning.
|
||||||
|
|
||||||
`GwAdmin` is the gateway-specific dashboard-admin role. It is the
|
`GwAdmin` is the gateway-specific dashboard-admin role. It is the
|
||||||
default `LdapOptions.RequiredGroup`, so the dashboard cookie login and
|
default `LdapOptions.RequiredGroup`, so the dashboard cookie login and
|
||||||
`DashboardLdapLiveTests` (`MXGATEWAY_RUN_LIVE_LDAP_TESTS=1`) reject
|
`DashboardLdapLiveTests` (`MXGATEWAY_RUN_LIVE_LDAP_TESTS=1`) reject
|
||||||
`admin` until a `GwAdmin` group exists and `admin` is a member.
|
logins unless the user is a member of `GwAdmin`.
|
||||||
GLAuth's baseline config ships only the five LmxOpcUa role groups, so
|
The `GwAdmin` (gid 5610) and `GwReader` (gid 5611) groups already exist in the shared
|
||||||
`GwAdmin` must be added to GLAuth rather than run from a separate LDAP
|
config at `scadaproj/infra/glauth/config.toml`. Dashboard test users are
|
||||||
server:
|
`multi-role`/`password` (Administrator) and `gw-viewer`/`password` (Viewer).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**RETIRED — per-box provisioning (reference/rollback only):**
|
||||||
|
|
||||||
1. Edit `C:\publish\glauth\glauth.cfg`
|
1. Edit `C:\publish\glauth\glauth.cfg`
|
||||||
2. Append the group:
|
2. Append the group:
|
||||||
@@ -199,15 +224,16 @@ echo -n "yourpassword" | openssl dgst -sha256
|
|||||||
|
|
||||||
## Quick verification
|
## Quick verification
|
||||||
|
|
||||||
From mxaccessgw's dev box, prove the directory is reachable:
|
From mxaccessgw's dev box, prove the shared directory is reachable:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Plain bind via PowerShell + System.DirectoryServices.Protocols
|
# Plain bind via PowerShell + System.DirectoryServices.Protocols
|
||||||
$ldap = New-Object System.DirectoryServices.Protocols.LdapConnection("localhost:3893")
|
# (shared GLAuth on 10.100.0.35 — was localhost, now the docker host)
|
||||||
|
$ldap = New-Object System.DirectoryServices.Protocols.LdapConnection("10.100.0.35:3893")
|
||||||
$ldap.AuthType = [System.DirectoryServices.Protocols.AuthType]::Basic
|
$ldap.AuthType = [System.DirectoryServices.Protocols.AuthType]::Basic
|
||||||
$ldap.SessionOptions.ProtocolVersion = 3
|
$ldap.SessionOptions.ProtocolVersion = 3
|
||||||
$ldap.SessionOptions.SecureSocketLayer = $false
|
$ldap.SessionOptions.SecureSocketLayer = $false
|
||||||
$cred = New-Object System.Net.NetworkCredential("cn=admin,dc=zb,dc=local","admin123")
|
$cred = New-Object System.Net.NetworkCredential("cn=multi-role,dc=zb,dc=local","password")
|
||||||
$ldap.Bind($cred)
|
$ldap.Bind($cred)
|
||||||
"Bind OK"
|
"Bind OK"
|
||||||
```
|
```
|
||||||
@@ -215,17 +241,32 @@ $ldap.Bind($cred)
|
|||||||
Or via `ldapsearch` if you have OpenLDAP CLI tools:
|
Or via `ldapsearch` if you have OpenLDAP CLI tools:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ldapsearch -x -H ldap://localhost:3893 \
|
ldapsearch -x -H ldap://10.100.0.35:3893 \
|
||||||
-D "cn=admin,dc=zb,dc=local" -w admin123 \
|
-D "cn=serviceaccount,dc=zb,dc=local" -w serviceaccount123 \
|
||||||
-b "dc=zb,dc=local" "(uid=admin)"
|
-b "dc=zb,dc=local" "(uid=multi-role)"
|
||||||
```
|
```
|
||||||
|
|
||||||
The response should list `admin`'s entry with `memberOf` populated for
|
The response should list `multi-role`'s entry with `memberOf` including
|
||||||
all five role groups — plus `GwAdmin` once the gateway-specific group
|
`ou=GwAdmin,ou=groups,dc=zb,dc=local`.
|
||||||
is provisioned.
|
|
||||||
|
|
||||||
## Service management
|
## Service management
|
||||||
|
|
||||||
|
> **RETIRED — per-box NSSM service (reference/rollback only).** The shared GLAuth is
|
||||||
|
> managed via `docker compose` on `10.100.0.35` (`scadaproj/infra/glauth/`). The
|
||||||
|
> Windows NSSM `GLAuth` service on the dev box has been stopped and set to
|
||||||
|
> `StartupType=Manual`; only restart it if you need to roll back to a local directory.
|
||||||
|
>
|
||||||
|
> **Active (shared) management:**
|
||||||
|
> ```bash
|
||||||
|
> ssh 10.100.0.35
|
||||||
|
> cd ~/Desktop/scadaproj/infra/glauth
|
||||||
|
> docker compose ps # check container status
|
||||||
|
> docker compose up -d --force-recreate # apply config.toml changes
|
||||||
|
> docker compose logs -f # tail logs
|
||||||
|
> ```
|
||||||
|
|
||||||
|
**RETIRED — per-box NSSM commands (rollback reference):**
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Status / start / stop / restart
|
# Status / start / stop / restart
|
||||||
nssm status GLAuth
|
nssm status GLAuth
|
||||||
@@ -259,7 +300,7 @@ applies to mxaccessgw verbatim. Keys that change:
|
|||||||
|
|
||||||
| Field | GLAuth dev value | AD production value |
|
| Field | GLAuth dev value | AD production value |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `Server` | `localhost` | a domain controller FQDN, or the domain itself |
|
| `Server` | `10.100.0.35` (shared docker host) | a domain controller FQDN, or the domain itself |
|
||||||
| `Port` | `3893` | `636` (LDAPS) — AD increasingly rejects plain bind under LDAP-signing enforcement |
|
| `Port` | `3893` | `636` (LDAPS) — AD increasingly rejects plain bind under LDAP-signing enforcement |
|
||||||
| `UseTls` | `false` | `true` |
|
| `UseTls` | `false` | `true` |
|
||||||
| `AllowInsecureLdap` | `true` | `false` |
|
| `AllowInsecureLdap` | `true` | `false` |
|
||||||
@@ -275,12 +316,12 @@ add a `tokenGroups` query as an enhancement.
|
|||||||
|
|
||||||
## Security notes for production
|
## Security notes for production
|
||||||
|
|
||||||
- **Plaintext passwords in `glauth.cfg` are dev-only.** The config is
|
- **Plaintext passwords in `config.toml` are dev-only.** The shared config is in
|
||||||
unencrypted on disk; anyone with read access to `C:\publish\glauth\`
|
`scadaproj/infra/glauth/config.toml` (unencrypted); restrict filesystem access on
|
||||||
can SHA256-rainbow-table the entries. Treat the dev creds as
|
`10.100.0.35` accordingly. Treat the dev creds as throwaway. Production LDAP is Active
|
||||||
throwaway. Production LDAP is Active Directory.
|
Directory. *(The retired per-box `C:\publish\glauth\glauth.cfg` has the same caveat.)*
|
||||||
- The 3-fail / 10-minute lockout is per source IP, not per user — a
|
- The 3-fail / 10-minute lockout is per source IP, not per user — a
|
||||||
shared NAT can lock out a whole office. Tunable in `[behaviors]`.
|
shared NAT can lock out a whole office. Tunable in `[behaviors]`.
|
||||||
- LDAPS isn't enabled in dev; binding sends passwords cleartext on the
|
- LDAPS isn't enabled in dev; binding sends passwords cleartext on the
|
||||||
wire. Fine for `localhost`, never expose port 3893 off-box without
|
wire. The shared GLAuth listens only on the LAN (`10.100.0.35`); never
|
||||||
enabling TLS first.
|
expose port 3893 externally without enabling TLS first.
|
||||||
|
|||||||
@@ -6,13 +6,19 @@
|
|||||||
cookie scheme's LoginPath="/login" redirect lands here for unauthenticated users.
|
cookie scheme's LoginPath="/login" redirect lands here for unauthenticated users.
|
||||||
|
|
||||||
The card is the shared kit's <LoginCard>: it renders a NATIVE static
|
The card is the shared kit's <LoginCard>: it renders a NATIVE static
|
||||||
<form method="post" action="/login"> (username/password + hidden returnUrl). A native
|
<form method="post" action="/auth/login"> (username/password + hidden returnUrl). A native
|
||||||
form submit is not a Blazor event, so it reaches the minimal-API POST /login endpoint
|
form submit is not a Blazor event, so it reaches the minimal-API POST /auth/login endpoint
|
||||||
regardless of this app's InteractiveServer render mode. <AntiforgeryToken/> supplies the
|
regardless of this app's InteractiveServer render mode. <AntiforgeryToken/> supplies the
|
||||||
token that PostLoginAsync's antiforgery.ValidateRequestAsync checks. *@
|
token that PostLoginAsync's antiforgery.ValidateRequestAsync checks.
|
||||||
|
|
||||||
|
NOTE: the POST target is /auth/login, NOT /login. This @page lives at "/login" and the
|
||||||
|
Razor Components endpoint matches ALL methods, so a POST to /login collided with the
|
||||||
|
minimal-API MapPost("/login") and threw AmbiguousMatchException (HTTP 500). Posting to a
|
||||||
|
distinct /auth/login path (mirroring ScadaBridge) keeps the GET page and POST handler from
|
||||||
|
sharing a route. *@
|
||||||
@attribute [AllowAnonymous]
|
@attribute [AllowAnonymous]
|
||||||
|
|
||||||
<LoginCard Product="MXAccess Gateway" Action="/login" ReturnUrl="@ReturnUrl" Error="@Error">
|
<LoginCard Product="MXAccess Gateway" Action="/auth/login" ReturnUrl="@ReturnUrl" Error="@Error">
|
||||||
<AntiforgeryToken />
|
<AntiforgeryToken />
|
||||||
</LoginCard>
|
</LoginCard>
|
||||||
|
|
||||||
|
|||||||
@@ -29,8 +29,14 @@ public static class DashboardEndpointRouteBuilderExtensions
|
|||||||
// kit's <LoginCard>. Its [AllowAnonymous] attribute overrides the
|
// kit's <LoginCard>. Its [AllowAnonymous] attribute overrides the
|
||||||
// RequireAuthorization(ViewerPolicy) that MapRazorComponents<App>() applies,
|
// RequireAuthorization(ViewerPolicy) that MapRazorComponents<App>() applies,
|
||||||
// so the cookie scheme's LoginPath="/login" redirect resolves for anonymous users.
|
// so the cookie scheme's LoginPath="/login" redirect resolves for anonymous users.
|
||||||
|
//
|
||||||
|
// The credential POST is mapped to /auth/login, NOT /login. The @page "/login"
|
||||||
|
// Razor Components endpoint matches ALL HTTP methods, so a MapPost("/login") shared
|
||||||
|
// the "/login" route with it and every POST threw AmbiguousMatchException (HTTP 500).
|
||||||
|
// A distinct /auth/login path (as ScadaBridge does) keeps the GET page and the POST
|
||||||
|
// handler on separate routes. The <LoginCard Action="/auth/login"> form posts here.
|
||||||
endpoints.MapPost(
|
endpoints.MapPost(
|
||||||
"/login",
|
"/auth/login",
|
||||||
(HttpContext httpContext, IAntiforgery antiforgery, IDashboardAuthenticator authenticator) =>
|
(HttpContext httpContext, IAntiforgery antiforgery, IDashboardAuthenticator authenticator) =>
|
||||||
PostLoginAsync(httpContext, antiforgery, authenticator))
|
PostLoginAsync(httpContext, antiforgery, authenticator))
|
||||||
.AllowAnonymous()
|
.AllowAnonymous()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<PackageReference Include="ZB.MOM.WW.Auth.ApiKeys" Version="0.1.2" />
|
<PackageReference Include="ZB.MOM.WW.Auth.ApiKeys" Version="0.1.2" />
|
||||||
<PackageReference Include="ZB.MOM.WW.Auth.AspNetCore" Version="0.1.2" />
|
<PackageReference Include="ZB.MOM.WW.Auth.AspNetCore" Version="0.1.2" />
|
||||||
<PackageReference Include="ZB.MOM.WW.Audit" Version="0.1.0" />
|
<PackageReference Include="ZB.MOM.WW.Audit" Version="0.1.0" />
|
||||||
<PackageReference Include="ZB.MOM.WW.Theme" Version="0.2.0" />
|
<PackageReference Include="ZB.MOM.WW.Theme" Version="0.3.1" />
|
||||||
<PackageReference Include="ZB.MOM.WW.Configuration" Version="0.1.0" />
|
<PackageReference Include="ZB.MOM.WW.Configuration" Version="0.1.0" />
|
||||||
<PackageReference Include="ZB.MOM.WW.Health" Version="0.1.0" />
|
<PackageReference Include="ZB.MOM.WW.Health" Version="0.1.0" />
|
||||||
<PackageReference Include="ZB.MOM.WW.Telemetry" Version="0.1.0" />
|
<PackageReference Include="ZB.MOM.WW.Telemetry" Version="0.1.0" />
|
||||||
|
|||||||
@@ -87,13 +87,18 @@ public sealed class GatewayApplicationTests
|
|||||||
Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/events");
|
Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/events");
|
||||||
Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/settings");
|
Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/settings");
|
||||||
|
|
||||||
// GET /login is now served by the [AllowAnonymous] Blazor <Login> component
|
// GET /login is served by the [AllowAnonymous] Blazor <Login> component
|
||||||
// (Components/Pages/Login.razor → @page "/login"), not a named minimal-API
|
// (Components/Pages/Login.razor → @page "/login"), not a named minimal-API
|
||||||
// endpoint. The form still POSTs to the minimal-API DashboardLoginPost endpoint.
|
// endpoint. The credential POST goes to the DashboardLoginPost endpoint at
|
||||||
|
// /auth/login — a DISTINCT route. The Blazor component endpoint matches all HTTP
|
||||||
|
// methods, so sharing the "/login" route with MapPost previously made POST /login
|
||||||
|
// ambiguous (AmbiguousMatchException → HTTP 500). Pinning the POST to /auth/login
|
||||||
|
// is the regression guard for that fix.
|
||||||
Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/login"
|
Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/login"
|
||||||
&& endpoint.Metadata.GetMetadata<Microsoft.AspNetCore.Components.Endpoints.ComponentTypeMetadata>() is not null);
|
&& endpoint.Metadata.GetMetadata<Microsoft.AspNetCore.Components.Endpoints.ComponentTypeMetadata>() is not null);
|
||||||
Assert.Contains(endpoints, endpoint =>
|
Assert.Contains(endpoints, endpoint =>
|
||||||
endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName == "DashboardLoginPost");
|
endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName == "DashboardLoginPost"
|
||||||
|
&& endpoint.RoutePattern.RawText == "/auth/login");
|
||||||
Assert.Contains(endpoints, endpoint =>
|
Assert.Contains(endpoints, endpoint =>
|
||||||
endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName == "DashboardLogout");
|
endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName == "DashboardLogout");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user