feat(auth): ScadaBridge TransportExport excludes inbound API keys (re-arch C4; methods-only, import ignores legacy key sections); keys re-issued per environment
This commit is contained in:
@@ -82,7 +82,7 @@ public sealed class BundleExporter : IBundleExporter
|
||||
DatabaseConnections: resolved.DatabaseConnections,
|
||||
NotificationLists: resolved.NotificationLists,
|
||||
SmtpConfigurations: resolved.SmtpConfigs,
|
||||
ApiKeys: resolved.ApiKeys,
|
||||
// Inbound API keys are not transported between environments (re-arch C4).
|
||||
ApiMethods: resolved.ApiMethods);
|
||||
var contentDto = _entitySerializer.ToBundleContent(aggregate);
|
||||
|
||||
@@ -95,7 +95,6 @@ public sealed class BundleExporter : IBundleExporter
|
||||
DbConnections: resolved.DatabaseConnections.Count,
|
||||
NotificationLists: resolved.NotificationLists.Count,
|
||||
SmtpConfigs: resolved.SmtpConfigs.Count,
|
||||
ApiKeys: resolved.ApiKeys.Count,
|
||||
ApiMethods: resolved.ApiMethods.Count);
|
||||
|
||||
// 4. Build a TEMPLATE manifest. BundleSerializer.Pack re-stamps both
|
||||
|
||||
@@ -98,13 +98,9 @@ public sealed class DependencyResolver
|
||||
if (sm is not null) smtpConfigs[sm.Id] = sm;
|
||||
}
|
||||
|
||||
var apiKeys = new Dictionary<int, ApiKey>();
|
||||
foreach (var id in selection.ApiKeyIds.Distinct())
|
||||
{
|
||||
var k = await _inboundApi.GetApiKeyByIdAsync(id, ct).ConfigureAwait(false);
|
||||
if (k is not null) apiKeys[k.Id] = k;
|
||||
}
|
||||
|
||||
// Inbound API keys are intentionally NOT resolved into the bundle: per the
|
||||
// inbound-API-key re-architecture (C4) keys are not transported between
|
||||
// environments. Only API methods travel.
|
||||
var apiMethods = new Dictionary<int, ApiMethod>();
|
||||
foreach (var id in selection.ApiMethodIds.Distinct())
|
||||
{
|
||||
@@ -148,7 +144,6 @@ public sealed class DependencyResolver
|
||||
dbConnections.Values,
|
||||
notificationLists.Values,
|
||||
smtpConfigs.Values,
|
||||
apiKeys.Values,
|
||||
apiMethods.Values);
|
||||
|
||||
return new ResolvedExport(
|
||||
@@ -160,7 +155,6 @@ public sealed class DependencyResolver
|
||||
DatabaseConnections: dbConnections.Values.OrderBy(d => d.Name, StringComparer.Ordinal).ToList(),
|
||||
NotificationLists: notificationLists.Values.OrderBy(n => n.Name, StringComparer.Ordinal).ToList(),
|
||||
SmtpConfigs: smtpConfigs.Values.OrderBy(s => s.Host, StringComparer.Ordinal).ToList(),
|
||||
ApiKeys: apiKeys.Values.OrderBy(a => a.Name, StringComparer.Ordinal).ToList(),
|
||||
ApiMethods: apiMethods.Values.OrderBy(a => a.Name, StringComparer.Ordinal).ToList(),
|
||||
ContentManifest: manifest);
|
||||
}
|
||||
@@ -368,7 +362,6 @@ public sealed class DependencyResolver
|
||||
IEnumerable<DatabaseConnectionDefinition> dbConnections,
|
||||
IEnumerable<NotificationList> notificationLists,
|
||||
IEnumerable<SmtpConfiguration> smtpConfigs,
|
||||
IEnumerable<ApiKey> apiKeys,
|
||||
IEnumerable<ApiMethod> apiMethods)
|
||||
{
|
||||
var entries = new List<ManifestContentEntry>();
|
||||
@@ -419,10 +412,7 @@ public sealed class DependencyResolver
|
||||
{
|
||||
entries.Add(new ManifestContentEntry("SmtpConfiguration", s.Host, 1, Array.Empty<string>()));
|
||||
}
|
||||
foreach (var k in apiKeys.OrderBy(x => x.Name, StringComparer.Ordinal))
|
||||
{
|
||||
entries.Add(new ManifestContentEntry("ApiKey", k.Name, 1, Array.Empty<string>()));
|
||||
}
|
||||
// Inbound API keys are not transported (re-arch C4) — no ApiKey manifest entries.
|
||||
foreach (var m in apiMethods.OrderBy(x => x.Name, StringComparer.Ordinal))
|
||||
{
|
||||
entries.Add(new ManifestContentEntry("ApiMethod", m.Name, 1, Array.Empty<string>()));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi; // ApiMethod
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
||||
@@ -23,6 +23,6 @@ public sealed record ResolvedExport(
|
||||
IReadOnlyList<DatabaseConnectionDefinition> DatabaseConnections,
|
||||
IReadOnlyList<NotificationList> NotificationLists,
|
||||
IReadOnlyList<SmtpConfiguration> SmtpConfigs,
|
||||
IReadOnlyList<ApiKey> ApiKeys,
|
||||
// Inbound API keys are not transported between environments (re-arch C4); only methods.
|
||||
IReadOnlyList<ApiMethod> ApiMethods,
|
||||
IReadOnlyList<ManifestContentEntry> ContentManifest);
|
||||
|
||||
@@ -244,26 +244,8 @@ public sealed class ArtifactDiff
|
||||
return BuildItem("SmtpConfiguration", incoming.Host, changes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares an incoming API key against the existing API key in the database.
|
||||
/// </summary>
|
||||
/// <param name="incoming">The incoming API key from the bundle.</param>
|
||||
/// <param name="existing">The existing API key in the database, or null if new.</param>
|
||||
/// <returns>An import preview item describing the conflict type and differences.</returns>
|
||||
public ImportPreviewItem CompareApiKey(ApiKeyDto incoming, ApiKey? existing)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(incoming);
|
||||
if (existing is null) return New("ApiKey", incoming.Name);
|
||||
|
||||
var changes = new List<FieldChange>();
|
||||
AddIfDifferent(changes, "IsEnabled", existing.IsEnabled, incoming.IsEnabled);
|
||||
// KeyHash is opaque — record only changed/unchanged, not the value.
|
||||
if (!string.Equals(existing.KeyHash, incoming.KeyHash, StringComparison.Ordinal))
|
||||
{
|
||||
changes.Add(new FieldChange("KeyHash", "<changed>", "<changed>"));
|
||||
}
|
||||
return BuildItem("ApiKey", incoming.Name, changes);
|
||||
}
|
||||
// CompareApiKey was removed in re-arch C4: inbound API keys are not transported
|
||||
// between environments, so the import preview never diffs keys.
|
||||
|
||||
/// <summary>
|
||||
/// Compares an incoming API method against the existing API method in the database.
|
||||
@@ -277,7 +259,7 @@ public sealed class ArtifactDiff
|
||||
if (existing is null) return New("ApiMethod", incoming.Name);
|
||||
|
||||
var changes = new List<FieldChange>();
|
||||
AddIfDifferent(changes, "ApprovedApiKeyIds", existing.ApprovedApiKeyIds, incoming.ApprovedApiKeyIds);
|
||||
// ApprovedApiKeyIds is not transported (re-arch C4) and is excluded from the diff.
|
||||
AddIfDifferent(changes, "ParameterDefinitions", existing.ParameterDefinitions, incoming.ParameterDefinitions);
|
||||
AddIfDifferent(changes, "ReturnDefinition", existing.ReturnDefinition, incoming.ReturnDefinition);
|
||||
AddIfDifferent(changes, "TimeoutSeconds", existing.TimeoutSeconds, incoming.TimeoutSeconds);
|
||||
|
||||
@@ -408,14 +408,12 @@ public sealed class BundleImporter : IBundleImporter
|
||||
items.Add(_diff.CompareSmtpConfiguration(sm, existing));
|
||||
}
|
||||
|
||||
// ---- ApiKeys (no by-name lookup — scan GetAll) ----
|
||||
var allApiKeys = await _inboundApiRepo.GetAllApiKeysAsync(ct).ConfigureAwait(false);
|
||||
var apiKeyByName = allApiKeys.ToDictionary(k => k.Name, k => k, StringComparer.Ordinal);
|
||||
foreach (var k in content.ApiKeys)
|
||||
{
|
||||
apiKeyByName.TryGetValue(k.Name, out var existing);
|
||||
items.Add(_diff.CompareApiKey(k, existing));
|
||||
}
|
||||
// ---- ApiKeys ----
|
||||
// Inbound API keys are not transported between environments (re-arch C4).
|
||||
// New bundles never carry a keys section. A pre-C4 bundle may still contain
|
||||
// one; we do NOT surface those keys as importable preview rows (they would
|
||||
// offer Add/Overwrite actions for keys that can't be meaningfully re-created
|
||||
// from a hash). They are counted, ignored, and reported at apply time.
|
||||
|
||||
// ---- ApiMethods ----
|
||||
foreach (var m in content.ApiMethods)
|
||||
@@ -617,6 +615,12 @@ public sealed class BundleImporter : IBundleImporter
|
||||
r => r);
|
||||
var summary = new ImportSummary();
|
||||
|
||||
// Inbound API keys are not transported between environments (re-arch C4).
|
||||
// A pre-C4 bundle may still contain a keys section; we ignore those keys
|
||||
// entirely (never re-create them) but count them so the result can tell
|
||||
// the operator to re-issue keys on this environment.
|
||||
var apiKeysIgnored = content.ApiKeys?.Count ?? 0;
|
||||
|
||||
// Set the correlation BEFORE the transaction so any audit writes
|
||||
// triggered during the apply pick up the BundleImportId — AuditService
|
||||
// reads the scoped context at the moment LogAsync is called.
|
||||
@@ -660,7 +664,8 @@ public sealed class BundleImporter : IBundleImporter
|
||||
await ApplyDatabaseConnectionsAsync(content.DatabaseConnections, resolutionMap, user, summary, ct).ConfigureAwait(false);
|
||||
await ApplyNotificationListsAsync(content.NotificationLists, resolutionMap, user, summary, ct).ConfigureAwait(false);
|
||||
await ApplySmtpConfigsAsync(content.SmtpConfigs, resolutionMap, user, summary, ct).ConfigureAwait(false);
|
||||
await ApplyApiKeysAsync(content.ApiKeys, resolutionMap, user, summary, ct).ConfigureAwait(false);
|
||||
// Inbound API keys are NOT applied from a bundle (re-arch C4) — any keys
|
||||
// in a legacy bundle were counted above (apiKeysIgnored) and are skipped.
|
||||
await ApplyApiMethodsAsync(content.ApiMethods, resolutionMap, user, summary, ct).ConfigureAwait(false);
|
||||
|
||||
// FU-B / #39 + remainder of #37 — second-pass rewire of name-keyed
|
||||
@@ -700,6 +705,9 @@ public sealed class BundleImporter : IBundleImporter
|
||||
summary.Skipped,
|
||||
summary.Renamed,
|
||||
},
|
||||
// re-arch C4: legacy inbound API keys present in the bundle that
|
||||
// were ignored (not transported / not re-created here).
|
||||
ApiKeysIgnored = apiKeysIgnored,
|
||||
},
|
||||
cancellationToken: ct).ConfigureAwait(false);
|
||||
|
||||
@@ -721,7 +729,8 @@ public sealed class BundleImporter : IBundleImporter
|
||||
Skipped: summary.Skipped,
|
||||
Renamed: summary.Renamed,
|
||||
StaleInstanceIds: Array.Empty<int>(),
|
||||
AuditEventCorrelation: bundleImportId.ToString());
|
||||
AuditEventCorrelation: bundleImportId.ToString(),
|
||||
ApiKeysIgnored: apiKeysIgnored);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -2015,59 +2024,9 @@ public sealed class BundleImporter : IBundleImporter
|
||||
target.Credentials = dto.Secrets?.Values.TryGetValue("Credentials", out var cred) == true ? cred : null;
|
||||
}
|
||||
|
||||
private async Task ApplyApiKeysAsync(
|
||||
IReadOnlyList<ApiKeyDto> dtos,
|
||||
Dictionary<(string, string), ImportResolution> map,
|
||||
string user,
|
||||
ImportSummary summary,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (dtos.Count == 0) return;
|
||||
var all = await _inboundApiRepo.GetAllApiKeysAsync(ct).ConfigureAwait(false);
|
||||
var byName = all.ToDictionary(k => k.Name, k => k, StringComparer.Ordinal);
|
||||
|
||||
foreach (var dto in dtos)
|
||||
{
|
||||
var resolution = ResolveOrDefault(map, "ApiKey", dto.Name);
|
||||
switch (resolution.Action)
|
||||
{
|
||||
case ResolutionAction.Skip:
|
||||
summary.Skipped++;
|
||||
break;
|
||||
case ResolutionAction.Rename:
|
||||
{
|
||||
var name = resolution.RenameTo ?? dto.Name;
|
||||
var key = ApiKey.FromHash(name, dto.KeyHash);
|
||||
key.IsEnabled = dto.IsEnabled;
|
||||
await _inboundApiRepo.AddApiKeyAsync(key, ct).ConfigureAwait(false);
|
||||
await _auditService.LogAsync(user, "Create", "ApiKey", "0", name,
|
||||
new { key.Name, RenamedFrom = dto.Name }, ct).ConfigureAwait(false);
|
||||
summary.Renamed++;
|
||||
break;
|
||||
}
|
||||
case ResolutionAction.Overwrite when byName.TryGetValue(dto.Name, out var ex):
|
||||
ex.KeyHash = dto.KeyHash;
|
||||
ex.IsEnabled = dto.IsEnabled;
|
||||
await _inboundApiRepo.UpdateApiKeyAsync(ex, ct).ConfigureAwait(false);
|
||||
await _auditService.LogAsync(user, "Update", "ApiKey", ex.Id.ToString(), ex.Name,
|
||||
new { ex.Name, ex.IsEnabled }, ct).ConfigureAwait(false);
|
||||
summary.Overwritten++;
|
||||
break;
|
||||
case ResolutionAction.Add:
|
||||
case ResolutionAction.Overwrite:
|
||||
default:
|
||||
{
|
||||
var key = ApiKey.FromHash(dto.Name, dto.KeyHash);
|
||||
key.IsEnabled = dto.IsEnabled;
|
||||
await _inboundApiRepo.AddApiKeyAsync(key, ct).ConfigureAwait(false);
|
||||
await _auditService.LogAsync(user, "Create", "ApiKey", "0", key.Name,
|
||||
new { key.Name, key.IsEnabled }, ct).ConfigureAwait(false);
|
||||
summary.Added++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// ApplyApiKeysAsync was removed in re-arch C4: inbound API keys are not
|
||||
// transported between environments, so a bundle never re-creates keys. Any keys
|
||||
// present in a legacy (pre-C4) bundle are counted and ignored in ApplyAsync.
|
||||
|
||||
private async Task ApplyApiMethodsAsync(
|
||||
IReadOnlyList<ApiMethodDto> dtos,
|
||||
@@ -2098,7 +2057,9 @@ public sealed class BundleImporter : IBundleImporter
|
||||
}
|
||||
case ResolutionAction.Overwrite when existing is not null:
|
||||
existing.Script = dto.Script;
|
||||
existing.ApprovedApiKeyIds = dto.ApprovedApiKeyIds;
|
||||
// ApprovedApiKeyIds is NOT overwritten from a bundle (re-arch C4):
|
||||
// method→key scopes are re-granted per environment and any value on
|
||||
// the target row is preserved across an import.
|
||||
existing.ParameterDefinitions = dto.ParameterDefinitions;
|
||||
existing.ReturnDefinition = dto.ReturnDefinition;
|
||||
existing.TimeoutSeconds = dto.TimeoutSeconds;
|
||||
@@ -2124,9 +2085,10 @@ public sealed class BundleImporter : IBundleImporter
|
||||
|
||||
private static ApiMethod BuildApiMethod(ApiMethodDto dto, string? overrideName)
|
||||
{
|
||||
// ApprovedApiKeyIds is intentionally left at its default (null): keys are not
|
||||
// transported (re-arch C4) and method→key scopes are re-granted per environment.
|
||||
return new ApiMethod(overrideName ?? dto.Name, dto.Script)
|
||||
{
|
||||
ApprovedApiKeyIds = dto.ApprovedApiKeyIds,
|
||||
ParameterDefinitions = dto.ParameterDefinitions,
|
||||
ReturnDefinition = dto.ReturnDefinition,
|
||||
TimeoutSeconds = dto.TimeoutSeconds,
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace ZB.MOM.WW.ScadaBridge.Transport.Serialization;
|
||||
/// ignorant POCO types — what <see cref="EntitySerializer"/> consumes/produces
|
||||
/// on the application side of the bundle boundary.
|
||||
/// </summary>
|
||||
// ApiKeys is intentionally absent: inbound API keys are not transported between
|
||||
// environments (re-arch C4). The aggregate only carries API methods.
|
||||
public sealed record EntityAggregate(
|
||||
IReadOnlyList<TemplateFolder> TemplateFolders,
|
||||
IReadOnlyList<Template> Templates,
|
||||
@@ -22,7 +24,6 @@ public sealed record EntityAggregate(
|
||||
IReadOnlyList<DatabaseConnectionDefinition> DatabaseConnections,
|
||||
IReadOnlyList<NotificationList> NotificationLists,
|
||||
IReadOnlyList<SmtpConfiguration> SmtpConfigurations,
|
||||
IReadOnlyList<ApiKey> ApiKeys,
|
||||
IReadOnlyList<ApiMethod> ApiMethods);
|
||||
|
||||
/// <summary>
|
||||
@@ -30,6 +31,14 @@ public sealed record EntityAggregate(
|
||||
/// order so importers can apply them inline. Lists are never null on the wire
|
||||
/// — empty arrays are preferred over nulls so JSON consumers can rely on each
|
||||
/// property being present.
|
||||
/// <para>
|
||||
/// <see cref="ApiKeys"/> is a <b>legacy, read-only</b> field retained purely for
|
||||
/// backward-compatible deserialization of bundles produced before the
|
||||
/// inbound-API-key re-architecture (C4). New exports never populate it (it stays
|
||||
/// <c>null</c> and is dropped from the JSON by the serializer's
|
||||
/// <c>WhenWritingNull</c> policy); the importer counts any keys present in an old
|
||||
/// bundle, ignores them, and surfaces a note — keys are re-created per environment.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed record BundleContentDto(
|
||||
IReadOnlyList<TemplateFolderDto> TemplateFolders,
|
||||
@@ -39,8 +48,8 @@ public sealed record BundleContentDto(
|
||||
IReadOnlyList<DatabaseConnectionDto> DatabaseConnections,
|
||||
IReadOnlyList<NotificationListDto> NotificationLists,
|
||||
IReadOnlyList<SmtpConfigDto> SmtpConfigs,
|
||||
IReadOnlyList<ApiKeyDto> ApiKeys,
|
||||
IReadOnlyList<ApiMethodDto> ApiMethods);
|
||||
IReadOnlyList<ApiMethodDto> ApiMethods,
|
||||
IReadOnlyList<ApiKeyDto>? ApiKeys = null);
|
||||
|
||||
/// <summary>
|
||||
/// Carved-off secret values for an entity. The outer DTO carries all non-
|
||||
@@ -144,16 +153,20 @@ public sealed record SmtpConfigDto(
|
||||
TimeSpan RetryDelay,
|
||||
SecretsBlock? Secrets);
|
||||
|
||||
// Legacy DTO: only deserialized from pre-C4 bundles so the importer can count and
|
||||
// ignore the keys they contain. New exports never emit an ApiKeys array.
|
||||
public sealed record ApiKeyDto(
|
||||
string Name,
|
||||
string KeyHash,
|
||||
bool IsEnabled,
|
||||
SecretsBlock? Secrets);
|
||||
|
||||
// ApprovedApiKeyIds is intentionally absent: it linked methods to keys that are no
|
||||
// longer transported (re-arch C4). Scopes are re-granted per environment. The field
|
||||
// in any old bundle is simply ignored on read.
|
||||
public sealed record ApiMethodDto(
|
||||
string Name,
|
||||
string Script,
|
||||
string? ApprovedApiKeyIds,
|
||||
string? ParameterDefinitions,
|
||||
string? ReturnDefinition,
|
||||
int TimeoutSeconds);
|
||||
|
||||
@@ -153,17 +153,14 @@ public sealed class EntitySerializer
|
||||
RetryDelay: smtp.RetryDelay,
|
||||
Secrets: secrets);
|
||||
}).ToList(),
|
||||
// ApiKey stores only KeyHash already; no plaintext to carve. SecretsBlock
|
||||
// stays null per design — KeyHash is on the public DTO.
|
||||
ApiKeys: aggregate.ApiKeys.Select(k => new ApiKeyDto(
|
||||
Name: k.Name,
|
||||
KeyHash: k.KeyHash,
|
||||
IsEnabled: k.IsEnabled,
|
||||
Secrets: null)).ToList(),
|
||||
// Inbound API keys are not transported between environments (re-arch C4):
|
||||
// the bundle carries API methods only. ApiMethod.ApprovedApiKeyIds is also
|
||||
// excluded — it references keys that aren't in the bundle; method→key scopes
|
||||
// are re-granted per environment via the admin UI/CLI. The legacy ApiKeys
|
||||
// field on the DTO stays null (and is dropped by WhenWritingNull).
|
||||
ApiMethods: aggregate.ApiMethods.Select(m => new ApiMethodDto(
|
||||
Name: m.Name,
|
||||
Script: m.Script,
|
||||
ApprovedApiKeyIds: m.ApprovedApiKeyIds,
|
||||
ParameterDefinitions: m.ParameterDefinitions,
|
||||
ReturnDefinition: m.ReturnDefinition,
|
||||
TimeoutSeconds: m.TimeoutSeconds)).ToList());
|
||||
@@ -336,21 +333,15 @@ public sealed class EntitySerializer
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var apiKeys = content.ApiKeys
|
||||
.Select((dto, ix) =>
|
||||
{
|
||||
var key = ApiKey.FromHash(dto.Name, dto.KeyHash);
|
||||
key.Id = ix + 1;
|
||||
key.IsEnabled = dto.IsEnabled;
|
||||
return key;
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// Inbound API keys are not transported (re-arch C4) — content.ApiKeys is a
|
||||
// legacy field only present on pre-C4 bundles; it is ignored here. The
|
||||
// BundleImporter is responsible for counting and reporting any such keys.
|
||||
// ApiMethod.ApprovedApiKeyIds is likewise not reconstructed: scopes are
|
||||
// re-granted per environment.
|
||||
var apiMethods = content.ApiMethods
|
||||
.Select((dto, ix) => new ApiMethod(dto.Name, dto.Script)
|
||||
{
|
||||
Id = ix + 1,
|
||||
ApprovedApiKeyIds = dto.ApprovedApiKeyIds,
|
||||
ParameterDefinitions = dto.ParameterDefinitions,
|
||||
ReturnDefinition = dto.ReturnDefinition,
|
||||
TimeoutSeconds = dto.TimeoutSeconds,
|
||||
@@ -366,7 +357,6 @@ public sealed class EntitySerializer
|
||||
DatabaseConnections: databaseConnections,
|
||||
NotificationLists: notificationLists,
|
||||
SmtpConfigurations: smtpConfigurations,
|
||||
ApiKeys: apiKeys,
|
||||
ApiMethods: apiMethods);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user