Skip to content

Commit 2c344e1

Browse files
Merge pull request #2397 from solliancenet/cj-extend-role-assignments
(0.9.7-beta135.1) Add new role definitions for agent and vectorization pipelines
2 parents 92cd391 + dad8b15 commit 2c344e1

File tree

24 files changed

+456
-122
lines changed

24 files changed

+456
-122
lines changed

src/dotnet/AIModel/ResourceProviders/AIModelResourceProviderService.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,13 @@ protected override async Task<object> GetResourcesAsync(
8888
};
8989

9090
/// <inheritdoc/>
91-
protected override async Task<object> UpsertResourceAsync(ResourcePath resourcePath, string? serializedResource, ResourceProviderFormFile? formFile, UnifiedUserIdentity userIdentity) =>
91+
protected override async Task<object> UpsertResourceAsync(
92+
ResourcePath resourcePath,
93+
string? serializedResource,
94+
ResourceProviderFormFile? formFile,
95+
ResourcePathAuthorizationResult authorizationResult,
96+
UnifiedUserIdentity userIdentity) =>
97+
9298
resourcePath.MainResourceTypeName switch
9399
{
94100
AIModelResourceTypeNames.AIModels => await UpdateAIModel(resourcePath, serializedResource!, userIdentity),

src/dotnet/Agent/ResourceProviders/AgentResourceProviderService.cs

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
using Microsoft.Extensions.DependencyInjection;
2929
using Microsoft.Extensions.Logging;
3030
using Microsoft.Extensions.Options;
31+
using Microsoft.Graph.Models;
3132
using System.Data;
3233
using System.Text;
3334
using System.Text.Json;
@@ -122,12 +123,18 @@ protected override async Task<object> GetResourcesAsync(
122123
};
123124

124125
/// <inheritdoc/>
125-
protected override async Task<object> UpsertResourceAsync(ResourcePath resourcePath, string? serializedResource, ResourceProviderFormFile? formFile, UnifiedUserIdentity userIdentity) =>
126+
protected override async Task<object> UpsertResourceAsync(
127+
ResourcePath resourcePath,
128+
string? serializedResource,
129+
ResourceProviderFormFile? formFile,
130+
ResourcePathAuthorizationResult authorizationResult,
131+
UnifiedUserIdentity userIdentity) =>
132+
126133
resourcePath.ResourceTypeName switch
127134
{
128-
AgentResourceTypeNames.Agents => await UpdateAgent(resourcePath, serializedResource!, userIdentity),
135+
AgentResourceTypeNames.Agents => await UpdateAgent(resourcePath, serializedResource!, authorizationResult, userIdentity),
129136
AgentResourceTypeNames.AgentFiles => await UpdateAgentFile(resourcePath, formFile!, userIdentity),
130-
AgentResourceTypeNames.AgentAccessTokens => await UpdateAgentAccessToken(resourcePath, serializedResource!),
137+
AgentResourceTypeNames.AgentAccessTokens => await UpdateAgentAccessToken(resourcePath, serializedResource!, authorizationResult, userIdentity),
131138
AgentResourceTypeNames.AgentFileToolAssociations => await UpdateFileToolAssociations(resourcePath, serializedResource!, userIdentity),
132139
_ => throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeName} is not supported by the {_name} resource provider.",
133140
StatusCodes.Status400BadRequest)
@@ -220,14 +227,30 @@ protected override async Task<T> GetResourceAsyncInternal<T>(
220227

221228
#region Resource management
222229

223-
private async Task<ResourceProviderUpsertResult> UpdateAgent(ResourcePath resourcePath, string serializedAgent, UnifiedUserIdentity userIdentity)
230+
private async Task<ResourceProviderUpsertResult> UpdateAgent(
231+
ResourcePath resourcePath,
232+
string serializedAgent,
233+
ResourcePathAuthorizationResult authorizationResult,
234+
UnifiedUserIdentity userIdentity)
224235
{
225236
var agent = JsonSerializer.Deserialize<AgentBase>(serializedAgent)
226237
?? throw new ResourceProviderException("The object definition is invalid.",
227238
StatusCodes.Status400BadRequest);
228239

229240
var existingAgentReference = await _resourceReferenceStore!.GetResourceReference(agent.Name);
230241

242+
if (existingAgentReference is not null
243+
&& !authorizationResult.Authorized)
244+
{
245+
// The resource already exists and the user is not authorized to update it.
246+
// Irrespective of whether the user has the required role or not, we need to throw an exception in the case of existing resources.
247+
// The required role only allows the user to create a new resource.
248+
// This check is needed because it's only here that we can determine if the resource exists.
249+
_logger.LogWarning("Access to the resource path {ResourcePath} was not authorized for user {UserName} : userId {UserId}.",
250+
resourcePath.RawResourcePath, userIdentity!.Username, userIdentity!.UserId);
251+
throw new ResourceProviderException("Access is not authorized.", StatusCodes.Status403Forbidden);
252+
}
253+
231254
if (resourcePath.ResourceTypeInstances[0].ResourceId != agent.Name)
232255
throw new ResourceProviderException("The resource path does not match the object definition (name mismatch).",
233256
StatusCodes.Status400BadRequest);
@@ -479,7 +502,7 @@ private async Task<List<ResourceProviderGetResult<AgentAccessToken>>> LoadAgentA
479502
agentClientSecretKey.InstanceId,
480503
agentClientSecretKey.ContextId);
481504

482-
return secretKeys.Select(k => new ResourceProviderGetResult<AgentAccessToken>()
505+
return [.. secretKeys.Select(k => new ResourceProviderGetResult<AgentAccessToken>()
483506
{
484507
Actions = [],
485508
Roles = [],
@@ -491,15 +514,28 @@ private async Task<List<ResourceProviderGetResult<AgentAccessToken>>> LoadAgentA
491514
Active = k.Active,
492515
ExpirationDate = k.ExpirationDate
493516
}
494-
}).ToList();
517+
})];
495518
}
496519

