Files
C4IT-F4SD-M42WebApi/F4SDM42WebApi/TicketFilterService.cs
2026-07-23 12:47:28 +02:00

180 lines
6.8 KiB
C#

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using C4IT.FASD.Base;
using C4IT.Logging;
using update4u.SPS.DataLayer;
using static C4IT.Logging.cLogManager;
namespace C4IT.F4SD
{
public partial class F4SDHelperService
{
private sealed class WorkspaceRoleCacheEntry
{
public DateTime ExpiresAtUtc { get; set; }
public List<Guid> RoleIds { get; set; }
}
private static readonly object workspaceRoleCacheLock = new object();
private static readonly Dictionary<string, WorkspaceRoleCacheEntry> workspaceRoleCache =
new Dictionary<string, WorkspaceRoleCacheEntry>(StringComparer.OrdinalIgnoreCase);
internal async Task<string> ApplyTicketFiltersAsync(string baseFilter, cTicketFilterPolicy policy)
{
var result = await TicketFilterCompiler.CompileAsync(policy, ResolveWorkspaceRolesAsync);
if (!result.Success)
{
LogEntry($"Ticket filter rejected (fail closed): {result.Error}", LogLevels.Error);
return null;
}
return string.IsNullOrWhiteSpace(result.Clause)
? baseFilter
: $"({baseFilter}) AND ({result.Clause})";
}
internal cTicketFilterCapabilities GetTicketFilterCapabilities()
{
var capabilities = new cTicketFilterCapabilities
{
Provider = TicketFilterCompiler.ProviderName,
CommonFields = new List<string> { TicketFilterCompiler.AssignmentGroupField },
ProviderFields = new List<string> { TicketFilterCompiler.QueueField }
};
if (SPSDataEngineSchemaReader.ClassGetIDFromName("ESMWorkspaceClassBase") != Guid.Empty)
capabilities.ProviderFields.Add(TicketFilterCompiler.WorkspaceField);
return capabilities;
}
private async Task<TicketFilterResolution> ResolveWorkspaceRolesAsync(cTicketFilter filter)
{
await Task.Delay(0);
var workspaceClassId = SPSDataEngineSchemaReader.ClassGetIDFromName("ESMWorkspaceClassBase");
if (workspaceClassId == Guid.Empty)
return WorkspaceFailure("ESMWorkspaceClassBase is not available.");
var roleIds = new HashSet<Guid>();
foreach (var value in filter?.Values ?? new List<cTicketFilterValue>())
{
var key = !string.IsNullOrWhiteSpace(value?.ID)
? "id:" + value.ID.Trim()
: "name:" + value?.Name?.Trim();
if (string.IsNullOrWhiteSpace(key) || key.EndsWith(":", StringComparison.Ordinal))
return WorkspaceFailure("A Workspace filter value has neither ID nor Name.");
if (TryGetCachedWorkspaceRoles(key, out var cached))
{
roleIds.UnionWith(cached);
continue;
}
DataTable table;
try
{
table = LoadWorkspaceRoles(
workspaceClassId,
!string.IsNullOrWhiteSpace(value.ID)
? $"ID = '{Escape(value.ID.Trim())}'"
: $"Title = '{Escape(value.Name.Trim())}'");
// IDs are stable and therefore preferred. The name is kept as a
// portable fallback for configurations imported into another ESM.
if ((table?.Rows == null || table.Rows.Count == 0)
&& !string.IsNullOrWhiteSpace(value.ID)
&& !string.IsNullOrWhiteSpace(value.Name))
{
table = LoadWorkspaceRoles(
workspaceClassId,
$"Title = '{Escape(value.Name.Trim())}'");
}
}
catch (Exception exception)
{
LogException(exception);
return WorkspaceFailure($"Workspace relation ESMWorkspaceClassBase.Roles could not be loaded for '{key}'.");
}
if (table?.Rows == null || table.Rows.Count == 0)
return WorkspaceFailure($"Workspace '{key}' was not found or has no resolvable Roles relation.");
var workspaceIds = table.Rows.Cast<DataRow>()
.Select(row => getGuidFromObject(row["WorkspaceID"]))
.Where(id => id != Guid.Empty)
.Distinct()
.ToList();
if (workspaceIds.Count != 1)
return WorkspaceFailure($"Workspace '{key}' is not unique.");
var resolved = table.Rows.Cast<DataRow>()
.Select(row => getGuidFromObject(row["RoleID"]))
.Where(id => id != Guid.Empty)
.Distinct()
.ToList();
if (resolved.Count == 0)
return WorkspaceFailure($"Workspace '{key}' contains no roles.");
SetCachedWorkspaceRoles(key, resolved);
roleIds.UnionWith(resolved);
}
return new TicketFilterResolution
{
Success = true,
RoleIds = roleIds.ToList()
};
}
private static DataTable LoadWorkspaceRoles(Guid workspaceClassId, string where)
{
return FragmentRequestBase.SimpleLoad(
workspaceClassId,
"ID as WorkspaceID, Title as WorkspaceTitle, Roles.T(SPSSecurityClassRole).ID as RoleID",
where);
}
private static bool TryGetCachedWorkspaceRoles(string key, out List<Guid> roleIds)
{
lock (workspaceRoleCacheLock)
{
if (workspaceRoleCache.TryGetValue(key, out var entry) && entry.ExpiresAtUtc > DateTime.UtcNow)
{
roleIds = new List<Guid>(entry.RoleIds);
return true;
}
}
roleIds = null;
return false;
}
private static void SetCachedWorkspaceRoles(string key, List<Guid> roleIds)
{
lock (workspaceRoleCacheLock)
{
workspaceRoleCache[key] = new WorkspaceRoleCacheEntry
{
ExpiresAtUtc = DateTime.UtcNow.AddMinutes(5),
RoleIds = new List<Guid>(roleIds)
};
}
}
private static TicketFilterResolution WorkspaceFailure(string error)
{
return new TicketFilterResolution
{
Success = false,
Error = error
};
}
}
}