Skip to content

Commit 904822d

Browse files
author
MPCoreDeveloper
committed
issues closed
1 parent 84c699f commit 904822d

38 files changed

+3832
-138
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Standardize all documentation/version labels to v1.7.0 ("V 1.70").
99
- Continue implementation until the scoped roadmap work is finished without pausing for confirmation.
1010
- When roadmap issues are completed, explicitly mark the corresponding issue draft documents as resolved/completed so the status is visibly updated in the repository.
11+
- Validate issue status claims against current open issues and actively work through unresolved issues, ensuring they are not considered done prematurely.
1112

1213
## Testing Policy
1314
- All test projects in SharpCoreDB must use **xUnit v3** (`xunit.v3` NuGet package, currently 3.2.2+). **Never** use `xunit` v2 (package id `xunit`). The old v2 package is incompatible with .NET 10 / C# 14.

Examples/Web/Orchardcore/SharpCoreDb.Orchardcore/SharpCoreDb.Orchardcore.csproj

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@
1616

1717
<!-- OrchardCore packages only restored when NOT in CI -->
1818
<ItemGroup Condition="'$(CI)' != 'true' AND '$(GITHUB_ACTIONS)' != 'true'">
19-
<!-- Requires OrchardCore preview feed: https://nuget.cloudsmith.io/orchardcore/preview/v3/index.json -->
20-
<PackageReference Include="OrchardCore.Application.Cms.Targets" Version="3.0.0-preview-18884" />
19+
<!-- Stable OrchardCore package -->
20+
<PackageReference Include="OrchardCore.Application.Cms.Targets" Version="2.2.1" />
2121
</ItemGroup>
2222
<ItemGroup>
23-
<PackageReference Include="HtmlSanitizer" Version="9.1.893-beta" />
23+
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
2424
<PackageReference Include="MimeKit" Version="4.15.1" />
25+
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.4.0" />
26+
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.5" />
27+
<PackageReference Include="AWSSDK.Core" Version="4.0.3.28" />
2528
</ItemGroup>
2629

2730
<ItemGroup Condition="'$(UseLocalSharpCoreDbSources)' == 'true'">

docs/PROJECT_STATUS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,11 @@ Detailed findings are documented in:
3737

3838
- Canonical docs entry points: `README.md`, `docs/INDEX.md`, `docs/README.md`
3939
- Obsolete/superseded phase-planning artifacts are removed during documentation maintenance.
40+
41+
## Roadmap Issue Closure Tracking
42+
43+
-`#125` Enforce database grants in Connect and session creation — completed and closed.
44+
-`#124` Per-database grants model for tenant isolation — completed and closed.
45+
-`#123` DatabaseRegistry runtime attach/detach APIs — completed and closed.
46+
-`#122` Runtime tenant database provisioning APIs (gRPC + REST) — completed and closed.
47+
-`#121` Tenant catalog in master database for SaaS lifecycle metadata — completed and closed.

src/SharpCoreDB.AppHost/SharpCoreDB.AppHost.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
</ItemGroup>
1616

1717
<ItemGroup>
18-
<PackageReference Include="Aspire.Hosting.AppHost" Version="13.2.1" />
18+
<PackageReference Include="Aspire.Hosting.AppHost" Version="13.2.2" />
1919
<PackageReference Include="KubernetesClient" Version="19.0.2" />
2020
</ItemGroup>
2121

src/SharpCoreDB.Server.Core/Api/TenantsController.cs

Lines changed: 172 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.AspNetCore.Mvc;
88
using Microsoft.Extensions.Logging;
99
using SharpCoreDB.Server.Core.Tenancy;
10+
using SharpCoreDB.Server.Core.Security;
1011
using System.Text.Json;
1112

1213
namespace SharpCoreDB.Server.Core.Api;
@@ -27,6 +28,7 @@ public sealed class TenantsController(
2728
TenantBackupRestoreService backupRestoreService,
2829
TenantMigrationPlanningService migrationPlanningService,
2930
TenantCatalogRepository catalogRepository,
31+
DatabaseGrantsRepository databaseGrantsRepository,
3032
ILogger<TenantsController> logger) : ControllerBase
3133
{
3234
/// <summary>
@@ -92,6 +94,22 @@ public async Task<ActionResult<CreateTenantApiResponse>> CreateTenant(
9294
}
9395
}
9496

