Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Unity.Reporting.Configuration
Expand All @@ -12,14 +11,16 @@ namespace Unity.Reporting.Configuration
public interface ITenantViewRoleAppService
{
/// <summary>
/// Retrieves the view role configuration for all tenants in the system.
/// Returns both explicitly configured roles and default inferred roles based on tenant naming patterns.
/// Retrieves the view role configuration for a specific tenant.
/// Returns either the explicitly configured role or a default inferred role based on the tenant's name.
/// </summary>
/// <param name="tenantId">The unique identifier of the tenant to retrieve the view role configuration for.</param>
/// <returns>
/// A list of <see cref="TenantViewRoleDto"/> objects containing the tenant information and their associated view roles.
/// Default roles follow the pattern {tenantname}_readonly when not explicitly configured.
/// A <see cref="TenantViewRoleDto"/> containing the tenant information and its associated view role.
/// Defaults to {LicencePlate}_readonly when a licence plate exists, falling back to {tenantname}_readonly for legacy tenants.
/// </returns>
Task<List<TenantViewRoleDto>> GetAllAsync();
/// <exception cref="InvalidOperationException">Thrown when the specified tenant is not found.</exception>
Task<TenantViewRoleDto> GetAsync(Guid tenantId);

/// <summary>
/// Updates the view role configuration for a specific tenant.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,45 @@ public class TenantViewRoleDto

/// <summary>
/// Gets or sets the database role name that will be granted SELECT permissions on reporting views for this tenant.
/// Defaults to {tenantname}_readonly if not explicitly configured.
/// Defaults to the tenant's {LicencePlate}_readonly role when a license plate is on record
/// (the role automatically provisioned for new tenants), falling back to the legacy
/// {tenantname}_readonly pattern for older tenants with no license plate, unless a role has
/// been explicitly saved for this tenant.
/// </summary>
public string ViewRole { get; set; } = string.Empty;

/// <summary>
/// Gets or sets a value indicating whether the current ViewRole value is an inferred default
/// that has not been explicitly saved to the database. When true, indicates the role name
/// follows the default pattern (e.g., {tenantname}_readonly) and requires explicit saving
/// to persist as a tenant-specific setting.
/// follows a default pattern (see <see cref="ViewRole"/>) and requires explicit saving to
/// persist as a tenant-specific setting.
/// </summary>
public bool IsDefaultInferred { get; set; }

/// <summary>
/// Gets or sets the tenant's license plate (its database name, e.g. "T_ABC123"), used since
/// tenant provisioning to name the tenant's two automatically-created database roles: the
/// license plate itself (read-write) and {LicencePlate}_readonly. Null for tenants that
/// predate this convention.
/// </summary>
public string? LicencePlate { get; set; }

/// <summary>
/// Gets or sets the {LicencePlate}_readonly role name expected to exist for this tenant. Null
/// when <see cref="LicencePlate"/> is null.
/// </summary>
public string? ExpectedReadOnlyRole { get; set; }

/// <summary>
/// Gets or sets whether <see cref="ExpectedReadOnlyRole"/> actually exists as a role in the
/// tenant's database, checked live. Always false when <see cref="LicencePlate"/> is null.
/// </summary>
public bool ReadOnlyRoleExists { get; set; }

/// <summary>
/// Gets or sets whether the tenant's main (read-write) role - named after
/// <see cref="LicencePlate"/> - actually exists in the tenant's database, checked live.
/// Always false when <see cref="LicencePlate"/> is null.
/// </summary>
public bool MainRoleExists { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Unity.Reporting.BackgroundJobs;
using Unity.Reporting.Domain.Configuration;
using Unity.Reporting.Settings;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.BackgroundJobs;
using Volo.Abp.MultiTenancy;
Expand All @@ -31,37 +32,68 @@ public class TenantViewRoleAppService(
IReportColumnsMapRepository reportColumnsMapRepository,
ICurrentTenant currentTenant) : ApplicationService, ITenantViewRoleAppService
{
private const string LicencePlateExtraPropertyKey = "LicencePlate";

/// <summary>
/// Retrieves all tenant view role configurations.
/// Returns a list of all tenants with their current view role assignments, defaulting to {tenantname}_readonly if not configured.
/// Retrieves the view role configuration for a specific tenant, along with the live existence
/// state of the two roles automatically provisioned for the tenant's license plate (its
/// read-write role and {LicencePlate}_readonly).
/// </summary>
public async Task<List<TenantViewRoleDto>> GetAllAsync()
public async Task<TenantViewRoleDto> GetAsync(Guid tenantId)
{
var tenants = await tenantRepository.GetListAsync();
var tenantViewRoles = new List<TenantViewRoleDto>();
var tenant = await tenantRepository.GetAsync(tenantId);

var licencePlate = tenant.ExtraProperties.TryGetValue(LicencePlateExtraPropertyKey, out var lp)
? lp?.ToString()
: null;
var expectedReadOnlyRole = string.IsNullOrWhiteSpace(licencePlate) ? null : $"{licencePlate}_readonly";

// An explicitly saved role always wins (covers legacy tenants whose role doesn't follow
// either naming convention). Otherwise prefer the license-plate readonly role - the one
// actually provisioned for new tenants - falling back to the legacy {tenantname}_readonly
// pattern only when there's no license plate on record at all.
var savedViewRole = await settingManager.GetOrNullAsync(ReportingSettings.TenantViewRole, "T", tenant.Id.ToString());

foreach (var tenant in tenants)
string viewRole;
bool isDefaultInferred;
if (!string.IsNullOrEmpty(savedViewRole))
{
viewRole = savedViewRole;
isDefaultInferred = false;
}
else if (expectedReadOnlyRole != null)
{
viewRole = expectedReadOnlyRole;
isDefaultInferred = true;
}
else
{
// Get tenant-specific setting first, fallback to default pattern
var viewRole = await settingManager.GetOrNullAsync(ReportingSettings.TenantViewRole, "T", tenant.Id.ToString());
viewRole = $"{tenant.Name.ToLowerInvariant()}_readonly";
isDefaultInferred = true;
}

bool isDefaultInferred = false;
if (string.IsNullOrEmpty(viewRole))
bool readOnlyRoleExists = false;
bool mainRoleExists = false;
if (!string.IsNullOrWhiteSpace(licencePlate))
{
using (currentTenant.Change(tenantId))
{
viewRole = $"{tenant.Name.ToLowerInvariant()}_readonly";
isDefaultInferred = true;
readOnlyRoleExists = await reportColumnsMapRepository.RoleExistsAsync(expectedReadOnlyRole!);
mainRoleExists = await reportColumnsMapRepository.RoleExistsAsync(licencePlate);
}

tenantViewRoles.Add(new TenantViewRoleDto
{
TenantId = tenant.Id,
TenantName = tenant.Name,
ViewRole = viewRole,
IsDefaultInferred = isDefaultInferred
});
}

return tenantViewRoles;
return new TenantViewRoleDto
{
TenantId = tenant.Id,
TenantName = tenant.Name,
ViewRole = viewRole,
IsDefaultInferred = isDefaultInferred,
LicencePlate = licencePlate,
ExpectedReadOnlyRole = expectedReadOnlyRole,
ReadOnlyRoleExists = readOnlyRoleExists,
MainRoleExists = mainRoleExists
};
}

/// <summary>
Expand All @@ -72,6 +104,8 @@ public async Task<TenantViewRoleDto> UpdateAsync(Guid tenantId, UpdateTenantView
{
var tenant = await tenantRepository.GetAsync(tenantId);

await EnsureRoleExistsAsync(tenantId, input.ViewRole);

await settingManager.SetAsync(ReportingSettings.TenantViewRole, input.ViewRole, "T", tenantId.ToString());

return new TenantViewRoleDto
Expand All @@ -91,6 +125,17 @@ public async Task AssignRoleToViewsAsync(Guid tenantId)
{
Logger.LogInformation("Starting role assignment for tenant: {TenantId}", tenantId);

var role = await settingManager.GetOrNullAsync(ReportingSettings.TenantViewRole, "T", tenantId.ToString());
if (string.IsNullOrWhiteSpace(role))
{
throw new UserFriendlyException("No view role is configured for this tenant yet. Save a role first.");
}

// Fail fast here rather than relying solely on AssignViewRoleBackgroundJob's own check -
// that check only logs a warning and silently no-ops, so without this the queue call
// always reports success even for a role that doesn't exist in the tenant's database.
await EnsureRoleExistsAsync(tenantId, role);

var jobArgs = new AssignViewRoleBackgroundJobArgs
{
TenantId = tenantId
Expand All @@ -100,6 +145,22 @@ public async Task AssignRoleToViewsAsync(Guid tenantId)
Logger.LogInformation("Queued role assignment job for tenant: {TenantId}", tenantId);
}

/// <summary>
/// Throws a <see cref="UserFriendlyException"/> if the given role does not exist as a real
/// PostgreSQL role in the tenant's own database.
/// </summary>
private async Task EnsureRoleExistsAsync(Guid tenantId, string role)
{
using (currentTenant.Change(tenantId))
{
if (!await reportColumnsMapRepository.RoleExistsAsync(role))
{
throw new UserFriendlyException(
$"Role '{role}' does not exist in this tenant's database. Create the role first, or choose an existing one from View DB Info.");
}
}
}

/// <summary>
/// Retrieves database information for a specific tenant, including available roles and reporting views.
/// This method queries the tenant's database to return a comprehensive list of database roles
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Loading