@@ -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
0 commit comments