feat(dashboard): ApiKeysPage lists and accepts dashboard_tags constraints
The constraints column enumerated only the eight positional ApiKeyConstraints members, so a key whose sole recorded policy was a dashboard tag summarised to an empty string and rendered as "-" — the same cell a key with no policy at all gets. ApiKeyConstraints.IsEmpty counts DashboardTags, so that key is not unconstrained, and the column was quietly telling operators otherwise about a grant that decides who can watch a session's events. The create form had no dashboard-tags input either, so tagged keys could only be minted from the apikey create-key CLI. Adds the field beside the other constraint lists (same ParseList separators) and attaches it through the record's init-only member, since it postdates the eight-member constructor. CreateModel, OpenCreateDialog and TryBuildCreateRequest widen to internal for the new render tests: the create form is behind a click and static rendering cannot dispatch one. That is the assembly's existing InternalsVisibleTo seam.
This commit is contained in:
@@ -115,6 +115,19 @@ else
|
||||
<label for="browseSubtrees" class="form-label small">Browse subtrees</label>
|
||||
<textarea id="browseSubtrees" class="form-control form-control-sm" rows="2" @bind="CreateModel.BrowseSubtrees" @bind:event="oninput"></textarea>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="dashboardTags" class="form-label small">Dashboard tags</label>
|
||||
<textarea id="dashboardTags" class="form-control form-control-sm" rows="2"
|
||||
aria-describedby="dashboardTagsHelp"
|
||||
@bind="CreateModel.DashboardTags" @bind:event="oninput"></textarea>
|
||||
<div id="dashboardTagsHelp" class="form-text small">
|
||||
Comma- or newline-separated; mirrors <code>apikey create-key --dashboard-tags</code>.
|
||||
Matched case-insensitively against the viewer grants in
|
||||
<code>Dashboard:GroupToTag</code>. Scopes dashboard event visibility only —
|
||||
never what the key may read, write, or browse. Empty leaves the key's sessions
|
||||
untagged, whose visibility follows <code>Dashboard:UntaggedSessionVisibility</code>.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="maxWriteClassification" class="form-label small">Max write classification</label>
|
||||
<input id="maxWriteClassification" class="form-control form-control-sm" @bind="CreateModel.MaxWriteClassification" @bind:event="oninput" />
|
||||
@@ -238,7 +251,14 @@ else
|
||||
GatewayScopes.Admin
|
||||
];
|
||||
|
||||
private ApiKeyCreateModel CreateModel { get; } = new();
|
||||
/// <summary>
|
||||
/// Backing state for the create dialog. Internal rather than private so
|
||||
/// <c>ApiKeysPageDashboardTagsTests</c> can drive the model-to-request mapping
|
||||
/// directly — the assembly's established test seam (see <c>InternalsVisibleTo</c>
|
||||
/// in <c>Properties/AssemblyInfo.cs</c>), because a create form behind a click is
|
||||
/// unreachable from static rendering.
|
||||
/// </summary>
|
||||
internal ApiKeyCreateModel CreateModel { get; } = new();
|
||||
|
||||
private bool CanManageApiKeys { get; set; }
|
||||
|
||||
@@ -399,7 +419,8 @@ else
|
||||
LastGeneratedApiKey = result.ApiKey;
|
||||
}
|
||||
|
||||
private void OpenCreateDialog()
|
||||
/// <summary>Opens the create dialog. Internal so a render test can reach the form's markup.</summary>
|
||||
internal void OpenCreateDialog()
|
||||
{
|
||||
IsCreateDialogOpen = true;
|
||||
}
|
||||
@@ -412,7 +433,11 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryBuildCreateRequest(
|
||||
/// <summary>Maps <see cref="CreateModel"/> onto a create request, or reports why it cannot.</summary>
|
||||
/// <param name="request">The built request when this returns true.</param>
|
||||
/// <param name="validationMessage">The reason the model is unusable when this returns false.</param>
|
||||
/// <returns>True when the model produced a request.</returns>
|
||||
internal bool TryBuildCreateRequest(
|
||||
[System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out DashboardApiKeyManagementRequest? request,
|
||||
out string? validationMessage)
|
||||
{
|
||||
@@ -449,7 +474,12 @@ else
|
||||
MaxWriteClassification: maxWriteClassification,
|
||||
BrowseSubtrees: ParseList(CreateModel.BrowseSubtrees),
|
||||
ReadAlarmOnly: CreateModel.ReadAlarmOnly,
|
||||
ReadHistorizedOnly: CreateModel.ReadHistorizedOnly));
|
||||
ReadHistorizedOnly: CreateModel.ReadHistorizedOnly)
|
||||
{
|
||||
// Init-only rather than positional (it was bolted onto the record after the
|
||||
// eight-member constructor shipped), so it is attached here instead.
|
||||
DashboardTags = ParseList(CreateModel.DashboardTags),
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -514,6 +544,11 @@ else
|
||||
AddList(parts, "read_tag_globs", constraints.ReadTagGlobs);
|
||||
AddList(parts, "write_tag_globs", constraints.WriteTagGlobs);
|
||||
AddList(parts, "browse_subtrees", constraints.BrowseSubtrees);
|
||||
|
||||
// Listed like the rest even though it restricts no data path: IsEmpty counts it, so a key
|
||||
// whose only policy is a dashboard tag is not "unconstrained", and omitting it here left
|
||||
// that key's cell empty — rendered as "-", the same cell a key with no policy at all gets.
|
||||
AddList(parts, "dashboard_tags", constraints.DashboardTags);
|
||||
if (constraints.MaxWriteClassification is { } max)
|
||||
{
|
||||
parts.Add($"max_write_classification={max}");
|
||||
@@ -548,7 +583,7 @@ else
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private sealed class ApiKeyCreateModel
|
||||
internal sealed class ApiKeyCreateModel
|
||||
{
|
||||
public string KeyId { get; set; } = string.Empty;
|
||||
|
||||
@@ -568,6 +603,8 @@ else
|
||||
|
||||
public string MaxWriteClassification { get; set; } = string.Empty;
|
||||
|
||||
public string DashboardTags { get; set; } = string.Empty;
|
||||
|
||||
public bool ReadAlarmOnly { get; set; }
|
||||
|
||||
public bool ReadHistorizedOnly { get; set; }
|
||||
@@ -583,6 +620,7 @@ else
|
||||
WriteTagGlobs = string.Empty;
|
||||
BrowseSubtrees = string.Empty;
|
||||
MaxWriteClassification = string.Empty;
|
||||
DashboardTags = string.Empty;
|
||||
ReadAlarmOnly = false;
|
||||
ReadHistorizedOnly = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user