-
+
@if (Model)
{
diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/docs/FormsIoMappingUsageExamples.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/docs/FormsIoMappingUsageExamples.cs
index 6ad1043167..a49fb5ee78 100644
--- a/applications/Unity.GrantManager/modules/Unity.Reporting/docs/FormsIoMappingUsageExamples.cs
+++ b/applications/Unity.GrantManager/modules/Unity.Reporting/docs/FormsIoMappingUsageExamples.cs
@@ -80,12 +80,10 @@
bool viewExists = await columnsMappingService.ViewExistsAsync("my_form_view");
// Get view data with pagination
-var request = new ViewDataRequest
+var request = new ViewDataRequest
{
Skip = 0,
- Take = 100,
- Filter = "column_name IS NOT NULL", // Optional SQL WHERE clause
- OrderBy = "column_name ASC" // Optional SQL ORDER BY clause
+ Take = 100
};
ViewDataResult data = await columnsMappingService.GetViewDataAsync("my_form_view", request);
diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application.Contracts/Configuration/IReportMappingService.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application.Contracts/Configuration/IReportMappingService.cs
index 6626f00ca8..4a326e30aa 100644
--- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application.Contracts/Configuration/IReportMappingService.cs
+++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application.Contracts/Configuration/IReportMappingService.cs
@@ -101,18 +101,18 @@ public interface IReportMappingService
public Task GenerateViewAsync(Guid correlationId, string correlationProvider, string viewName);
///
- /// Retrieves paginated and filtered data from a generated database view with support for sorting and custom filtering.
+ /// Retrieves paginated data from a generated database view.
///
/// The name of the database view to query for data.
- /// The request parameters containing pagination settings, filtering criteria, and sort ordering.
+ /// The request parameters containing pagination settings.
/// A ViewDataResult containing the queried data rows, total record count, and column information for the requested page.
public Task GetViewDataAsync(string viewName, ViewDataRequest request);
-
+
///
/// Retrieves preview data from a generated database view showing only the top record for preview purposes.
///
/// The name of the database view to query for preview data.
- /// The request parameters for filtering (pagination settings are ignored as only top 1 record is returned).
+ /// The request parameters (pagination settings are ignored as only top 1 record is returned).
/// A ViewDataResult containing the preview data (single top record), count of 1, and column information.
public Task GetViewPreviewDataAsync(string viewName, ViewDataRequest request);
@@ -132,8 +132,8 @@ public interface IReportMappingService
}
///
- /// Represents a request for view data with pagination, filtering, and sorting options.
- /// Provides flexible data retrieval parameters for querying generated reporting views.
+ /// Represents a request for view data with pagination options.
+ /// Provides data retrieval parameters for querying generated reporting views.
///
public class ViewDataRequest
{
@@ -148,18 +148,6 @@ public class ViewDataRequest
/// Defaults to 100 to prevent excessive data transfer while allowing reasonable page sizes.
///
public int Take { get; set; } = 100;
-
- ///
- /// Gets or sets the SQL WHERE clause filter to apply to the view query.
- /// Should be a valid PostgreSQL WHERE clause condition without the "WHERE" keyword.
- ///
- public string? Filter { get; set; }
-
- ///
- /// Gets or sets the SQL ORDER BY clause to apply for result sorting.
- /// Should be a valid PostgreSQL ORDER BY clause without the "ORDER BY" keywords.
- ///
- public string? OrderBy { get; set; }
}
///
diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingService.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingService.cs
index 7524e0cf45..e5a8f62729 100644
--- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingService.cs
+++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingService.cs
@@ -375,12 +375,12 @@ await backgroundJobManager.EnqueueAsync(new GenerateViewBackgroundJobArgs
}
///
- /// Retrieves paginated and filtered data from a generated database view with support for sorting and custom filtering.
+ /// Retrieves paginated data from a generated database view.
/// Validates view existence, normalizes the view name, and delegates to the repository for secure data access
/// with proper pagination controls to handle large datasets efficiently.
///
/// The name of the database view to query for data.
- /// The request parameters containing pagination settings (skip/take), filtering criteria, and sort ordering.
+ /// The request parameters containing pagination settings (skip/take).
/// A ViewDataResult containing the queried data rows, total record count, and column information for the requested page.
///
/// Thrown when:
@@ -411,7 +411,7 @@ public async Task GetViewDataAsync(string viewName, ViewDataRequ
/// Validates view existence and normalizes the view name before querying.
///
/// The name of the database view to query for preview data.
- /// The request parameters for filtering (pagination settings are ignored as only top 1 record is returned).
+ /// The request parameters (pagination settings are ignored as only top 1 record is returned).
/// A ViewDataResult containing the preview data (single top record), count of 1, and column information.
///
/// Thrown when:
diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Domain/Configuration/IReportColumnsMapRepository.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Domain/Configuration/IReportColumnsMapRepository.cs
index d401204e6f..693de8ee57 100644
--- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Domain/Configuration/IReportColumnsMapRepository.cs
+++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Domain/Configuration/IReportColumnsMapRepository.cs
@@ -45,7 +45,7 @@ public interface IReportColumnsMapRepository : IBasicRepository
- /// Retrieves data from a generated view with pagination and filtering.
+ /// Retrieves data from a generated view with pagination.
///
/// The name of the view to query.
/// The request parameters for data retrieval.
diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/EntityFrameworkCore/Repositories/ReportColumnsMapRepository.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/EntityFrameworkCore/Repositories/ReportColumnsMapRepository.cs
index 6d3cb324d5..9c4cd9d1b3 100644
--- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/EntityFrameworkCore/Repositories/ReportColumnsMapRepository.cs
+++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/EntityFrameworkCore/Repositories/ReportColumnsMapRepository.cs
@@ -108,6 +108,15 @@ public async Task GetViewPreviewDataAsync(string viewName, ViewD
// Normalize view name to lowercase for consistency
var normalizedViewName = viewName.Trim().ToLowerInvariant();
+ // SECURITY: Validate the identifier before it is interpolated into SQL below.
+ // ViewExistsAsync alone is not sufficient - it only proves a matching row exists in
+ // pg_views, not that the name is free of characters that would break out of the
+ // quoted identifier it gets embedded in.
+ if (!IsValidPostgreSqlIdentifier(normalizedViewName))
+ {
+ throw new ArgumentException($"Invalid view name format: {viewName}", nameof(viewName));
+ }
+
var dbContext = await GetDbContextAsync();
var connection = dbContext.Database.GetDbConnection();
await dbContext.Database.OpenConnectionAsync();
@@ -132,18 +141,6 @@ ORDER BY a.""CreationTime"" DESC
LIMIT 1
)";
- // Add filtering if provided
- if (!string.IsNullOrWhiteSpace(request.Filter))
- {
- previewQuery += $" AND ({request.Filter})";
- }
-
- // Add ordering if provided
- if (!string.IsNullOrWhiteSpace(request.OrderBy))
- {
- previewQuery += $" ORDER BY {request.OrderBy}";
- }
-
// Execute the preview query
using var dataCommand = connection.CreateCommand();
dataCommand.CommandText = previewQuery;
@@ -180,6 +177,15 @@ public async Task GetViewDataAsync(string viewName, ViewDataRequ
// Normalize view name to lowercase for consistency
var normalizedViewName = viewName.Trim().ToLowerInvariant();
+ // SECURITY: Validate the identifier before it is interpolated into SQL below.
+ // ViewExistsAsync alone is not sufficient - it only proves a matching row exists in
+ // pg_views, not that the name is free of characters that would break out of the
+ // quoted identifier it gets embedded in.
+ if (!IsValidPostgreSqlIdentifier(normalizedViewName))
+ {
+ throw new ArgumentException($"Invalid view name format: {viewName}", nameof(viewName));
+ }
+
var dbContext = await GetDbContextAsync();
var connection = dbContext.Database.GetDbConnection();
await dbContext.Database.OpenConnectionAsync();
@@ -196,14 +202,6 @@ public async Task GetViewDataAsync(string viewName, ViewDataRequ
var baseQuery = $@"SELECT * FROM ""Reporting"".""{normalizedViewName}""";
var countQuery = $@"SELECT COUNT(*) FROM ""Reporting"".""{normalizedViewName}""";
- // Add filtering if provided
- if (!string.IsNullOrWhiteSpace(request.Filter))
- {
- var whereClause = $" WHERE {request.Filter}";
- baseQuery += whereClause;
- countQuery += whereClause;
- }
-
// Get total count
using (var countCommand = connection.CreateCommand())
{
@@ -212,12 +210,6 @@ public async Task GetViewDataAsync(string viewName, ViewDataRequ
result.TotalCount = Convert.ToInt32(countResult);
}
- // Add ordering if provided
- if (!string.IsNullOrWhiteSpace(request.OrderBy))
- {
- baseQuery += $" ORDER BY {request.OrderBy}";
- }
-
// Add pagination
baseQuery += $" OFFSET {request.Skip} LIMIT {request.Take}";
@@ -399,6 +391,14 @@ FROM pg_views
// Grant SELECT permission on each view to the role
foreach (var viewName in viewNames)
{
+ // SECURITY: Validate each identifier read back from pg_views before it is
+ // interpolated into SQL - quoted PostgreSQL identifiers can contain characters
+ // (embedded quotes, semicolons) that would otherwise break out of the quotes below.
+ if (!IsValidPostgreSqlIdentifier(viewName))
+ {
+ throw new ArgumentException($"Invalid view name format: {viewName}", nameof(viewName));
+ }
+
var sql = $"GRANT SELECT ON \"Reporting\".\"{viewName}\" TO \"{role}\"";
await dbContext.Database.ExecuteSqlRawAsync(sql);
}
@@ -551,7 +551,7 @@ FROM information_schema.views
///
/// The identifier to validate
/// True if the identifier is valid, false otherwise
- private static bool IsValidPostgreSqlIdentifier(string identifier)
+ internal static bool IsValidPostgreSqlIdentifier(string identifier)
{
if (string.IsNullOrWhiteSpace(identifier))
return false;
diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Pages/ReportingAdmin/Configuration/Index.cshtml b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Pages/ReportingAdmin/Configuration/Index.cshtml
index 540b18aa36..62e111b10e 100644
--- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Pages/ReportingAdmin/Configuration/Index.cshtml
+++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Pages/ReportingAdmin/Configuration/Index.cshtml
@@ -44,7 +44,8 @@
value="@tenantRole.ViewRole"
data-tenant-id="@tenantRole.TenantId"
data-is-default="@tenantRole.IsDefaultInferred"
- placeholder="@($"{tenantRole.TenantName.ToLowerInvariant()}_readonly")" />
+ placeholder="@($"{tenantRole.TenantName.ToLowerInvariant()}_readonly")"
+ aria-label="@($"View role for {tenantRole.TenantName}")" />
@if (tenantRole.IsDefaultInferred)
{
/// Retrieves preview data from a generated database view showing sample records for interface display.
- /// Fetches sample data from the first application ID found in the view with pagination and filtering support.
+ /// Fetches sample data from the first application ID found in the view with pagination support.
/// Provides preview functionality for users to validate view structure and content before full data access.
///
/// The name of the database view to query for preview data.
/// The number of records to skip for pagination (defaults to 0).
/// The maximum number of records to return (defaults to 100).
- /// Optional SQL WHERE clause filter for restricting preview data.
- /// Optional SQL ORDER BY clause for sorting preview results.
/// An OK result with preview data including sample records and column information, or BadRequest for invalid view names or query parameters.
[HttpGet]
[Route("GetViewPreviewData")]
- public async Task GetViewPreviewData(string viewName, int skip = 0, int take = 100, string? filter = null, string? orderBy = null)
+ public async Task GetViewPreviewData(string viewName, int skip = 0, int take = 100)
{
if (!ModelState.IsValid)
{
@@ -335,9 +333,7 @@ public async Task GetViewPreviewData(string viewName, int skip =
var request = new Unity.Reporting.Configuration.ViewDataRequest
{
Skip = skip,
- Take = take,
- Filter = filter,
- OrderBy = orderBy
+ Take = take
};
var result = await reportMappingService.GetViewPreviewDataAsync(viewName, request);
@@ -438,38 +434,6 @@ public class GenerateColumnNamesRequest
public Dictionary PathColumns { get; set; } = new Dictionary();
}
- ///
- /// Request model for view data retrieval operations with pagination, filtering, and sorting capabilities.
- /// Provides flexible parameters for querying generated reporting views with proper data access controls
- /// and performance optimization through pagination and selective filtering.
- ///
- public class ViewDataRequest
- {
- ///
- /// Gets or sets the number of records to skip for pagination.
- /// Used in combination with Take to implement efficient pagination for large datasets.
- ///
- public int Skip { get; set; }
-
- ///
- /// Gets or sets the maximum number of records to return in the query result.
- /// Provides control over result set size for performance and user interface optimization.
- ///
- public int Take { get; set; }
-
- ///
- /// Gets or sets the optional SQL WHERE clause filter for restricting query results.
- /// Should be a valid PostgreSQL WHERE clause condition without the "WHERE" keyword.
- ///
- public string? Filter { get; set; }
-
- ///
- /// Gets or sets the optional SQL ORDER BY clause for sorting query results.
- /// Should be a valid PostgreSQL ORDER BY clause without the "ORDER BY" keywords.
- ///
- public string? OrderBy { get; set; }
- }
-
///
/// Request model for report mapping deletion operations with configurable view cleanup behavior.
/// Specifies which mapping configuration to delete and whether to remove associated database objects
diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfigurationViewStatus/ReportingConfigurationViewStatusController.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfigurationViewStatus/ReportingConfigurationViewStatusController.cs
index 03f7d34790..160601b5d2 100644
--- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfigurationViewStatus/ReportingConfigurationViewStatusController.cs
+++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfigurationViewStatus/ReportingConfigurationViewStatusController.cs
@@ -75,9 +75,7 @@ public async Task PreviewData(Guid versionId, string provider)
var request = new ViewDataRequest
{
Skip = 0,
- Take = 100, // This will be ignored by the preview method since it uses LIMIT 1 pattern
- OrderBy = null,
- Filter = null
+ Take = 100 // This will be ignored by the preview method since it uses LIMIT 1 pattern
};
var viewData = await reportMappingService.GetViewPreviewDataAsync(reportColumnsMap.ViewName, request);
diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Menu/_MenuItem.cshtml b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Menu/_MenuItem.cshtml
index 7dae29b077..92997eed0b 100644
--- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Menu/_MenuItem.cshtml
+++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Menu/_MenuItem.cshtml
@@ -35,7 +35,8 @@ else
{