Skip to content

Commit ae8166c

Browse files
CopilotJerryNixon
andcommitted
Add /openapi/{role} endpoint for role-specific OpenAPI documents
Co-authored-by: JerryNixon <[email protected]>
1 parent 178910f commit ae8166c

3 files changed

Lines changed: 128 additions & 6 deletions

File tree

src/Core/Services/OpenAPI/IOpenApiDocumentor.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,20 @@ public interface IOpenApiDocumentor
1313
{
1414
/// <summary>
1515
/// Attempts to return the OpenAPI description document, if generated.
16+
/// Returns the superset of all roles' permissions.
1617
/// </summary>
1718
/// <param name="document">String representation of JSON OpenAPI description document.</param>
1819
/// <returns>True (plus string representation of document), when document exists. False, otherwise.</returns>
1920
public bool TryGetDocument([NotNullWhen(true)] out string? document);
2021

22+
/// <summary>
23+
/// Attempts to return a role-specific OpenAPI description document.
24+
/// </summary>
25+
/// <param name="role">The role name to filter permissions.</param>
26+
/// <param name="document">String representation of JSON OpenAPI description document.</param>
27+
/// <returns>True if role exists and document generated. False if role not found.</returns>
28+
public bool TryGetDocumentForRole(string role, [NotNullWhen(true)] out string? document);
29+
2130
/// <summary>
2231
/// Creates an OpenAPI description document using OpenAPI.NET.
2332
/// Document compliant with patches of OpenAPI V3.0 spec 3.0.0 and 3.0.1,

src/Core/Services/OpenAPI/OpenApiDocumentor.cs

Lines changed: 106 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,104 @@ public bool TryGetDocument([NotNullWhen(true)] out string? document)
101101
}
102102
}
103103

