Compare commits
4 Commits
admin-host
...
role-grant
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44d4448b37 | ||
| 572f8887e4 | |||
|
|
2acea08ced | ||
| 49f6c9484e |
@@ -10,6 +10,7 @@
|
||||
<li class="nav-item"><a class="nav-link text-light" href="/clusters">Clusters</a></li>
|
||||
<li class="nav-item"><a class="nav-link text-light" href="/reservations">Reservations</a></li>
|
||||
<li class="nav-item"><a class="nav-link text-light" href="/certificates">Certificates</a></li>
|
||||
<li class="nav-item"><a class="nav-link text-light" href="/role-grants">Role grants</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-5">
|
||||
|
||||
@@ -36,7 +36,10 @@ else if (_equipment.Count > 0)
|
||||
<td>@e.SAPID</td>
|
||||
<td>@e.Manufacturer / @e.Model</td>
|
||||
<td>@e.SerialNumber</td>
|
||||
<td><button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteAsync(e.EquipmentRowId)">Remove</button></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-secondary me-1" @onclick="() => StartEdit(e)">Edit</button>
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteAsync(e.EquipmentRowId)">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
@@ -47,8 +50,8 @@ else if (_equipment.Count > 0)
|
||||
{
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h5>New equipment</h5>
|
||||
<EditForm Model="_draft" OnValidSubmit="SaveAsync" FormName="new-equipment">
|
||||
<h5>@(_editMode ? "Edit equipment" : "New equipment")</h5>
|
||||
<EditForm Model="_draft" OnValidSubmit="SaveAsync" FormName="equipment-form">
|
||||
<DataAnnotationsValidator/>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
@@ -78,24 +81,13 @@ else if (_equipment.Count > 0)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 class="mt-4">OPC 40010 Identification</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4"><label class="form-label">Manufacturer</label><InputText @bind-Value="_draft.Manufacturer" class="form-control"/></div>
|
||||
<div class="col-md-4"><label class="form-label">Model</label><InputText @bind-Value="_draft.Model" class="form-control"/></div>
|
||||
<div class="col-md-4"><label class="form-label">Serial number</label><InputText @bind-Value="_draft.SerialNumber" class="form-control"/></div>
|
||||
<div class="col-md-4"><label class="form-label">Hardware rev</label><InputText @bind-Value="_draft.HardwareRevision" class="form-control"/></div>
|
||||
<div class="col-md-4"><label class="form-label">Software rev</label><InputText @bind-Value="_draft.SoftwareRevision" class="form-control"/></div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Year of construction</label>
|
||||
<InputNumber @bind-Value="_draft.YearOfConstruction" class="form-control"/>
|
||||
</div>
|
||||
</div>
|
||||
<IdentificationFields Equipment="_draft"/>
|
||||
|
||||
@if (_error is not null) { <div class="alert alert-danger mt-3">@_error</div> }
|
||||
|
||||
<div class="mt-3">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Save</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm ms-2" @onclick="() => _showForm = false">Cancel</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm ms-2" @onclick="Cancel">Cancel</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
@@ -106,6 +98,7 @@ else if (_equipment.Count > 0)
|
||||
[Parameter] public long GenerationId { get; set; }
|
||||
private List<Equipment>? _equipment;
|
||||
private bool _showForm;
|
||||
private bool _editMode;
|
||||
private Equipment _draft = NewBlankDraft();
|
||||
private string? _error;
|
||||
|
||||
@@ -125,20 +118,68 @@ else if (_equipment.Count > 0)
|
||||
private void StartAdd()
|
||||
{
|
||||
_draft = NewBlankDraft();
|
||||
_editMode = false;
|
||||
_error = null;
|
||||
_showForm = true;
|
||||
}
|
||||
|
||||
private void StartEdit(Equipment row)
|
||||
{
|
||||
// Shallow-clone so Cancel doesn't mutate the list-displayed row with in-flight form edits.
|
||||
_draft = new Equipment
|
||||
{
|
||||
EquipmentRowId = row.EquipmentRowId,
|
||||
GenerationId = row.GenerationId,
|
||||
EquipmentId = row.EquipmentId,
|
||||
EquipmentUuid = row.EquipmentUuid,
|
||||
DriverInstanceId = row.DriverInstanceId,
|
||||
DeviceId = row.DeviceId,
|
||||
UnsLineId = row.UnsLineId,
|
||||
Name = row.Name,
|
||||
MachineCode = row.MachineCode,
|
||||
ZTag = row.ZTag,
|
||||
SAPID = row.SAPID,
|
||||
Manufacturer = row.Manufacturer,
|
||||
Model = row.Model,
|
||||
SerialNumber = row.SerialNumber,
|
||||
HardwareRevision = row.HardwareRevision,
|
||||
SoftwareRevision = row.SoftwareRevision,
|
||||
YearOfConstruction = row.YearOfConstruction,
|
||||
AssetLocation = row.AssetLocation,
|
||||
ManufacturerUri = row.ManufacturerUri,
|
||||
DeviceManualUri = row.DeviceManualUri,
|
||||
EquipmentClassRef = row.EquipmentClassRef,
|
||||
Enabled = row.Enabled,
|
||||
};
|
||||
_editMode = true;
|
||||
_error = null;
|
||||
_showForm = true;
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
_showForm = false;
|
||||
_editMode = false;
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
_error = null;
|
||||
_draft.EquipmentUuid = Guid.NewGuid();
|
||||
_draft.EquipmentId = DraftValidator.DeriveEquipmentId(_draft.EquipmentUuid);
|
||||
_draft.GenerationId = GenerationId;
|
||||
try
|
||||
{
|
||||
await EquipmentSvc.CreateAsync(GenerationId, _draft, CancellationToken.None);
|
||||
if (_editMode)
|
||||
{
|
||||
await EquipmentSvc.UpdateAsync(_draft, CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
_draft.EquipmentUuid = Guid.NewGuid();
|
||||
_draft.EquipmentId = DraftValidator.DeriveEquipmentId(_draft.EquipmentUuid);
|
||||
_draft.GenerationId = GenerationId;
|
||||
await EquipmentSvc.CreateAsync(GenerationId, _draft, CancellationToken.None);
|
||||
}
|
||||
_showForm = false;
|
||||
_editMode = false;
|
||||
await ReloadAsync();
|
||||
}
|
||||
catch (Exception ex) { _error = ex.Message; }
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
|
||||
|
||||
@* Reusable OPC 40010 Machinery Identification editor. Binds to an Equipment row and renders the
|
||||
nine decision #139 fields in a consistent 3-column Bootstrap grid. Used by EquipmentTab's
|
||||
create + edit forms so the same UI renders regardless of which flow opened it. *@
|
||||
|
||||
<h6 class="mt-4">OPC 40010 Identification</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Manufacturer</label>
|
||||
<InputText @bind-Value="Equipment!.Manufacturer" class="form-control"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Model</label>
|
||||
<InputText @bind-Value="Equipment!.Model" class="form-control"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Serial number</label>
|
||||
<InputText @bind-Value="Equipment!.SerialNumber" class="form-control"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Hardware rev</label>
|
||||
<InputText @bind-Value="Equipment!.HardwareRevision" class="form-control"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Software rev</label>
|
||||
<InputText @bind-Value="Equipment!.SoftwareRevision" class="form-control"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Year of construction</label>
|
||||
<InputNumber @bind-Value="Equipment!.YearOfConstruction" class="form-control"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Asset location</label>
|
||||
<InputText @bind-Value="Equipment!.AssetLocation" class="form-control"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Manufacturer URI</label>
|
||||
<InputText @bind-Value="Equipment!.ManufacturerUri" class="form-control" placeholder="https://…"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Device manual URI</label>
|
||||
<InputText @bind-Value="Equipment!.DeviceManualUri" class="form-control" placeholder="https://…"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public Equipment? Equipment { get; set; }
|
||||
}
|
||||
161
src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/RoleGrants.razor
Normal file
161
src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/RoleGrants.razor
Normal file
@@ -0,0 +1,161 @@
|
||||
@page "/role-grants"
|
||||
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Services
|
||||
@inject ILdapGroupRoleMappingService RoleSvc
|
||||
@inject ClusterService ClusterSvc
|
||||
|
||||
<h1 class="mb-4">LDAP group → Admin role grants</h1>
|
||||
|
||||
<div class="alert alert-info small mb-4">
|
||||
Maps LDAP groups to Admin UI roles (ConfigViewer / ConfigEditor / FleetAdmin). Control-plane
|
||||
only — OPC UA data-path authorization reads <code>NodeAcl</code> rows directly and is
|
||||
unaffected by these mappings (see decision #150). A fleet-wide grant applies across every
|
||||
cluster; a cluster-scoped grant only binds within the named cluster. The same LDAP group
|
||||
may hold different roles on different clusters.
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end mb-3">
|
||||
<button class="btn btn-primary btn-sm" @onclick="StartAdd">Add grant</button>
|
||||
</div>
|
||||
|
||||
@if (_rows is null)
|
||||
{
|
||||
<p>Loading…</p>
|
||||
}
|
||||
else if (_rows.Count == 0)
|
||||
{
|
||||
<p class="text-muted">No role grants defined yet. Without at least one FleetAdmin grant,
|
||||
only the bootstrap admin can publish drafts.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-sm table-hover">
|
||||
<thead>
|
||||
<tr><th>LDAP group</th><th>Role</th><th>Scope</th><th>Created</th><th>Notes</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var r in _rows)
|
||||
{
|
||||
<tr>
|
||||
<td><code>@r.LdapGroup</code></td>
|
||||
<td><span class="badge bg-secondary">@r.Role</span></td>
|
||||
<td>@(r.IsSystemWide ? "Fleet-wide" : $"Cluster: {r.ClusterId}")</td>
|
||||
<td class="small">@r.CreatedAtUtc.ToString("yyyy-MM-dd")</td>
|
||||
<td class="small text-muted">@r.Notes</td>
|
||||
<td><button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteAsync(r.Id)">Revoke</button></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@if (_showForm)
|
||||
{
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h5>New role grant</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">LDAP group (DN)</label>
|
||||
<input class="form-control" @bind="_group" placeholder="cn=fleet-admin,ou=groups,dc=…"/>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Role</label>
|
||||
<select class="form-select" @bind="_role">
|
||||
@foreach (var r in Enum.GetValues<AdminRole>())
|
||||
{
|
||||
<option value="@r">@r</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2 pt-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="systemWide" @bind="_isSystemWide"/>
|
||||
<label class="form-check-label" for="systemWide">Fleet-wide</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Cluster @(_isSystemWide ? "(disabled)" : "")</label>
|
||||
<select class="form-select" @bind="_clusterId" disabled="@_isSystemWide">
|
||||
<option value="">-- select --</option>
|
||||
@if (_clusters is not null)
|
||||
{
|
||||
@foreach (var c in _clusters)
|
||||
{
|
||||
<option value="@c.ClusterId">@c.ClusterId</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Notes (optional)</label>
|
||||
<input class="form-control" @bind="_notes"/>
|
||||
</div>
|
||||
</div>
|
||||
@if (_error is not null) { <div class="alert alert-danger mt-3">@_error</div> }
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-sm btn-primary" @onclick="SaveAsync">Save</button>
|
||||
<button class="btn btn-sm btn-secondary ms-2" @onclick="() => _showForm = false">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private IReadOnlyList<LdapGroupRoleMapping>? _rows;
|
||||
private List<ServerCluster>? _clusters;
|
||||
private bool _showForm;
|
||||
private string _group = string.Empty;
|
||||
private AdminRole _role = AdminRole.ConfigViewer;
|
||||
private bool _isSystemWide;
|
||||
private string _clusterId = string.Empty;
|
||||
private string? _notes;
|
||||
private string? _error;
|
||||
|
||||
protected override async Task OnInitializedAsync() => await ReloadAsync();
|
||||
|
||||
private async Task ReloadAsync()
|
||||
{
|
||||
_rows = await RoleSvc.ListAllAsync(CancellationToken.None);
|
||||
_clusters = await ClusterSvc.ListAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
private void StartAdd()
|
||||
{
|
||||
_group = string.Empty;
|
||||
_role = AdminRole.ConfigViewer;
|
||||
_isSystemWide = false;
|
||||
_clusterId = string.Empty;
|
||||
_notes = null;
|
||||
_error = null;
|
||||
_showForm = true;
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
_error = null;
|
||||
try
|
||||
{
|
||||
var row = new LdapGroupRoleMapping
|
||||
{
|
||||
LdapGroup = _group.Trim(),
|
||||
Role = _role,
|
||||
IsSystemWide = _isSystemWide,
|
||||
ClusterId = _isSystemWide ? null : (string.IsNullOrWhiteSpace(_clusterId) ? null : _clusterId),
|
||||
Notes = string.IsNullOrWhiteSpace(_notes) ? null : _notes,
|
||||
};
|
||||
await RoleSvc.CreateAsync(row, CancellationToken.None);
|
||||
_showForm = false;
|
||||
await ReloadAsync();
|
||||
}
|
||||
catch (Exception ex) { _error = ex.Message; }
|
||||
}
|
||||
|
||||
private async Task DeleteAsync(Guid id)
|
||||
{
|
||||
await RoleSvc.DeleteAsync(id, CancellationToken.None);
|
||||
await ReloadAsync();
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,8 @@ builder.Services.AddScoped<ReservationService>();
|
||||
builder.Services.AddScoped<DraftValidationService>();
|
||||
builder.Services.AddScoped<AuditLogService>();
|
||||
builder.Services.AddScoped<HostStatusService>();
|
||||
builder.Services.AddScoped<ZB.MOM.WW.OtOpcUa.Configuration.Services.ILdapGroupRoleMappingService,
|
||||
ZB.MOM.WW.OtOpcUa.Configuration.Services.LdapGroupRoleMappingService>();
|
||||
|
||||
// Cert-trust management — reads the OPC UA server's PKI store root so rejected client certs
|
||||
// can be promoted to trusted via the Admin UI. Singleton: no per-request state, just
|
||||
|
||||
Reference in New Issue
Block a user