97+
/// <summary>
98+
/// Creates a tenant database at runtime.
99+
/// POST /api/v1/tenants/databases
100+
/// </summary>
101+
[HttpPost("databases")]
102+
[ProducesResponseType(201)]
103+
[ProducesResponseType(400)]
104+
[ProducesResponseType(409)]
105+
[ProducesResponseType(500)]
106+
public Task<ActionResult<CreateTenantApiResponse>> CreateTenantDatabase(
107+
[FromBody] CreateTenantApiRequest request,
108+
CancellationToken cancellationToken = default)
109+
{
110+
return CreateTenant(request, cancellationToken);
111+
}
112+
95113
/// <summary>
96114
/// Rotates a tenant database encryption key reference.
97115
/// POST /api/v1/tenants/{tenantId}/databases/{databaseName}/encryption/rotate
@@ -338,6 +356,21 @@ public async Task<ActionResult<DeleteTenantApiResponse>> DeleteTenant(
338356
}
339357
}
340358

359+
/// <summary>
360+
/// Deletes a tenant database at runtime.
361+
/// DELETE /api/v1/tenants/databases/{tenantId}
362+
/// </summary>
363+
[HttpDelete("databases/{tenantId}")]
364+
[ProducesResponseType(202)]
365+
[ProducesResponseType(404)]
366+
public Task<ActionResult<DeleteTenantApiResponse>> DeleteTenantDatabase(
367+
string tenantId,
368+
[FromHeader(Name = "Idempotency-Key")] string? idempotencyKey = null,
369+
CancellationToken cancellationToken = default)
370+
{
371+
return DeleteTenant(tenantId, idempotencyKey, cancellationToken);
372+
}
373+
341374
/// <summary>
342375
/// Gets provisioning operation status.
343376
/// GET /api/v1/tenants/operations/{operationId}
@@ -378,6 +411,18 @@ public ActionResult<GetOperationStatusApiResponse> GetOperationStatus(string ope
378411
}
379412
}
380413

414+
/// <summary>
415+
/// Gets tenant provisioning status.
416+
/// GET /api/v1/tenants/provisioning/{operationId}
417+
/// </summary>
418+
[HttpGet("provisioning/{operationId}")]
419+
[ProducesResponseType(200)]
420+
[ProducesResponseType(404)]
421+
public ActionResult<GetOperationStatusApiResponse> GetTenantProvisioningStatus(string operationId)
422+
{
423+
return GetOperationStatus(operationId);
424+
}
425+
381426
/// <summary>
382427
/// Gets effective quota policy for a tenant.
383428
/// GET /api/v1/tenants/{tenantId}/quotas
@@ -609,11 +654,137 @@ public async Task<ActionResult<TenantMigrationPlanApiResponse>> CreateTenantMigr
609654
});
610655
}
611656