104+
/// <summary>
105+
/// Attempts to return a role-specific OpenAPI description document.
106+
/// </summary>
107+
/// <param name="role">The role name to filter permissions (case-insensitive).</param>
108+
/// <param name="document">String representation of JSON OpenAPI description document.</param>
109+
/// <returns>True if role exists and document generated. False if role not found.</returns>
110+
public bool TryGetDocumentForRole(string role, [NotNullWhen(true)] out string? document)
111+
{
112+
document = null;
113+
RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig();
114+
115+
// Check if the role exists in any entity's permissions
116+
bool roleExists = false;
117+
foreach (KeyValuePair<string, Entity> kvp in runtimeConfig.Entities)
118+
{
119+
if (kvp.Value.Permissions?.Any(p => string.Equals(p.Role, role, StringComparison.OrdinalIgnoreCase)) == true)
120+
{
121+
roleExists = true;
122+
break;
123+
}
124+
}
125+
126+
if (!roleExists)
127+
{
128+
return false;
129+
}
130+
131+
try
132+
{
133+
OpenApiDocument? roleDoc = GenerateDocumentForRole(runtimeConfig, role);
134+
if (roleDoc is null)
135+
{
136+
return false;
137+
}
138+
139+
using (StringWriter textWriter = new(CultureInfo.InvariantCulture))
140+
{
141+
OpenApiJsonWriter jsonWriter = new(textWriter);
142+
roleDoc.SerializeAsV3(jsonWriter);
143+
document = textWriter.ToString();
144+
return true;
145+
}
146+
}
147+
catch
148+
{
149+
return false;
150+
}
151+
}
152+
153+
/// <summary>
154+
/// Generates an OpenAPI document filtered for a specific role.
155+
/// </summary>
156+
private OpenApiDocument? GenerateDocumentForRole(RuntimeConfig runtimeConfig, string role)
157+
{
158+
string restEndpointPath = runtimeConfig.RestPath;
159+
string? runtimeBaseRoute = runtimeConfig.Runtime?.BaseRoute;
160+
string url = string.IsNullOrEmpty(runtimeBaseRoute) ? restEndpointPath : runtimeBaseRoute + "/" + restEndpointPath;
161+
162+
OpenApiComponents components = new()
163+
{
164+
Schemas = CreateComponentSchemas(runtimeConfig.Entities, runtimeConfig.DefaultDataSourceName, role)
165+
};
166+
167+
List<OpenApiTag> globalTags = new();
168+
foreach (KeyValuePair<string, Entity> kvp in runtimeConfig.Entities)
169+
{
170+
Entity entity = kvp.Value;
171+
if (!entity.Rest.Enabled || !HasAnyAvailableOperations(entity, role))
172+
{
173+
continue;
174+
}
175+
176+
string restPath = entity.Rest?.Path ?? kvp.Key;
177+
globalTags.Add(new OpenApiTag
178+
{
179+
Name = restPath,
180+
Description = string.IsNullOrWhiteSpace(entity.Description) ? null : entity.Description
181+
});
182+
}
183+
184+
return new OpenApiDocument()
185+
{
186+
Info = new OpenApiInfo
187+
{
188+
Version = ProductInfo.GetProductVersion(),
189+
// Use the role name directly since it was already validated to exist in permissions
190+
Title = $"{DOCUMENTOR_UI_TITLE} - {role}"
191+
},
192+
Servers = new List<OpenApiServer>
193+
{
194+
new() { Url = url }
195+
},
196+
Paths = BuildPaths(runtimeConfig.Entities, runtimeConfig.DefaultDataSourceName, role),
197+
Components = components,
198+
Tags = globalTags
199+
};
200+
}
201+
104202
/// <summary>
105203
/// Creates an OpenAPI description document using OpenAPI.NET.
106204
/// Document compliant with patches of OpenAPI V3.0 spec 3.0.0 and 3.0.1,
@@ -198,8 +296,9 @@ public void CreateDocument(bool doOverrideExistingDocument = false)
198296
/// A path with no primary key nor parameter representing the primary key value:
199297
/// "/EntityName"
200298
/// </example>
299+
/// <param name="role">Optional role to filter permissions. If null, returns superset of all roles.</param>
201300
/// <returns>All possible paths in the DAB engine's REST API endpoint.</returns>
202-
private OpenApiPaths BuildPaths(RuntimeEntities entities, string defaultDataSourceName)
301+
private OpenApiPaths BuildPaths(RuntimeEntities entities, string defaultDataSourceName, string? role = null)
203302
{
204303
OpenApiPaths pathsCollection = new();
205304

@@ -247,7 +346,7 @@ private OpenApiPaths BuildPaths(RuntimeEntities entities, string defaultDataSour
247346
openApiTag
248347
};
249348

250-
Dictionary<OperationType, bool> configuredRestOperations = GetConfiguredRestOperations(entity, dbObject);
349+
Dictionary<OperationType, bool> configuredRestOperations = GetConfiguredRestOperations(entity, dbObject, role);
251350

252351
// Skip entities with no available operations
253352
if (!configuredRestOperations.ContainsValue(true))
@@ -1154,8 +1253,9 @@ private static OpenApiMediaType CreateResponseContainer(string responseObjectSch
11541253
/// 3) {EntityName}_NoPK -> No primary keys present in schema, used for POST requests where PK is autogenerated and GET (all).
11551254
/// Schema objects can be referenced elsewhere in the OpenAPI document with the intent to reduce document verbosity.
11561255
/// </summary>
1256+
/// <param name="role">Optional role to filter permissions. If null, returns superset of all roles.</param>
11571257
/// <returns>Collection of schemas for entities defined in the runtime configuration.</returns>
1158-
private Dictionary<string, OpenApiSchema> CreateComponentSchemas(RuntimeEntities entities, string defaultDataSourceName)
1258+
private Dictionary<string, OpenApiSchema> CreateComponentSchemas(RuntimeEntities entities, string defaultDataSourceName, string? role = null)
11591259
{
11601260
Dictionary<string, OpenApiSchema> schemas = new();
11611261
// for rest scenario we need the default datasource name.
@@ -1168,7 +1268,7 @@ private Dictionary<string, OpenApiSchema> CreateComponentSchemas(RuntimeEntities
11681268
string entityName = entityDbMetadataMap.Key;
11691269
DatabaseObject dbObject = entityDbMetadataMap.Value;
11701270

1171-
if (!entities.TryGetValue(entityName, out Entity? entity) || !entity.Rest.Enabled || !HasAnyAvailableOperations(entity))
1271+
if (!entities.TryGetValue(entityName, out Entity? entity) || !entity.Rest.Enabled || !HasAnyAvailableOperations(entity, role))
11721272
{
11731273
// Don't create component schemas for:
11741274
// 1. Linking entity: The entity will be null when we are dealing with a linking entity, which is not exposed in the config.
@@ -1180,8 +1280,8 @@ private Dictionary<string, OpenApiSchema> CreateComponentSchemas(RuntimeEntities
11801280
SourceDefinition sourceDefinition = metadataProvider.GetSourceDefinition(entityName);
11811281
HashSet<string> exposedColumnNames = GetExposedColumnNames(entityName, sourceDefinition.Columns.Keys.ToList(), metadataProvider);
11821282

1183-
// Filter fields based on the superset of permissions across all roles
1184-
exposedColumnNames = FilterFieldsByPermissions(entity, exposedColumnNames);
1283+
// Filter fields based on the superset of permissions across all roles (or specific role)
1284+
exposedColumnNames = FilterFieldsByPermissions(entity, exposedColumnNames, role);
11851285

11861286
HashSet<string> nonAutoGeneratedPKColumnNames = new();
11871287

src/Service/Controllers/RestController.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ private async Task<IActionResult> HandleOperation(
222222
string routeAfterPathBase = _restService.GetRouteAfterPathBase(route);
223223

224224
// Explicitly handle OpenAPI description document retrieval requests.
225+
// Supports /openapi (superset of all roles) and /openapi/{role} (role-specific)
225226
if (string.Equals(routeAfterPathBase, OpenApiDocumentor.OPENAPI_ROUTE, StringComparison.OrdinalIgnoreCase))
226227
{
227228
if (_openApiDocumentor.TryGetDocument(out string? document))
@@ -232,6 +233,18 @@ private async Task<IActionResult> HandleOperation(
232233
return NotFound();
233234
}
234235

236+
// Handle /openapi/{role} route for role-specific OpenAPI documents
237+
if (routeAfterPathBase.StartsWith(OpenApiDocumentor.OPENAPI_ROUTE + "/", StringComparison.OrdinalIgnoreCase))
238+
{
239+
string role = Uri.UnescapeDataString(routeAfterPathBase.Substring(OpenApiDocumentor.OPENAPI_ROUTE.Length + 1));
240+
if (!string.IsNullOrEmpty(role) && _openApiDocumentor.TryGetDocumentForRole(role, out string? roleDocument))
241+
{
242+
return Content(roleDocument, MediaTypeNames.Application.Json);
243+
}
244+
245+
return NotFound();
246+
}
247+
235248
(string entityName, string primaryKeyRoute) = _restService.GetEntityNameAndPrimaryKeyRouteFromRoute(routeAfterPathBase);
236249

237250
// This activity tracks the query execution. This will create a new activity nested under the REST request activity.

0 commit comments

Comments
 (0)