180 lines
6.8 KiB
C#
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> ApplyActivityFiltersAsync(string baseFilter, cActivityFilterPolicy policy)
|
|
{
|
|
var result = await ActivityFilterCompiler.CompileAsync(policy, ResolveWorkspaceRolesAsync);
|
|
if (!result.Success)
|
|
{
|
|
LogEntry($"Activity filter rejected (fail closed): {result.Error}", LogLevels.Error);
|
|
return null;
|
|
}
|
|
|
|
return string.IsNullOrWhiteSpace(result.Clause)
|
|
? baseFilter
|
|
: $"({baseFilter}) AND ({result.Clause})";
|
|
}
|
|
|
|
internal cTicketActivityFilterCapabilities GetActivityFilterCapabilities()
|
|
{
|
|
var capabilities = new cTicketActivityFilterCapabilities
|
|
{
|
|
Provider = ActivityFilterCompiler.ProviderName,
|
|
CommonFields = new List<string> { ActivityFilterCompiler.AssignmentGroupField },
|
|
ProviderFields = new List<string> { ActivityFilterCompiler.QueueField }
|
|
};
|
|
|
|
if (SPSDataEngineSchemaReader.ClassGetIDFromName("ESMWorkspaceClassBase") != Guid.Empty)
|
|
capabilities.ProviderFields.Add(ActivityFilterCompiler.WorkspaceField);
|
|
|
|
return capabilities;
|
|
}
|
|
|
|
private async Task<ActivityFilterResolution> ResolveWorkspaceRolesAsync(cActivityFilter 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<cActivityFilterValue>())
|
|
{
|
|
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 ActivityFilterResolution
|
|
{
|
|
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 ActivityFilterResolution WorkspaceFailure(string error)
|
|
{
|
|
return new ActivityFilterResolution
|
|
{
|
|
Success = false,
|
|
Error = error
|
|
};
|
|
}
|
|
}
|
|
}
|