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 RoleIds { get; set; } } private static readonly object workspaceRoleCacheLock = new object(); private static readonly Dictionary workspaceRoleCache = new Dictionary(StringComparer.OrdinalIgnoreCase); internal async Task 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 { TicketFilterCompiler.AssignmentGroupField }, ProviderFields = new List { TicketFilterCompiler.QueueField } }; if (SPSDataEngineSchemaReader.ClassGetIDFromName("ESMWorkspaceClassBase") != Guid.Empty) capabilities.ProviderFields.Add(TicketFilterCompiler.WorkspaceField); return capabilities; } private async Task 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(); foreach (var value in filter?.Values ?? new List()) { 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() .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() .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 roleIds) { lock (workspaceRoleCacheLock) { if (workspaceRoleCache.TryGetValue(key, out var entry) && entry.ExpiresAtUtc > DateTime.UtcNow) { roleIds = new List(entry.RoleIds); return true; } } roleIds = null; return false; } private static void SetCachedWorkspaceRoles(string key, List roleIds) { lock (workspaceRoleCacheLock) { workspaceRoleCache[key] = new WorkspaceRoleCacheEntry { ExpiresAtUtc = DateTime.UtcNow.AddMinutes(5), RoleIds = new List(roleIds) }; } } private static TicketFilterResolution WorkspaceFailure(string error) { return new TicketFilterResolution { Success = false, Error = error }; } } }