diff --git a/F4SDM42WebApi/ActivityFilterService.cs b/F4SDM42WebApi/ActivityFilterService.cs new file mode 100644 index 0000000..1c887f6 --- /dev/null +++ b/F4SDM42WebApi/ActivityFilterService.cs @@ -0,0 +1,179 @@ +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 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 { ActivityFilterCompiler.AssignmentGroupField }, + ProviderFields = new List { ActivityFilterCompiler.QueueField } + }; + + if (SPSDataEngineSchemaReader.ClassGetIDFromName("ESMWorkspaceClassBase") != Guid.Empty) + capabilities.ProviderFields.Add(ActivityFilterCompiler.WorkspaceField); + + return capabilities; + } + + private async Task 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(); + 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 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 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 ActivityFilterResolution WorkspaceFailure(string error) + { + return new ActivityFilterResolution + { + Success = false, + Error = error + }; + } + } +} diff --git a/F4SDM42WebApi/F4SD - M42WebApi.csproj b/F4SDM42WebApi/F4SD - M42WebApi.csproj index f9f36b2..4143401 100644 --- a/F4SDM42WebApi/F4SD - M42WebApi.csproj +++ b/F4SDM42WebApi/F4SD - M42WebApi.csproj @@ -90,12 +90,16 @@ - - + + + - - + + + Shared\ActivityFilterCompiler.cs + + Properties\SharedAssemblyInfo.cs diff --git a/F4SDM42WebApi/F4SDHelperService.UserId.cs b/F4SDM42WebApi/F4SDHelperService.UserId.cs index 9fbd131..d0dc9eb 100644 --- a/F4SDM42WebApi/F4SDHelperService.UserId.cs +++ b/F4SDM42WebApi/F4SDHelperService.UserId.cs @@ -22,7 +22,8 @@ namespace C4IT.F4SD Guid userId, int hours, int queueoption, - List queues) + List queues, + cActivityFilterPolicy filters = null) { var method = MethodBase.GetCurrentMethod(); LogMethodBegin(method); @@ -33,6 +34,9 @@ namespace C4IT.F4SD return new List(); var activityFilter = GetActivityFilter(userId, hours, ticketAndServiceRequestEnabled(), queueoption, queues); + activityFilter = await ApplyActivityFiltersAsync(activityFilter, filters); + if (string.IsNullOrWhiteSpace(activityFilter)) + return new List(); LogEntry($"Generating ticket list for userId '{userId}'. ASQL filter: {activityFilter}", LogLevels.Debug); var activityTable = FragmentRequestBase.SimpleLoad( @@ -91,7 +95,8 @@ namespace C4IT.F4SD string scope, IEnumerable keys, int queueoption, - List queues) + List queues, + cActivityFilterPolicy filters = null) { var method = MethodBase.GetCurrentMethod(); LogMethodBegin(method); @@ -106,10 +111,10 @@ namespace C4IT.F4SD if (normalizedKeys.Count == 0) normalizedKeys.AddRange(TicketOverviewKeys); - var entries = await LoadTicketOverviewEntries(userId, useRoleScope, queueoption, queues); + var entries = await LoadTicketOverviewEntries(userId, useRoleScope, queueoption, queues, filters); List unassignedEntries = null; if (!useRoleScope && normalizedKeys.Any(IsUnassignedOverviewKey)) - unassignedEntries = await LoadTicketOverviewUnassignedEntriesForPersonalScope(userId, queueoption, queues); + unassignedEntries = await LoadTicketOverviewUnassignedEntriesForPersonalScope(userId, queueoption, queues, filters); var counts = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var key in normalizedKeys) @@ -138,10 +143,11 @@ namespace C4IT.F4SD IEnumerable roleGuids, IEnumerable keys, int queueoption, - List queues) + List queues, + cActivityFilterPolicy filters = null) { var roleIds = await ResolveTicketOverviewRoleIdsAsync(userId, roleGuids); - return await getTicketOverviewCountsByRoles(null, roleIds, keys, queueoption, queues); + return await getTicketOverviewCountsByRoles(null, roleIds, keys, queueoption, queues, filters); } internal async Task> getTicketOverviewRelations( @@ -150,7 +156,8 @@ namespace C4IT.F4SD string key, int count, int queueoption, - List queues) + List queues, + cActivityFilterPolicy filters = null) { var method = MethodBase.GetCurrentMethod(); LogMethodBegin(method); @@ -161,8 +168,8 @@ namespace C4IT.F4SD var useRoleScope = string.Equals(scope, "role", StringComparison.OrdinalIgnoreCase); var entries = !useRoleScope && IsUnassignedOverviewKey(key) - ? await LoadTicketOverviewUnassignedEntriesForPersonalScope(userId, queueoption, queues) - : await LoadTicketOverviewEntries(userId, useRoleScope, queueoption, queues); + ? await LoadTicketOverviewUnassignedEntriesForPersonalScope(userId, queueoption, queues, filters) + : await LoadTicketOverviewEntries(userId, useRoleScope, queueoption, queues, filters); var filtered = entries .Where(entry => MatchesTicketOverviewKey(entry, key)) @@ -212,25 +219,28 @@ namespace C4IT.F4SD Guid userId, bool useRoleScope, int queueoption, - List queues) + List queues, + cActivityFilterPolicy filters = null) { if (userId == Guid.Empty) return new List(); - var filter = await BuildTicketOverviewFilterAsync(userId, useRoleScope, queueoption, queues); + var filter = await BuildTicketOverviewFilterAsync(userId, useRoleScope, queueoption, queues, filters); return await LoadTicketOverviewEntriesByFilter(filter); } private async Task> LoadTicketOverviewUnassignedEntriesForPersonalScope( Guid userId, int queueoption, - List queues) + List queues, + cActivityFilterPolicy filters = null) { if (userId == Guid.Empty) return new List(); var roleIds = await ResolveTicketOverviewRoleIdsAsync(userId, null); var filter = BuildTicketOverviewFilterForRoleIds(roleIds, null, queueoption, queues); + filter = await ApplyActivityFiltersAsync(filter, filters); if (string.IsNullOrWhiteSpace(filter)) return new List(); @@ -241,14 +251,16 @@ namespace C4IT.F4SD Guid userId, bool useRoleScope, int queueoption, - List queues) + List queues, + cActivityFilterPolicy filters = null) { var filter = BuildTicketOverviewBaseFilter(queueoption, queues); if (!useRoleScope) - return filter + $" AND (Recipient = '{Escape(userId.ToString("D"))}')"; + return await ApplyActivityFiltersAsync(filter + $" AND (Recipient = '{Escape(userId.ToString("D"))}')", filters); var roleIds = await ResolveTicketOverviewRoleIdsAsync(userId, null); - return BuildTicketOverviewFilterForRoleIds(roleIds, filter); + filter = BuildTicketOverviewFilterForRoleIds(roleIds, filter); + return await ApplyActivityFiltersAsync(filter, filters); } private async Task> ResolveTicketOverviewRoleIdsAsync(Guid userId, IEnumerable roleGuids) diff --git a/F4SDM42WebApi/F4SDHelperService.cs b/F4SDM42WebApi/F4SDHelperService.cs index aad51af..77d0256 100644 --- a/F4SDM42WebApi/F4SDHelperService.cs +++ b/F4SDM42WebApi/F4SDHelperService.cs @@ -186,7 +186,7 @@ namespace C4IT.F4SD } - internal async Task> getTicketListByUser(string userSid, int hours, int queueoption, List queues) + internal async Task> getTicketListByUser(string userSid, int hours, int queueoption, List queues, cActivityFilterPolicy filters = null) { var CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); @@ -196,7 +196,10 @@ namespace C4IT.F4SD await Task.Delay(0); var tickets = new List(); - var activityFilter = GetActivityFilter(userSid, hours, ticketAndServiceRequestEnabled(), queueoption, queues); + var activityFilter = GetActivityFilter(userSid, hours, ticketAndServiceRequestEnabled(), queueoption, queues); + activityFilter = await ApplyActivityFiltersAsync(activityFilter, filters); + if (string.IsNullOrWhiteSpace(activityFilter)) + return new List(); LogEntry($"ASql Filter: {activityFilter}"); var activityTable = FragmentRequestBase.SimpleLoad(SPSActivityClassBaseID, @@ -374,12 +377,13 @@ namespace C4IT.F4SD public string ActivityType { get; set; } } - internal async Task getTicketOverviewCounts( - string sid, - string scope, - IEnumerable keys, - int queueoption, - List queues) + internal async Task getTicketOverviewCounts( + string sid, + string scope, + IEnumerable keys, + int queueoption, + List queues, + cActivityFilterPolicy filters = null) { var CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); @@ -396,11 +400,11 @@ namespace C4IT.F4SD normalizedKeys.AddRange(TicketOverviewKeys); } - var entries = await LoadTicketOverviewEntries(sid, useRoleScope, queueoption, queues); + var entries = await LoadTicketOverviewEntries(sid, useRoleScope, queueoption, queues, filters); List unassignedEntries = null; if (!useRoleScope && normalizedKeys.Any(IsUnassignedOverviewKey)) { - unassignedEntries = await LoadTicketOverviewUnassignedEntriesForPersonalScope(sid, queueoption, queues); + unassignedEntries = await LoadTicketOverviewUnassignedEntriesForPersonalScope(sid, queueoption, queues, filters); } var counts = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -430,12 +434,13 @@ namespace C4IT.F4SD } } - internal async Task getTicketOverviewCountsByRoles( - string sid, - IEnumerable roleGuids, - IEnumerable keys, - int queueoption, - List queues) + internal async Task getTicketOverviewCountsByRoles( + string sid, + IEnumerable roleGuids, + IEnumerable keys, + int queueoption, + List queues, + cActivityFilterPolicy filters = null) { var CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); @@ -455,7 +460,7 @@ namespace C4IT.F4SD if (roleIds.Count == 0) return new TicketOverviewCountsByRoleResult(); - var entries = await LoadTicketOverviewEntriesByRoleIds(roleIds, queueoption, queues); + var entries = await LoadTicketOverviewEntriesByRoleIds(roleIds, queueoption, queues, filters); var entriesByRole = entries .GroupBy(entry => entry.RecipientRoleId) .ToDictionary(group => group.Key, group => group.ToList()); @@ -493,13 +498,14 @@ namespace C4IT.F4SD } } - internal async Task> getTicketOverviewRelations( - string sid, - string scope, - string key, - int count, - int queueoption, - List queues) + internal async Task> getTicketOverviewRelations( + string sid, + string scope, + string key, + int count, + int queueoption, + List queues, + cActivityFilterPolicy filters = null) { var CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); @@ -512,11 +518,11 @@ namespace C4IT.F4SD List entries; if (!useRoleScope && IsUnassignedOverviewKey(key)) { - entries = await LoadTicketOverviewUnassignedEntriesForPersonalScope(sid, queueoption, queues); + entries = await LoadTicketOverviewUnassignedEntriesForPersonalScope(sid, queueoption, queues, filters); } else { - entries = await LoadTicketOverviewEntries(sid, useRoleScope, queueoption, queues); + entries = await LoadTicketOverviewEntries(sid, useRoleScope, queueoption, queues, filters); } var filtered = entries @@ -572,11 +578,12 @@ namespace C4IT.F4SD } } - private async Task> LoadTicketOverviewEntries( - string sid, - bool useRoleScope, - int queueoption, - List queues) + private async Task> LoadTicketOverviewEntries( + string sid, + bool useRoleScope, + int queueoption, + List queues, + cActivityFilterPolicy filters = null) { var CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); @@ -587,7 +594,7 @@ namespace C4IT.F4SD if (string.IsNullOrWhiteSpace(sid)) return new List(); - var filter = await BuildTicketOverviewFilterAsync(sid, useRoleScope, queueoption, queues); + var filter = await BuildTicketOverviewFilterAsync(sid, useRoleScope, queueoption, queues, filters); return await LoadTicketOverviewEntriesByFilter(filter); } catch (Exception E) @@ -601,10 +608,11 @@ namespace C4IT.F4SD } } - private async Task> LoadTicketOverviewUnassignedEntriesForPersonalScope( - string sid, - int queueoption, - List queues) + private async Task> LoadTicketOverviewUnassignedEntriesForPersonalScope( + string sid, + int queueoption, + List queues, + cActivityFilterPolicy filters = null) { var CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); @@ -616,7 +624,8 @@ namespace C4IT.F4SD return new List(); var roleIds = await ResolveTicketOverviewRoleIdsAsync(sid, null); - var filter = BuildTicketOverviewFilterForRoleIds(roleIds, null, queueoption, queues); + var filter = BuildTicketOverviewFilterForRoleIds(roleIds, null, queueoption, queues); + filter = await ApplyActivityFiltersAsync(filter, filters); if (string.IsNullOrWhiteSpace(filter)) return new List(); @@ -634,10 +643,11 @@ namespace C4IT.F4SD } } - private async Task> LoadTicketOverviewEntriesByRoleIds( - IEnumerable roleIds, - int queueoption, - List queues) + private async Task> LoadTicketOverviewEntriesByRoleIds( + IEnumerable roleIds, + int queueoption, + List queues, + cActivityFilterPolicy filters = null) { var CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); @@ -645,7 +655,8 @@ namespace C4IT.F4SD { await Task.Delay(0); - var filter = BuildTicketOverviewFilterForRoleIds(roleIds, null, queueoption, queues); + var filter = BuildTicketOverviewFilterForRoleIds(roleIds, null, queueoption, queues); + filter = await ApplyActivityFiltersAsync(filter, filters); return await LoadTicketOverviewEntriesByFilter(filter); } catch (Exception E) @@ -784,23 +795,25 @@ namespace C4IT.F4SD return null; } - private async Task BuildTicketOverviewFilterAsync( - string sid, - bool useRoleScope, - int queueoption, - List queues) + private async Task BuildTicketOverviewFilterAsync( + string sid, + bool useRoleScope, + int queueoption, + List queues, + cActivityFilterPolicy filters = null) { var filter = BuildTicketOverviewBaseFilter(queueoption, queues); if (!useRoleScope) { var recipientFilter = $"Recipient.Accounts.T(SPSAccountClassAd).Sid = '{Escape(sid)}'"; - filter += $" AND ({recipientFilter})"; - return filter; + filter += $" AND ({recipientFilter})"; + return await ApplyActivityFiltersAsync(filter, filters); } var roleIds = await ResolveTicketOverviewRoleIdsAsync(sid, null); - return BuildTicketOverviewFilterForRoleIds(roleIds, filter); + filter = BuildTicketOverviewFilterForRoleIds(roleIds, filter); + return await ApplyActivityFiltersAsync(filter, filters); } private string BuildTicketOverviewBaseFilter(int queueoption, List queues) diff --git a/F4SDM42WebApi/F4SDM42WebApiController.cs b/F4SDM42WebApi/F4SDM42WebApiController.cs index 7a7a25f..3caefd2 100644 --- a/F4SDM42WebApi/F4SDM42WebApiController.cs +++ b/F4SDM42WebApi/F4SDM42WebApiController.cs @@ -154,6 +154,25 @@ namespace C4IT.F4SD ParseQueues(queues)) ?? new List(); } + [Route("v2/getTicketList"), HttpPost] + public async Task> getTicketListV2([FromBody] cTicketActivityListRequest request) + { + if (request?.UserId != Guid.Empty) + return await _f4stHelperService.getTicketListByUser( + request.UserId, + request.Hours, + 0, + new List(), + request.Filters) ?? new List(); + + return await _f4stHelperService.getTicketListByUser( + request?.Sid ?? string.Empty, + request?.Hours ?? 0, + 0, + new List(), + request?.Filters) ?? new List(); + } + [Route("getTicketDetails"), HttpGet] public async Task getTicketDetails([FromUri] Guid objectId) @@ -227,6 +246,28 @@ namespace C4IT.F4SD ParseQueues(queues)) ?? new F4SDHelperService.TicketOverviewCountsResult(); } + [Route("v2/getTicketOverviewCounts"), HttpPost] + public async Task getTicketOverviewCountsV2( + [FromBody] cTicketOverviewFilterRequest request) + { + if (request?.UserId != Guid.Empty) + return await _f4stHelperService.getTicketOverviewCounts( + request.UserId, + request.Scope, + request.Keys, + 0, + new List(), + request.Filters) ?? new F4SDHelperService.TicketOverviewCountsResult(); + + return await _f4stHelperService.getTicketOverviewCounts( + request?.Sid ?? string.Empty, + request?.Scope ?? "personal", + request?.Keys ?? new List(), + 0, + new List(), + request?.Filters) ?? new F4SDHelperService.TicketOverviewCountsResult(); + } + [Obsolete("Use getTicketOverviewCountsByRolesForUser with a Matrix42 user ID.")] [Route("getTicketOverviewCountsByRoles"), HttpPost] public async Task getTicketOverviewCountsByRoles([FromBody] TicketOverviewCountsByRolesRequest request) @@ -322,6 +363,36 @@ namespace C4IT.F4SD queueoption, ParseQueues(queues)) ?? new List(); } + + [Route("v2/getTicketOverviewRelations"), HttpPost] + public async Task> getTicketOverviewRelationsV2( + [FromBody] cTicketOverviewFilterRequest request) + { + if (request?.UserId != Guid.Empty) + return await _f4stHelperService.getTicketOverviewRelations( + request.UserId, + request.Scope, + request.Key, + request.Count, + 0, + new List(), + request.Filters) ?? new List(); + + return await _f4stHelperService.getTicketOverviewRelations( + request?.Sid ?? string.Empty, + request?.Scope ?? "personal", + request?.Key ?? string.Empty, + request?.Count ?? 0, + 0, + new List(), + request?.Filters) ?? new List(); + } + + [Route("v2/getActivityFilterCapabilities"), HttpGet] + public cTicketActivityFilterCapabilities getActivityFilterCapabilitiesV2() + { + return _f4stHelperService.GetActivityFilterCapabilities(); + } /* [Route("updateActivitySolution/{objectId}"), HttpPost] public async Task updateActivitySolution(Guid objectId, [FromBody] string SolutionHtml) diff --git a/Legacy/F4SDM42WebApi/ActivityFilterService.cs b/Legacy/F4SDM42WebApi/ActivityFilterService.cs new file mode 100644 index 0000000..fa8d053 --- /dev/null +++ b/Legacy/F4SDM42WebApi/ActivityFilterService.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System; +using System.Threading; +using System.Threading.Tasks; + +using C4IT.FASD.Base; +using C4IT.Logging; + +using static C4IT.Logging.cLogManager; + +namespace C4IT.F4SD +{ + public partial class F4SDHelperService + { + private static readonly AsyncLocal currentActivityFilters = + new AsyncLocal(); + + private sealed class ActivityFilterScope : IDisposable + { + private readonly cActivityFilterPolicy previous; + + public ActivityFilterScope(cActivityFilterPolicy filters) + { + previous = currentActivityFilters.Value; + currentActivityFilters.Value = filters; + } + + public void Dispose() + { + currentActivityFilters.Value = previous; + } + } + + internal IDisposable BeginActivityFilterScope(cActivityFilterPolicy filters) + { + return new ActivityFilterScope(filters); + } + + internal async Task ApplyActivityFiltersAsync(string baseFilter, cActivityFilterPolicy policy) + { + var result = await ActivityFilterCompiler.CompileAsync(policy ?? currentActivityFilters.Value, null); + 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() + { + return new cTicketActivityFilterCapabilities + { + Provider = ActivityFilterCompiler.ProviderName, + CommonFields = new List { ActivityFilterCompiler.AssignmentGroupField }, + ProviderFields = new List { ActivityFilterCompiler.QueueField } + }; + } + } +} diff --git a/Legacy/F4SDM42WebApi/F4SD - M42WebApi.Legacy.csproj b/Legacy/F4SDM42WebApi/F4SD - M42WebApi.Legacy.csproj index 188d6a8..cb2a9f7 100644 --- a/Legacy/F4SDM42WebApi/F4SD - M42WebApi.Legacy.csproj +++ b/Legacy/F4SDM42WebApi/F4SD - M42WebApi.Legacy.csproj @@ -54,8 +54,10 @@ + + Shared\ActivityFilterCompiler.cs Properties\SharedAssemblyInfo.cs diff --git a/Legacy/F4SDM42WebApi/F4SDHelperService.cs b/Legacy/F4SDM42WebApi/F4SDHelperService.cs index ccabb2d..dac90a2 100644 --- a/Legacy/F4SDM42WebApi/F4SDHelperService.cs +++ b/Legacy/F4SDM42WebApi/F4SDHelperService.cs @@ -29,7 +29,7 @@ using static C4IT.Logging.cLogManager; namespace C4IT.F4SD { - public class F4SDHelperService + public partial class F4SDHelperService { private static Guid SPSUserClassBaseID = SPSDataEngineSchemaReader.ClassGetIDFromName("SPSUserClassBase"); private static Guid SPSAccountClassBaseID = SPSDataEngineSchemaReader.ClassGetIDFromName("SPSAccountClassBase"); @@ -166,6 +166,9 @@ namespace C4IT.F4SD var tickets = new List(); var activityFilter = GetActivityFilter(userSid, hours, ticketAndServiceRequestEnabled(), queueoption, queues); + activityFilter = await ApplyActivityFiltersAsync(activityFilter, null); + if (string.IsNullOrWhiteSpace(activityFilter)) + return new List(); LogEntry($"ASql Filter: {activityFilter}"); var activityTable = FragmentRequestBase.SimpleLoad(SPSActivityClassBaseID, @@ -279,6 +282,9 @@ namespace C4IT.F4SD var tickets = new List(); var activityFilter = GetActivityFilter(userId, hours, ticketAndServiceRequestEnabled(), queueoption, queues); + activityFilter = await ApplyActivityFiltersAsync(activityFilter, null); + if (string.IsNullOrWhiteSpace(activityFilter)) + return new List(); LogEntry($"ASql Filter: {activityFilter}"); var activityTable = FragmentRequestBase.SimpleLoad(SPSActivityClassBaseID, @@ -982,6 +988,7 @@ namespace C4IT.F4SD var roleIds = await ResolveTicketOverviewRoleIdsAsync(sid, null); var filter = BuildTicketOverviewFilterForRoleIds(roleIds, null, queueoption, queues); + filter = await ApplyActivityFiltersAsync(filter, null); if (string.IsNullOrWhiteSpace(filter)) return new List(); @@ -1015,6 +1022,7 @@ namespace C4IT.F4SD var roleIds = await ResolveTicketOverviewRoleIdsAsync(userId, null); var filter = BuildTicketOverviewFilterForRoleIds(roleIds, null, queueoption, queues); + filter = await ApplyActivityFiltersAsync(filter, null); if (string.IsNullOrWhiteSpace(filter)) return new List(); @@ -1044,6 +1052,7 @@ namespace C4IT.F4SD await Task.Delay(0); var filter = BuildTicketOverviewFilterForRoleIds(roleIds, null, queueoption, queues); + filter = await ApplyActivityFiltersAsync(filter, null); return await LoadTicketOverviewEntriesByFilter(filter); } catch (Exception E) @@ -1195,11 +1204,12 @@ namespace C4IT.F4SD { var recipientFilter = $"Recipient.Accounts.T(SPSAccountClassAd).Sid = '{Escape(sid)}'"; filter += $" AND ({recipientFilter})"; - return filter; + return await ApplyActivityFiltersAsync(filter, null); } var roleIds = await ResolveTicketOverviewRoleIdsAsync(sid, null); - return BuildTicketOverviewFilterForRoleIds(roleIds, filter); + filter = BuildTicketOverviewFilterForRoleIds(roleIds, filter); + return await ApplyActivityFiltersAsync(filter, null); } private async Task BuildTicketOverviewFilterAsync( @@ -1214,11 +1224,12 @@ namespace C4IT.F4SD { var recipientFilter = $"Recipient = '{Escape(userId.ToString("D"))}'"; filter += $" AND ({recipientFilter})"; - return filter; + return await ApplyActivityFiltersAsync(filter, null); } var roleIds = await ResolveTicketOverviewRoleIdsAsync(userId, null); - return BuildTicketOverviewFilterForRoleIds(roleIds, filter); + filter = BuildTicketOverviewFilterForRoleIds(roleIds, filter); + return await ApplyActivityFiltersAsync(filter, null); } private string BuildTicketOverviewBaseFilter(int queueoption, List queues) diff --git a/Legacy/F4SDM42WebApi/F4SDM42WebApiController.cs b/Legacy/F4SDM42WebApi/F4SDM42WebApiController.cs index c98cb31..f5f5e71 100644 --- a/Legacy/F4SDM42WebApi/F4SDM42WebApiController.cs +++ b/Legacy/F4SDM42WebApi/F4SDM42WebApiController.cs @@ -138,6 +138,26 @@ namespace C4IT.F4SD ); } + [Route("v2/getTicketList"), HttpPost] + public async Task> getTicketListV2([FromBody] cTicketActivityListRequest request) + { + using (_f4stHelperService.BeginActivityFilterScope(request?.Filters)) + { + if (request?.UserId != Guid.Empty) + return await _f4stHelperService.getTicketListByUser( + request.UserId, + request.Hours, + 0, + new List()) ?? new List(); + + return await _f4stHelperService.getTicketListByUser( + request?.Sid ?? string.Empty, + request?.Hours ?? 0, + 0, + new List()) ?? new List(); + } + } + [Route("getTicketDetails"), HttpGet] public async Task getTicketDetails(Guid objectId) @@ -193,6 +213,29 @@ namespace C4IT.F4SD return await _f4stHelperService.getTicketOverviewCounts(userId, scope, parsedKeys, queueoption, decodedQueues); } + [Route("v2/getTicketOverviewCounts"), HttpPost] + public async Task getTicketOverviewCountsV2( + [FromBody] cTicketOverviewFilterRequest request) + { + using (_f4stHelperService.BeginActivityFilterScope(request?.Filters)) + { + if (request?.UserId != Guid.Empty) + return await _f4stHelperService.getTicketOverviewCounts( + request.UserId, + request.Scope, + request.Keys, + 0, + new List()) ?? new F4SDHelperService.TicketOverviewCountsResult(); + + return await _f4stHelperService.getTicketOverviewCounts( + request?.Sid ?? string.Empty, + request?.Scope ?? "personal", + request?.Keys ?? new List(), + 0, + new List()) ?? new F4SDHelperService.TicketOverviewCountsResult(); + } + } + [Obsolete] [Route("getTicketOverviewCountsByRoles"), HttpPost] public async Task getTicketOverviewCountsByRoles([FromBody] TicketOverviewCountsByRolesRequest request) @@ -269,6 +312,37 @@ namespace C4IT.F4SD return await _f4stHelperService.getTicketOverviewRelations(userId, scope, key, count, queueoption, decodedQueues); } + [Route("v2/getTicketOverviewRelations"), HttpPost] + public async Task> getTicketOverviewRelationsV2( + [FromBody] cTicketOverviewFilterRequest request) + { + using (_f4stHelperService.BeginActivityFilterScope(request?.Filters)) + { + if (request?.UserId != Guid.Empty) + return await _f4stHelperService.getTicketOverviewRelations( + request.UserId, + request.Scope, + request.Key, + request.Count, + 0, + new List()) ?? new List(); + + return await _f4stHelperService.getTicketOverviewRelations( + request?.Sid ?? string.Empty, + request?.Scope ?? "personal", + request?.Key ?? string.Empty, + request?.Count ?? 0, + 0, + new List()) ?? new List(); + } + } + + [Route("v2/getActivityFilterCapabilities"), HttpGet] + public cTicketActivityFilterCapabilities getActivityFilterCapabilitiesV2() + { + return _f4stHelperService.GetActivityFilterCapabilities(); + } + /* [Route("updateActivitySolution/{objectId}"), HttpPost] diff --git a/Shared/ActivityFilterCompiler.cs b/Shared/ActivityFilterCompiler.cs new file mode 100644 index 0000000..4855dc7 --- /dev/null +++ b/Shared/ActivityFilterCompiler.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using C4IT.FASD.Base; + +namespace C4IT.F4SD +{ + internal sealed class ActivityFilterResolution + { + public bool Success { get; set; } + public string Error { get; set; } + public List RoleIds { get; set; } = new List(); + } + + internal sealed class ActivityFilterCompilationResult + { + public bool Success { get; set; } + public string Clause { get; set; } + public string Error { get; set; } + } + + internal static class ActivityFilterCompiler + { + internal const string ProviderName = "Matrix42"; + internal const string AssignmentGroupField = "AssignmentGroup"; + internal const string QueueField = "Queue"; + internal const string WorkspaceField = "Workspace"; + + internal static async Task CompileAsync( + cActivityFilterPolicy policy, + Func> workspaceResolver) + { + var clauses = new List(); + foreach (var filter in policy?.Filters ?? new List()) + { + if (filter == null || !filter.Enabled) + continue; + + var providerSpecific = !string.IsNullOrWhiteSpace(filter.Provider); + if (providerSpecific && !string.Equals(filter.Provider, ProviderName, StringComparison.OrdinalIgnoreCase)) + return Fail($"Unsupported activity filter provider '{filter.Provider}'."); + + var field = filter.Field?.Trim(); + string emptyClause; + string valueClause; + + if (!providerSpecific && string.Equals(field, AssignmentGroupField, StringComparison.OrdinalIgnoreCase)) + { + emptyClause = "RecipientRole IS NULL"; + valueClause = BuildValueClause("RecipientRole.T(SPSSecurityClassRole).ID", "RecipientRole.T(SPSSecurityClassRole).Name", filter.Values); + } + else if (providerSpecific && string.Equals(field, QueueField, StringComparison.OrdinalIgnoreCase)) + { + emptyClause = "Queue IS NULL"; + valueClause = BuildValueClause("Queue.ID", "Queue.Name", filter.Values); + } + else if (providerSpecific && string.Equals(field, WorkspaceField, StringComparison.OrdinalIgnoreCase)) + { + if (workspaceResolver == null) + return Fail("The Matrix42 Workspace filter is not supported by this product version."); + + var resolution = await workspaceResolver(filter); + if (resolution?.Success != true) + return Fail(resolution?.Error ?? "The Matrix42 Workspace filter could not be resolved."); + + emptyClause = "RecipientRole IS NULL"; + valueClause = BuildGuidClause("RecipientRole.T(SPSSecurityClassRole).ID", resolution.RoleIds); + } + else + { + return Fail($"Unsupported activity filter field '{filter.Provider}:{field}'."); + } + + if (filter.EmptyHandling == enumActivityFilterEmptyHandling.only) + { + clauses.Add($"({emptyClause})"); + continue; + } + + if (string.IsNullOrWhiteSpace(valueClause)) + return Fail($"Activity filter '{filter.Provider}:{field}' has no valid values."); + + var matchClause = filter.Match == enumActivityFilterMatch.exclude + ? $"NOT ({valueClause})" + : $"({valueClause})"; + + clauses.Add(filter.EmptyHandling == enumActivityFilterEmptyHandling.include + ? $"(({emptyClause}) OR ({matchClause}))" + : $"((NOT ({emptyClause})) AND ({matchClause}))"); + } + + return new ActivityFilterCompilationResult + { + Success = true, + Clause = string.Join(" AND ", clauses) + }; + } + + private static string BuildValueClause(string idField, string nameField, IEnumerable values) + { + var items = (values ?? Enumerable.Empty()) + .Where(value => value != null) + .ToList(); + var ids = items + .Select(value => value.ID?.Trim()) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Select(Quote) + .ToList(); + var names = items + .Select(value => value.Name?.Trim()) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Select(Quote) + .ToList(); + + var clauses = new List(); + if (ids.Count > 0) + clauses.Add($"{idField} IN ({string.Join(", ", ids)})"); + if (names.Count > 0) + clauses.Add($"{nameField} IN ({string.Join(", ", names)})"); + return string.Join(" OR ", clauses); + } + + private static string BuildGuidClause(string field, IEnumerable values) + { + var ids = (values ?? Enumerable.Empty()) + .Where(value => value != Guid.Empty) + .Distinct() + .Select(value => Quote(value.ToString("D"))) + .ToList(); + return ids.Count == 0 ? null : $"{field} IN ({string.Join(", ", ids)})"; + } + + private static string Quote(string value) + { + return $"'{(value ?? string.Empty).Replace("'", "''")}'"; + } + + private static ActivityFilterCompilationResult Fail(string error) + { + return new ActivityFilterCompilationResult + { + Success = false, + Error = error + }; + } + } +} diff --git a/docs/postman/C4IT-F4SD-M42WebApi.postman_collection.json b/docs/postman/C4IT-F4SD-M42WebApi.postman_collection.json index ef929a5..3d4bf0c 100644 --- a/docs/postman/C4IT-F4SD-M42WebApi.postman_collection.json +++ b/docs/postman/C4IT-F4SD-M42WebApi.postman_collection.json @@ -1022,6 +1022,80 @@ } } ] + }, + { + "name": "06 Activity Filter APIs v2", + "item": [ + { + "name": "get activity filter capabilities", + "request": { + "method": "GET", + "header": [{ "key": "Accept", "value": "application/json" }], + "url": { + "raw": "{{baseUrl}}/api/c4itf4sdwebapi/v2/getActivityFilterCapabilities", + "host": ["{{baseUrl}}"], + "path": ["api", "c4itf4sdwebapi", "v2", "getActivityFilterCapabilities"] + } + } + }, + { + "name": "get filtered ticket list", + "request": { + "method": "POST", + "header": [ + { "key": "Accept", "value": "application/json" }, + { "key": "Content-Type", "value": "application/json" } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"Sid\": \"{{sid}}\",\r\n \"Hours\": {{hours}},\r\n \"Filters\": {\r\n \"Filters\": [\r\n {\r\n \"Provider\": \"Matrix42\",\r\n \"Field\": \"Queue\",\r\n \"Enabled\": true,\r\n \"Match\": \"include\",\r\n \"EmptyHandling\": \"exclude\",\r\n \"Values\": [{ \"Name\": \"{{queueName}}\" }]\r\n }\r\n ]\r\n }\r\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/c4itf4sdwebapi/v2/getTicketList", + "host": ["{{baseUrl}}"], + "path": ["api", "c4itf4sdwebapi", "v2", "getTicketList"] + } + } + }, + { + "name": "get filtered ticket overview counts", + "request": { + "method": "POST", + "header": [ + { "key": "Accept", "value": "application/json" }, + { "key": "Content-Type", "value": "application/json" } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"UserId\": \"{{userId}}\",\r\n \"Scope\": \"{{scope}}\",\r\n \"Keys\": [\"TicketsNew\", \"TicketsActive\"],\r\n \"Filters\": { \"Filters\": [] }\r\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/c4itf4sdwebapi/v2/getTicketOverviewCounts", + "host": ["{{baseUrl}}"], + "path": ["api", "c4itf4sdwebapi", "v2", "getTicketOverviewCounts"] + } + } + }, + { + "name": "get filtered ticket overview relations", + "request": { + "method": "POST", + "header": [ + { "key": "Accept", "value": "application/json" }, + { "key": "Content-Type", "value": "application/json" } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"UserId\": \"{{userId}}\",\r\n \"Scope\": \"{{scope}}\",\r\n \"Key\": \"{{overviewKey}}\",\r\n \"Count\": {{overviewRelationExpectedCount}},\r\n \"Filters\": { \"Filters\": [] }\r\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/c4itf4sdwebapi/v2/getTicketOverviewRelations", + "host": ["{{baseUrl}}"], + "path": ["api", "c4itf4sdwebapi", "v2", "getTicketOverviewRelations"] + } + } + } + ] } ] }