612-
// ...existing code...
657+
/// <summary>
658+
/// Grants permissions for a principal on a tenant database.
659+
/// POST /api/v1/tenants/{tenantId}/databases/{databaseName}/grants
660+
/// </summary>
661+
[HttpPost("{tenantId}/databases/{databaseName}/grants")]
662+
[ProducesResponseType(200)]
663+
[ProducesResponseType(400)]
664+
[ProducesResponseType(404)]
665+
public async Task<ActionResult<DatabaseGrantApiResponse>> GrantDatabaseAccess(
666+
string tenantId,
667+
string databaseName,
668+
[FromBody] GrantDatabaseAccessApiRequest request,
669+
CancellationToken cancellationToken = default)
670+
{
671+
if (string.IsNullOrWhiteSpace(request.Principal) || string.IsNullOrWhiteSpace(request.Permission))
672+
{
673+
return BadRequest(new ErrorResponse { Message = "Principal and Permission are required" });
674+
}
675+
676+
var tenant = await catalogRepository.GetTenantByIdAsync(tenantId, cancellationToken);
677+
if (tenant is null)
678+
{
679+
return NotFound(new ErrorResponse { Message = "Tenant not found" });
680+
}
681+
682+
if (!Enum.TryParse<DatabasePermission>(request.Permission, ignoreCase: true, out var permission))
683+
{
684+
return BadRequest(new ErrorResponse { Message = "Invalid database permission" });
685+
}
686+
687+
var grant = await databaseGrantsRepository.GrantPermissionAsync(
688+
tenantId,
689+
databaseName,
690+
request.Principal,
691+
permission,
692+
request.IsGrantable,
693+
cancellationToken);
694+
695+
return Ok(new DatabaseGrantApiResponse
696+
{
697+
GrantId = grant.GrantId,
698+
TenantId = grant.TenantId,
699+
DatabaseName = grant.DatabaseName,
700+
Principal = grant.Principal,
701+
Permission = grant.Permission.ToString(),
702+
IsGrantable = grant.IsGrantable,
703+
CreatedAt = grant.CreatedAt,
704+
ExpiresAt = grant.ExpiresAt,
705+
});
706+
}
707+
708+
/// <summary>
709+
/// Revokes a database grant.
710+
/// DELETE /api/v1/tenants/grants/{grantId}
711+
/// </summary>
712+
[HttpDelete("grants/{grantId}")]
713+
[ProducesResponseType(204)]
714+
[ProducesResponseType(400)]
715+
public async Task<IActionResult> RevokeDatabaseGrant(
716+
string grantId,
717+
CancellationToken cancellationToken = default)
718+
{
719+
if (string.IsNullOrWhiteSpace(grantId))
720+
{
721+
return BadRequest(new ErrorResponse { Message = "GrantId is required" });
722+
}
723+
724+
await databaseGrantsRepository.RevokeGrantAsync(grantId, cancellationToken);
725+
return NoContent();
726+
}
727+
728+
/// <summary>
729+
/// Lists active database grants for a principal.
730+
/// GET /api/v1/tenants/grants?principal={principal}
731+
/// </summary>
732+
[HttpGet("grants")]
733+
[ProducesResponseType(200)]
734+
[ProducesResponseType(400)]
735+
public async Task<ActionResult<List<DatabaseGrantApiResponse>>> ListDatabaseGrants(
736+
[FromQuery] string principal,
737+
CancellationToken cancellationToken = default)
738+
{
739+
if (string.IsNullOrWhiteSpace(principal))
740+
{
741+
return BadRequest(new ErrorResponse { Message = "Principal is required" });
742+
}
743+
744+
var grants = await databaseGrantsRepository.GetPrincipalGrantsAsync(principal, cancellationToken);
745+
var response = grants.Select(static grant => new DatabaseGrantApiResponse
746+
{
747+
GrantId = grant.GrantId,
748+
TenantId = grant.TenantId,
749+
DatabaseName = grant.DatabaseName,
750+
Principal = grant.Principal,
751+
Permission = grant.Permission.ToString(),
752+
IsGrantable = grant.IsGrantable,
753+
CreatedAt = grant.CreatedAt,
754+
ExpiresAt = grant.ExpiresAt,
755+
}).ToList();
756+
757+
return Ok(response);
758+
}
613759
}
614760

615761
// API Request/Response DTOs
616762

763+
/// <summary>
764+
/// Request to grant principal access to a database.
765+
/// </summary>
766+
public sealed class GrantDatabaseAccessApiRequest
767+
{
768+
public required string Principal { get; init; }
769+
public required string Permission { get; init; }
770+
public bool IsGrantable { get; init; }
771+
}
772+
773+
/// <summary>
774+
/// Response model representing a database grant.
775+
/// </summary>
776+
public sealed class DatabaseGrantApiResponse
777+
{
778+
public required string GrantId { get; init; }
779+
public required string TenantId { get; init; }
780+
public required string DatabaseName { get; init; }
781+
public required string Principal { get; init; }
782+
public required string Permission { get; init; }
783+
public required bool IsGrantable { get; init; }
784+
public required DateTime CreatedAt { get; init; }
785+
public required DateTime ExpiresAt { get; init; }
786+
}
787+
617788
/// <summary>
618789
/// Request to create a new tenant.
619790
/// </summary>

0 commit comments

Comments
 (0)