diff --git a/archreview/plans/R2-05-adminui-authz-plan.md.tasks.json b/archreview/plans/R2-05-adminui-authz-plan.md.tasks.json index d430662c..6b04a7dd 100644 --- a/archreview/plans/R2-05-adminui-authz-plan.md.tasks.json +++ b/archreview/plans/R2-05-adminui-authz-plan.md.tasks.json @@ -13,7 +13,7 @@ { "id": "T9", "subject": "Converge Deployments/Scripts/ScriptEdit from Roles-string to ConfigEditor policy", "status": "completed", "blockedBy": ["T4", "T5"] }, { "id": "T10", "subject": "Read pages to AuthenticatedRead (16 incl. Home insert) + RoleGrants to FleetAdmin constant", "status": "completed", "blockedBy": ["T4", "T5"] }, { "id": "T11", "subject": "Guard GREEN checkpoint + full AdminUI.Tests; commit the gate", "status": "completed", "blockedBy": ["T3", "T6", "T7", "T8", "T9", "T10"] }, - { "id": "T12", "subject": "Converge non-page literals to constants (ScriptAnalysisEndpoints, imperative DriverOperator x4, Certificates FleetAdmin x2)", "status": "pending", "blockedBy": ["T11"] }, + { "id": "T12", "subject": "Converge non-page literals to constants (ScriptAnalysisEndpoints, imperative DriverOperator x4, Certificates FleetAdmin x2)", "status": "completed", "blockedBy": ["T11"] }, { "id": "T13", "subject": "Docs: CLAUDE.md ScriptAnalysis gate sentence, docs/security.md, STATUS.md R2-05 row", "status": "pending", "blockedBy": ["T11"] }, { "id": "T14", "subject": "Full-solution test sweep (dotnet test ZB.MOM.WW.OtOpcUa.slnx)", "status": "pending", "blockedBy": ["T12", "T13"] }, { "id": "T15", "subject": "Live /run positive pass on docker-dev :9200 (rebuild BOTH centrals; auto-admin drives all page tiers; negative proof stays in T2/T3)", "status": "pending", "blockedBy": ["T14"] }, diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Alerts.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Alerts.razor index b2a649b6..a9c77a6c 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Alerts.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Alerts.razor @@ -161,7 +161,7 @@ else // permitted users. The username is re-read at click time (GetCurrentUserNameAsync) so a // mid-session token refresh lands in the published command + audit accurately. var auth = await AuthState.GetAuthenticationStateAsync(); - var authResult = await AuthorizationService.AuthorizeAsync(auth.User, null, "DriverOperator"); + var authResult = await AuthorizationService.AuthorizeAsync(auth.User, null, AdminUiPolicies.DriverOperator); _canOperate = authResult.Succeeded; } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Certificates.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Certificates.razor index 641e1337..887bf051 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Certificates.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Certificates.razor @@ -87,7 +87,7 @@ else @if (store.Kind is StoreKind.Trusted or StoreKind.Rejected) { - + @if (store.Kind == StoreKind.Rejected) { @@ -183,7 +183,7 @@ else // Defense-in-depth: the action buttons are FleetAdmin-gated in markup, but this handler // runs on the server circuit — re-check the policy before mutating the trust store. var authState = await AuthState.GetAuthenticationStateAsync(); - if (!(await AuthorizationService.AuthorizeAsync(authState.User, null, "FleetAdmin")).Succeeded) + if (!(await AuthorizationService.AuthorizeAsync(authState.User, null, AdminUiPolicies.FleetAdmin)).Succeeded) { _statusError = true; _statusMsg = "Unauthorized — FleetAdmin required."; diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverStatusPanel.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverStatusPanel.razor index d2d2f2c3..e069aab0 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverStatusPanel.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverStatusPanel.razor @@ -163,7 +163,7 @@ // The username for audit logging is re-read at button-click time (not captured here) // so token-refreshes mid-session land in audit entries accurately. var auth = await AuthState.GetAuthenticationStateAsync(); - var authResult = await AuthorizationService.AuthorizeAsync(auth.User, null, "DriverOperator"); + var authResult = await AuthorizationService.AuthorizeAsync(auth.User, null, AdminUiPolicies.DriverOperator); _canOperate = authResult.Succeeded; if (!Enabled) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/GalaxyAddressPickerBody.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/GalaxyAddressPickerBody.razor index 5aa73aeb..b6a0463b 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/GalaxyAddressPickerBody.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/GalaxyAddressPickerBody.razor @@ -121,7 +121,7 @@ protected override async Task OnInitializedAsync() { var auth = await AuthState.GetAuthenticationStateAsync(); - var authResult = await AuthorizationService.AuthorizeAsync(auth.User, null, "DriverOperator"); + var authResult = await AuthorizationService.AuthorizeAsync(auth.User, null, AdminUiPolicies.DriverOperator); _canOperate = authResult.Succeeded; _built = Build(); await CurrentAddressChanged.InvokeAsync(_built); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/OpcUaClientAddressPickerBody.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/OpcUaClientAddressPickerBody.razor index a00ef2de..8d8d0c58 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/OpcUaClientAddressPickerBody.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/OpcUaClientAddressPickerBody.razor @@ -72,7 +72,7 @@ protected override async Task OnInitializedAsync() { var auth = await AuthState.GetAuthenticationStateAsync(); - var authResult = await AuthorizationService.AuthorizeAsync(auth.User, null, "DriverOperator"); + var authResult = await AuthorizationService.AuthorizeAsync(auth.User, null, AdminUiPolicies.DriverOperator); _canOperate = authResult.Succeeded; _built = _nodeId; await CurrentAddressChanged.InvokeAsync(_built); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/ScriptAnalysisEndpoints.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/ScriptAnalysisEndpoints.cs index 7c801db9..7fec4785 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/ScriptAnalysisEndpoints.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/ScriptAnalysisEndpoints.cs @@ -1,21 +1,21 @@ -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using ZB.MOM.WW.OtOpcUa.Security.Auth; namespace ZB.MOM.WW.OtOpcUa.AdminUI.ScriptAnalysis; public static class ScriptAnalysisEndpoints { - /// Maps the /api/script-analysis endpoint group (diagnostics, completions, hover, signature help, format), gated to the Administrator/Designer roles. + /// Maps the /api/script-analysis endpoint group (diagnostics, completions, hover, signature help, format), gated by the ConfigEditor policy (Administrator or Designer). /// The route builder to map the endpoints onto. /// The same route builder, for chaining. public static IEndpointRouteBuilder MapScriptAnalysisEndpoints(this IEndpointRouteBuilder endpoints) { - // Require Administrator or Designer — matches the Script page gate and the /deployments gate. + // ConfigEditor policy (Administrator or Designer) — matches the Script page gate and the /deployments gate. var group = endpoints.MapGroup("/api/script-analysis") - .RequireAuthorization(new AuthorizeAttribute { Roles = "Administrator,Designer" }); + .RequireAuthorization(AdminUiPolicies.ConfigEditor); group.MapPost("/diagnostics", (DiagnoseRequest r, ScriptAnalysisService s) => Results.Ok(s.Diagnose(r))); group.MapPost("/completions", async (CompletionsRequest r, ScriptAnalysisService s) => Results.Ok(await s.CompleteAsync(r))); group.MapPost("/hover", async (HoverRequest r, ScriptAnalysisService s) => Results.Ok(await s.Hover(r)));