497-
private async Task<ResourceProviderUpsertResult> UpdateAgentAccessToken(ResourcePath resourcePath, string serializedAgentAccessToken)
520+
private async Task<ResourceProviderUpsertResult> UpdateAgentAccessToken(
521+
ResourcePath resourcePath,
522+
string serializedAgentAccessToken,
523+
ResourcePathAuthorizationResult authorizationResult,
524+
UnifiedUserIdentity userIdentity)
498525
{
499526
var agentAccessToken = JsonSerializer.Deserialize<AgentAccessToken>(serializedAgentAccessToken)
500527
?? throw new ResourceProviderException("The object definition is invalid.",
501528
StatusCodes.Status400BadRequest);
502529

530+
if (!authorizationResult.Authorized
531+
|| !authorizationResult.HasRequiredRole)
532+
{
533+
// Agent access keys can be created or updated only by users with the required role and authorization on the agent.
534+
_logger.LogWarning("Access to the resource path {ResourcePath} was not authorized for user {UserName} : userId {UserId}.",
535+
resourcePath.RawResourcePath, userIdentity!.Username, userIdentity!.UserId);
536+
throw new ResourceProviderException("Access is not authorized.", StatusCodes.Status403Forbidden);
537+
}
538+
503539
var agentClientSecretKey = new AgentClientSecretKey
504540
{
505541
InstanceId = resourcePath.InstanceId!,

src/dotnet/Authorization/ResourceProviders/AuthorizationResourceProviderService.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,13 @@ private static List<RoleDefinition> LoadRoleDefinitions(ResourceTypeInstance ins
8989
#endregion
9090

9191
/// <inheritdoc/>
92-
protected override async Task<object> UpsertResourceAsync(ResourcePath resourcePath, string? serializedResource, ResourceProviderFormFile? formFile, UnifiedUserIdentity userIdentity) =>
92+
protected override async Task<object> UpsertResourceAsync(
93+
ResourcePath resourcePath,
94+
string? serializedResource,
95+
ResourceProviderFormFile? formFile,
96+
ResourcePathAuthorizationResult authorizationResult,
97+
UnifiedUserIdentity userIdentity) =>
98+
9399
resourcePath.MainResourceTypeName switch
94100
{
95101
AuthorizationResourceTypeNames.RoleAssignments => await UpdateRoleAssignments(resourcePath, serializedResource!, userIdentity),

src/dotnet/AuthorizationEngine/Services/AuthorizationCore.cs

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -310,10 +310,9 @@ public ActionAuthorizationResult ProcessAuthorizationRequest(string instanceId,
310310
authorizationResults[rp] = ProcessAuthorizationRequestForResourcePath(parsedResourcePath, new ActionAuthorizationRequest()
311311
{
312312
Action = authorizationRequest.Action,
313+
RoleName = authorizationRequest.RoleName,
313314
ResourcePaths = [rp],
314-
ExpandResourceTypePaths = parsedResourcePath.IsResourceTypePath
315-
? authorizationRequest.ExpandResourceTypePaths
316-
: false,
315+
ExpandResourceTypePaths = parsedResourcePath.IsResourceTypePath && authorizationRequest.ExpandResourceTypePaths,
317316
IncludeRoles = authorizationRequest.IncludeRoles,
318317
IncludeActions = authorizationRequest.IncludeActions,
319318
UserContext = authorizationRequest.UserContext
@@ -351,6 +350,7 @@ private ResourcePathAuthorizationResult ProcessAuthorizationRequestForResourcePa
351350
ResourceName = resourcePath.MainResourceId,
352351
ResourcePath = resourcePath.RawResourcePath,
353352
Authorized = false,
353+
HasRequiredRole = false,
354354
Roles = [],
355355
PolicyDefinitionIds = [],
356356
SubordinateResourcePathsAuthorizationResults = []
@@ -364,11 +364,10 @@ private ResourcePathAuthorizationResult ProcessAuthorizationRequestForResourcePa
364364
if (_policyAssignmentCaches.TryGetValue(resourcePath.InstanceId!, out var policyAssignmentCache))
365365
{
366366
// Policies are only assigned to resource type paths.
367-
result.PolicyDefinitionIds = policyAssignmentCache
367+
result.PolicyDefinitionIds = [.. policyAssignmentCache
368368
.GetPolicyAssignments(resourcePath.GetResourceTypeObjectId())
369369
.Where(pa => securityPrincipalIds.Contains(pa.PrincipalId))
370-
.Select(pa => pa.PolicyDefinitionId)
371-
.ToList();
370+
.Select(pa => pa.PolicyDefinitionId)];
372371
}
373372

374373
// Get cache associated with the instance id.
@@ -387,21 +386,26 @@ private ResourcePathAuthorizationResult ProcessAuthorizationRequestForResourcePa
387386
{
388387
// Check if the scope of the role assignment covers the resource.
389388
// Check if the actions of the role definition include the requested action.
390-
if (resourcePath.IncludesResourcePath(roleAssignment.ScopeResourcePath!)
391-
&& roleAssignment.AllowedActions.Contains(authorizationRequest.Action))
389+
if (resourcePath.IncludesResourcePath(roleAssignment.ScopeResourcePath!))
392390
{
393-
result.Authorized = true;
394-
395-
// If we are not asked to include roles or actions and not asked to expand resource paths,
396-
// we can return immediately (this is the most common case).
397-
// Otherwise, we need to go through the entire list of security principals and their role assignments,
398-
// to include collect all the roles/actions and/or all the subordinate authorized resource paths.
399-
if (!authorizationRequest.IncludeRoles
400-
&& !authorizationRequest.IncludeActions
401-
&& !authorizationRequest.ExpandResourceTypePaths)
402-
return result;
403-
404-
allSecurableActions.UnionWith(roleAssignment.AllowedActions);
391+
if (roleAssignment.RoleDefinition!.Name == authorizationRequest.RoleName)
392+
result.HasRequiredRole = true;
393+
394+
if (roleAssignment.AllowedActions.Contains(authorizationRequest.Action))
395+
{
396+
result.Authorized = true;
397+
398+
// If we are not asked to include roles or actions and not asked to expand resource paths,
399+
// we can return immediately (this is the most common case).
400+
// Otherwise, we need to go through the entire list of security principals and their role assignments,
401+
// to include collect all the roles/actions and/or all the subordinate authorized resource paths.
402+
if (!authorizationRequest.IncludeRoles
403+
&& !authorizationRequest.IncludeActions
404+
&& !authorizationRequest.ExpandResourceTypePaths)
405+
return result;
406+
407+
allSecurableActions.UnionWith(roleAssignment.AllowedActions);
408+
}
405409
}
406410
}
407411
else
@@ -424,12 +428,14 @@ private ResourcePathAuthorizationResult ProcessAuthorizationRequestForResourcePa
424428
if (authorizationRequest.IncludeRoles
425429
&& allRoleAssignments.Count > 0)
426430
{
427-
// Include the display names of the roles in the result that match the scope of the resource.
428-
result.Roles = allRoleAssignments
431+
var matchingRoleAssignments = allRoleAssignments
429432
.Where(ra => resourcePath.IncludesResourcePath(ra.ScopeResourcePath!))
430-
.Select(ra => ra.RoleDefinition!.DisplayName!)
431-
.Distinct()
432433
.ToList();
434+
435+
// Include the display names of the roles in the result that match the scope of the resource.
436+
result.Roles = [.. matchingRoleAssignments
437+
.Select(ra => ra.RoleDefinition!.DisplayName!)
438+
.Distinct()];
433439
}
434440

435441
if (authorizationRequest.IncludeActions
@@ -618,15 +624,15 @@ public List<SecretKey> GetSecretKeys(string instanceId, string contextId)
618624
{
619625
var persistedSecretKeys = secretKeyCache.GetKeys(contextId);
620626

621-
return persistedSecretKeys.Select(x => new SecretKey()
627+
return [.. persistedSecretKeys.Select(x => new SecretKey()
622628
{
623629
Id = x.Id,
624630
ContextId = contextId,
625631
InstanceId = x.InstanceId,
626632
Description = x.Description,
627633
Active = x.Active,
628634
ExpirationDate = x.ExpirationDate,
629-
}).ToList();
635+
})];
630636
}
631637

632638
return [];

src/dotnet/Common/Clients/AuthorizationServiceClient.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public AuthorizationServiceClient(
4949
public async Task<ActionAuthorizationResult> ProcessAuthorizationRequest(
5050
string instanceId,
5151
string action,
52+
string? roleName,
5253
List<string> resourcePaths,
5354
bool expandResourceTypePaths,
5455
bool includeRoleAssignments,
@@ -69,6 +70,7 @@ public async Task<ActionAuthorizationResult> ProcessAuthorizationRequest(
6970
var authorizationRequest = new ActionAuthorizationRequest
7071
{
7172
Action = action,
73+
RoleName = roleName,
7274
ResourcePaths = resourcePaths,
7375
ExpandResourceTypePaths = expandResourceTypePaths,
7476
IncludeRoles = includeRoleAssignments,

src/dotnet/Common/Clients/NullAuthorizationServiceClient.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public class NullAuthorizationServiceClient : IAuthorizationServiceClient
1414
public async Task<ActionAuthorizationResult> ProcessAuthorizationRequest(
1515
string instanceId,
1616
string action,
17+
string? roleName,
1718
List<string> resourcePaths,
1819
bool expandResourceTypePaths,
1920
bool includeRoleAssignments,
@@ -26,7 +27,8 @@ public async Task<ActionAuthorizationResult> ProcessAuthorizationRequest(
2627
{
2728
ResourceName = string.Empty,
2829
ResourcePath = rp,
29-
Authorized = true
30+
Authorized = true,
31+
HasRequiredRole = true,
3032
});
3133

3234
await Task.CompletedTask;

src/dotnet/Common/Constants/Data/RoleDefinitions.json

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,5 +185,90 @@
185185
"updated_on": "2024-10-22T00:00:00.0000000Z",
186186
"created_by": null,
187187
"updated_by": null
188+
},
189+
{
190+
"type": "FoundationaLLM.Authorization/roleDefinitions",
191+
"name": "2da16a58-ed63-431a-b90e-9df32c2cae4a",
192+
"object_id": "/providers/FoundationaLLM.Authorization/roleDefinitions/2da16a58-ed63-431a-b90e-9df32c2cae4a",
193+
"display_name": "Data Pipelines Contributor",
194+
"description": "Create new data pipelines.",
195+
"assignable_scopes": [
196+
"/"
197+
],
198+
"permissions": [
199+
{
200+
"actions": [
201+
"FoundationaLLM.DataPipeline/dataPipelines/read",
202+
"FoundationaLLM.Vectorization/vectorizationPipelines/read",
203+
"FoundationaLLM.DataSource/dataSources/read",
204+
"FoundationaLLM.Vectorization/textPartitioningProfiles/read",
205+
"FoundationaLLM.Vectorization/textEmbeddingProfiles/read",
206+
"FoundationaLLM.Vectorization/indexingProfiles/read",
207+
"FoundationaLLM.Plugin/plugins/read"
208+
],
209+
"not_actions": [],
210+
"data_actions": [],
211+
"not_data_actions": []
212+
}
213+
],
214+
"created_on": "2025-05-01T00:00:00.0000000Z",
215+
"updated_on": "2025-05-01T00:00:00.0000000Z",
216+
"created_by": null,
217+
"updated_by": null
218+
},
219+
{
220+
"type": "FoundationaLLM.Authorization/roleDefinitions",
221+
"name": "3f28aa77-a854-4aa7-ae11-ffda238275c9",
222+
"object_id": "/providers/FoundationaLLM.Authorization/roleDefinitions/3f28aa77-a854-4aa7-ae11-ffda238275c9",
223+
"display_name": "Agents Contributor",
224+
"description": "Create new agents.",
225+
"assignable_scopes": [
226+
"/"
227+
],
228+
"permissions": [
229+
{
230+
"actions": [
231+
"FoundationaLLM.Agent/agents/read",
232+
"FoundationaLLM.Agent/workflows/read",
233+
"FoundationaLLM.Agent/tools/read",
234+
"FoundationaLLM.Prompt/prompts/read",
235+
"FoundationaLLM.DataSource/dataSources/read",
236+
"FoundationaLLM.Vectorization/textEmbeddingProfiles/read",
237+
"FoundationaLLM.Vectorization/indexingProfiles/read",
238+
"FoundationaLLM.Configuration/apiEndpointConfigurations/read",
239+
"FoundationaLLM.AIModel/aiModels/read",
240+
"FoundationaLLM.Plugin/plugins/read"
241+
],
242+
"not_actions": [],
243+
"data_actions": [],
244+
"not_data_actions": []
245+
}
246+
],
247+
"created_on": "2025-05-01T00:00:00.0000000Z",
248+
"updated_on": "2025-05-01T00:00:00.0000000Z",
249+
"created_by": null,
250+
"updated_by": null
251+
},
252+
{
253+
"type": "FoundationaLLM.Authorization/roleDefinitions",
254+
"name": "8c5ea0d3-f5a1-4be5-90a7-a12921c45542",
255+
"object_id": "/providers/FoundationaLLM.Authorization/roleDefinitions/8c5ea0d3-f5a1-4be5-90a7-a12921c45542",
256+
"display_name": "Agent Access Tokens Contributor",
257+
"description": "Create new agent access tokens.",
258+
"assignable_scopes": [
259+
"/"
260+
],
261+
"permissions": [
262+
{
263+
"actions": [],
264+
"not_actions": [],
265+
"data_actions": [],
266+
"not_data_actions": []
267+
}
268+
],
269+
"created_on": "2025-05-01T00:00:00.0000000Z",
270+
"updated_on": "2025-05-01T00:00:00.0000000Z",
271+
"created_by": null,
272+
"updated_by": null
188273
}
189274
]

0 commit comments

Comments
 (0)