77using Microsoft . AspNetCore . Mvc ;
88using Microsoft . Extensions . Logging ;
99using SharpCoreDB . Server . Core . Tenancy ;
10+ using SharpCoreDB . Server . Core . Security ;
1011using System . Text . Json ;
1112
1213namespace 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