Skip to content

Commit 92da2fe

Browse files
CopilotJerryNixon
andcommitted
Refactor: Minimal implementation for filtering OpenAPI REST methods based on permissions
Co-authored-by: JerryNixon <[email protected]>
1 parent 8aa9195 commit 92da2fe

2 files changed

Lines changed: 139 additions & 304 deletions

File tree

src/Core/Services/OpenAPI/OpenApiDocumentor.cs

Lines changed: 77 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,16 @@ public void CreateDocument(bool doOverrideExistingDocument = false)
139139
};
140140

141141
// Collect all entity tags and their descriptions for the top-level tags array
142+
// Only include entities that have REST enabled and at least one available operation
142143
List<OpenApiTag> globalTags = new();
143144
foreach (KeyValuePair<string, Entity> kvp in runtimeConfig.Entities)
144145
{
145146
Entity entity = kvp.Value;
147+
if (!entity.Rest.Enabled || !HasAnyAvailableOperations(entity))
148+
{
149+
continue;
150+
}
151+
146152
string restPath = entity.Rest?.Path ?? kvp.Key;
147153
globalTags.Add(new OpenApiTag
148154
{
@@ -243,6 +249,12 @@ private OpenApiPaths BuildPaths(RuntimeEntities entities, string defaultDataSour
243249

244250
Dictionary<OperationType, bool> configuredRestOperations = GetConfiguredRestOperations(entity, dbObject);
245251

252+
// Skip entities with no available operations
253+
if (!configuredRestOperations.ContainsValue(true))
254+
{
255+
continue;
256+
}
257+
246258
if (dbObject.SourceType is EntitySourceType.StoredProcedure)
247259
{
248260
Dictionary<OperationType, OpenApiOperation> operations = CreateStoredProcedureOperations(
@@ -251,12 +263,15 @@ private OpenApiPaths BuildPaths(RuntimeEntities entities, string defaultDataSour
251263
configuredRestOperations: configuredRestOperations,
252264
tags: tags);
253265

254-
OpenApiPathItem openApiPathItem = new()
266+
if (operations.Count > 0)
255267
{
256-
Operations = operations
257-
};
268+
OpenApiPathItem openApiPathItem = new()
269+
{
270+
Operations = operations
271+
};
258272

259-
pathsCollection.TryAdd(entityBasePathComponent, openApiPathItem);
273+
pathsCollection.TryAdd(entityBasePathComponent, openApiPathItem);
274+
}
260275
}
261276
else
262277
{
@@ -269,13 +284,12 @@ private OpenApiPaths BuildPaths(RuntimeEntities entities, string defaultDataSour
269284
configuredRestOperations: configuredRestOperations,
270285
tags: tags);
271286

272-
Tuple<string, List<OpenApiParameter>> pkComponents = CreatePrimaryKeyPathComponentAndParameters(entityName, metadataProvider);
273-
string pkPathComponents = pkComponents.Item1;
274-
string fullPathComponent = entityBasePathComponent + pkPathComponents;
275-
276-
// Only add path if there are operations available
277287
if (pkOperations.Count > 0)
278288
{
289+
Tuple<string, List<OpenApiParameter>> pkComponents = CreatePrimaryKeyPathComponentAndParameters(entityName, metadataProvider);
290+
string pkPathComponents = pkComponents.Item1;
291+
string fullPathComponent = entityBasePathComponent + pkPathComponents;
292+
279293
OpenApiPathItem openApiPkPathItem = new()
280294
{
281295
Operations = pkOperations,
@@ -293,7 +307,6 @@ private OpenApiPaths BuildPaths(RuntimeEntities entities, string defaultDataSour
293307
configuredRestOperations: configuredRestOperations,
294308
tags: tags);
295309

296-
// Only add path if there are operations available
297310
if (operations.Count > 0)
298311
{
299312
OpenApiPathItem openApiPathItem = new()
@@ -318,7 +331,7 @@ private OpenApiPaths BuildPaths(RuntimeEntities entities, string defaultDataSour
318331
/// a path containing primary key parameters.
319332
/// TRUE: GET (one), PUT, PATCH, DELETE
320333
/// FALSE: GET (Many), POST</param>
321-
/// <param name="configuredRestOperations">Dictionary indicating which operations are available based on permissions.</param>
334+
/// <param name="configuredRestOperations">Operations available based on permissions.</param>
322335
/// <param name="tags">Tags denoting how the operations should be categorized.
323336
/// Typically one tag value, the entity's REST path.</param>
324337
/// <returns>Collection of operation types and associated definitions.</returns>
@@ -333,10 +346,6 @@ private Dictionary<OperationType, OpenApiOperation> CreateOperations(
333346

334347
if (includePrimaryKeyPathComponent)
335348
{
336-
// The OpenApiResponses dictionary key represents the integer value of the HttpStatusCode,
337-
// which is returned when using Enum.ToString("D").
338-
// The "D" format specified "displays the enumeration entry as an integer value in the shortest representation possible."
339-
// It will only contain $select query parameter to allow the user to specify which fields to return.
340349
if (configuredRestOperations[OperationType.Get])
341350
{
342351
OpenApiOperation getOperation = CreateBaseOperation(description: GETONE_DESCRIPTION, tags: tags);
@@ -345,11 +354,8 @@ private Dictionary<OperationType, OpenApiOperation> CreateOperations(
345354
openApiPathItemOperations.Add(OperationType.Get, getOperation);
346355
}
347356

348-
// PUT and PATCH requests have the same criteria for decided whether a request body is required.
349357
bool requestBodyRequired = IsRequestBodyRequired(sourceDefinition, considerPrimaryKeys: false);
350358

351-
// PUT requests must include the primary key(s) in the URI path and exclude from the request body,
352-
// independent of whether the PK(s) are autogenerated.
353359
if (configuredRestOperations[OperationType.Put])
354360
{
355361
OpenApiOperation putOperation = CreateBaseOperation(description: PUT_DESCRIPTION, tags: tags);
@@ -359,8 +365,6 @@ private Dictionary<OperationType, OpenApiOperation> CreateOperations(
359365
openApiPathItemOperations.Add(OperationType.Put, putOperation);
360366
}
361367

362-
// PATCH requests must include the primary key(s) in the URI path and exclude from the request body,
363-
// independent of whether the PK(s) are autogenerated.
364368
if (configuredRestOperations[OperationType.Patch])
365369
{
366370
OpenApiOperation patchOperation = CreateBaseOperation(description: PATCH_DESCRIPTION, tags: tags);
@@ -381,7 +385,6 @@ private Dictionary<OperationType, OpenApiOperation> CreateOperations(
381385
}
382386
else
383387
{
384-
// Primary key(s) are not included in the URI paths of the GET (all) and POST operations.
385388
if (configuredRestOperations[OperationType.Get])
386389
{
387390
OpenApiOperation getAllOperation = CreateBaseOperation(description: GETALL_DESCRIPTION, tags: tags);
@@ -394,12 +397,7 @@ private Dictionary<OperationType, OpenApiOperation> CreateOperations(
394397

395398
if (configuredRestOperations[OperationType.Post])
396399
{
397-
// The POST body must include fields for primary key(s) which are not autogenerated because a value must be supplied
398-
// for those fields. {entityName}_NoAutoPK represents the schema component which has all fields except for autogenerated primary keys.
399-
// When no autogenerated primary keys exist, then all fields can be included in the POST body which is represented by the schema
400-
// component: {entityName}.
401400
string postBodySchemaReferenceId = DoesSourceContainAutogeneratedPrimaryKey(sourceDefinition) ? $"{entityName}_NoAutoPK" : $"{entityName}";
402-
403401
OpenApiOperation postOperation = CreateBaseOperation(description: POST_DESCRIPTION, tags: tags);
404402
postOperation.RequestBody = CreateOpenApiRequestBodyPayload(postBodySchemaReferenceId, IsRequestBodyRequired(sourceDefinition, considerPrimaryKeys: true));
405403
postOperation.Responses.Add(HttpStatusCode.Created.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.Created), responseObjectSchemaName: entityName));
@@ -650,9 +648,8 @@ private static OpenApiParameter GetOpenApiQueryParameter(string name, string des
650648
/// <summary>
651649
/// Returns collection of OpenAPI OperationTypes and associated flag indicating whether they are enabled
652650
/// for the engine's REST endpoint.
653-
/// For stored procedures, the available REST methods are determined by entity.Rest.Methods.
654-
/// For tables and views, the available REST methods are determined by checking the entity's permissions
655-
/// across all roles. Only operations that are available to at least one role are enabled.
651+
/// Acts as a helper for stored procedures where the runtime config can denote any combination of REST verbs
652+
/// to enable.
656653
/// </summary>
657654
/// <param name="entity">The entity.</param>
658655
/// <param name="dbObject">Database object metadata, indicating entity SourceType</param>
@@ -711,60 +708,72 @@ private static Dictionary<OperationType, bool> GetConfiguredRestOperations(Entit
711708
}
712709
else
713710
{
714-
// For tables and views, determine available operations based on permissions across all roles.
715-
// An operation is available if at least one role has permission for it.
716-
HashSet<EntityActionOperation> availableOperations = GetAvailableOperationsFromPermissions(entity!);
717-
718-
// Map permission operations to REST operations:
719-
// Read -> GET, Create -> POST, Update -> PUT/PATCH, Delete -> DELETE
720-
configuredOperations[OperationType.Get] = availableOperations.Contains(EntityActionOperation.Read);
721-
configuredOperations[OperationType.Post] = availableOperations.Contains(EntityActionOperation.Create);
722-
configuredOperations[OperationType.Put] = availableOperations.Contains(EntityActionOperation.Update);
723-
configuredOperations[OperationType.Patch] = availableOperations.Contains(EntityActionOperation.Update);
724-
configuredOperations[OperationType.Delete] = availableOperations.Contains(EntityActionOperation.Delete);
711+
// For tables/views, determine available operations from permissions (superset of all roles)
712+
if (entity?.Permissions is not null)
713+
{
714+
foreach (EntityPermission permission in entity.Permissions)
715+
{
716+
if (permission.Actions is null)
717+
{
718+
continue;
719+
}
720+
721+
foreach (EntityAction action in permission.Actions)
722+
{
723+
if (action.Action == EntityActionOperation.All)
724+
{
725+
configuredOperations[OperationType.Get] = true;
726+
configuredOperations[OperationType.Post] = true;
727+
configuredOperations[OperationType.Put] = true;
728+
configuredOperations[OperationType.Patch] = true;
729+
configuredOperations[OperationType.Delete] = true;
730+
}
731+
else
732+
{
733+
switch (action.Action)
734+
{
735+
case EntityActionOperation.Read:
736+
configuredOperations[OperationType.Get] = true;
737+
break;
738+
case EntityActionOperation.Create:
739+
configuredOperations[OperationType.Post] = true;
740+
break;
741+
case EntityActionOperation.Update:
742+
configuredOperations[OperationType.Put] = true;
743+
configuredOperations[OperationType.Patch] = true;
744+
break;
745+
case EntityActionOperation.Delete:
746+
configuredOperations[OperationType.Delete] = true;
747+
break;
748+
}
749+
}
750+
}
751+
}
752+
}
725753
}
726754

727755
return configuredOperations;
728756
}
729757

730758
/// <summary>
731-
/// Returns the set of available operations for an entity by examining all permissions across all roles.
732-
/// An operation is considered available if at least one role has permission for it.
733-
/// The wildcard operation (*) expands to Create, Read, Update, and Delete.
759+
/// Checks if an entity has any available REST operations based on its permissions.
734760
/// </summary>
735-
/// <param name="entity">The entity to examine permissions for.</param>
736-
/// <returns>Set of available EntityActionOperations.</returns>
737-
private static HashSet<EntityActionOperation> GetAvailableOperationsFromPermissions(Entity entity)
761+
private static bool HasAnyAvailableOperations(Entity entity)
738762
{
739-
HashSet<EntityActionOperation> availableOperations = new();
740-
741-
if (entity?.Permissions is null)
763+
if (entity?.Permissions is null || entity.Permissions.Length == 0)
742764
{
743-
return availableOperations;
765+
return false;
744766
}
745767

746768
foreach (EntityPermission permission in entity.Permissions)
747769
{
748-
if (permission.Actions is null)
749-
{
750-
continue;
751-
}
752-
753-
foreach (EntityAction action in permission.Actions)
770+
if (permission.Actions?.Length > 0)
754771
{
755-
if (action.Action == EntityActionOperation.All)
756-
{
757-
// Wildcard (*) represents Create, Read, Update, Delete for tables/views
758-
availableOperations.UnionWith(EntityAction.ValidPermissionOperations);
759-
}
760-
else if (EntityAction.ValidPermissionOperations.Contains(action.Action))
761-
{
762-
availableOperations.Add(action.Action);
763-
}
772+
return true;
764773
}
765774
}
766775

767-
return availableOperations;
776+
return false;
768777
}
769778

770779
/// <summary>
@@ -1068,11 +1077,12 @@ private Dictionary<string, OpenApiSchema> CreateComponentSchemas(RuntimeEntities
10681077
string entityName = entityDbMetadataMap.Key;
10691078
DatabaseObject dbObject = entityDbMetadataMap.Value;
10701079

1071-
if (!entities.TryGetValue(entityName, out Entity? entity) || !entity.Rest.Enabled)
1080+
if (!entities.TryGetValue(entityName, out Entity? entity) || !entity.Rest.Enabled || !HasAnyAvailableOperations(entity))
10721081
{
10731082
// Don't create component schemas for:
10741083
// 1. Linking entity: The entity will be null when we are dealing with a linking entity, which is not exposed in the config.
10751084
// 2. Entity for which REST endpoint is disabled.
1085+
// 3. Entity with no available operations based on permissions.
10761086
continue;
10771087
}
10781088

0 commit comments

Comments
 (0)