// Copyright (c) Microsoft. All rights reserved. using System; using System.Net; using System.Net.Http; using System.Text; using System.Text.Json; using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Tasks; using Azure.Core; using Microsoft.Agents.AI.Purview.Models.Common; using Microsoft.Agents.AI.Purview.Models.Requests; using Microsoft.Agents.AI.Purview.Models.Responses; using Microsoft.Agents.AI.Purview.Serialization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.Agents.AI.Purview; /// /// Client for calling Purview APIs. /// internal sealed class PurviewClient : IPurviewClient { private readonly TokenCredential _tokenCredential; private readonly HttpClient _httpClient; private readonly string[] _scopes; private readonly string _graphUri; private readonly ILogger _logger; private static PurviewException CreateExceptionForStatusCode(HttpStatusCode statusCode, string endpointName) { // .net framework does not support TooManyRequests, so we have to convert to an int. switch ((int)statusCode) { case 429: return new PurviewRateLimitException($"Rate limit exceeded for {endpointName}."); case 401: case 403: return new PurviewAuthenticationException($"Unauthorized access to {endpointName}. Status code: {statusCode}"); case 402: return new PurviewPaymentRequiredException($"Payment required for {endpointName}. Status code: {statusCode}"); default: return new PurviewRequestException(statusCode, endpointName); } } /// /// Creates a new instance. /// /// The token credential used to authenticate with Purview. /// The settings used for purview requests. /// The HttpClient used to make network requests to Purview. /// The logger used to log information from the middleware. public PurviewClient(TokenCredential tokenCredential, PurviewSettings purviewSettings, HttpClient httpClient, ILogger logger) { this._tokenCredential = tokenCredential; this._httpClient = httpClient; this._scopes = [$"https://{purviewSettings.GraphBaseUri.Host}/.default"]; this._graphUri = purviewSettings.GraphBaseUri.ToString().TrimEnd('/'); this._logger = logger ?? NullLogger.Instance; } private static TokenInfo ExtractTokenInfo(string tokenString) { // Split JWT and decode payload string[] parts = tokenString.Split('.'); if (parts.Length < 2) { throw new PurviewRequestException("Invalid JWT access token format."); } string payload = parts[1]; // Pad base64 string if needed int mod4 = payload.Length % 4; if (mod4 > 0) { payload += new string('=', 4 - mod4); } byte[] bytes = Convert.FromBase64String(payload.Replace('-', '+').Replace('_', '/')); string json = Encoding.UTF8.GetString(bytes); using var doc = JsonDocument.Parse(json); var root = doc.RootElement; string? objectId = root.TryGetProperty("oid", out var oidProp) ? oidProp.GetString() : null; string? idType = root.TryGetProperty("idtyp", out var idtypProp) ? idtypProp.GetString() : null; string? tenant = root.TryGetProperty("tid", out var tidProp) ? tidProp.GetString() : null; string? clientId = root.TryGetProperty("appid", out var appidProp) ? appidProp.GetString() : null; string? userId = idType == "user" ? objectId : null; return new TokenInfo { UserId = userId, TenantId = tenant, ClientId = clientId }; } /// public async Task GetUserInfoFromTokenAsync(CancellationToken cancellationToken, string? tenantId = default) { TokenRequestContext tokenRequestContext = tenantId == null ? new(this._scopes) : new(this._scopes, tenantId: tenantId); AccessToken token = await this._tokenCredential.GetTokenAsync(tokenRequestContext, cancellationToken).ConfigureAwait(false); string tokenString = token.Token; return ExtractTokenInfo(tokenString); } /// public async Task ProcessContentAsync(ProcessContentRequest request, CancellationToken cancellationToken) { var token = await this._tokenCredential.GetTokenAsync(new TokenRequestContext(this._scopes, tenantId: request.TenantId), cancellationToken).ConfigureAwait(false); string userId = request.UserId; string uri = $"{this._graphUri}/users/{userId}/dataSecurityAndGovernance/processContent"; using (HttpRequestMessage message = new(HttpMethod.Post, new Uri(uri))) { message.Headers.Add("Authorization", $"Bearer {token.Token}"); message.Headers.Add("User-Agent", "agent-framework-dotnet"); if (request.ScopeIdentifier != null) { message.Headers.Add("If-None-Match", request.ScopeIdentifier); } if (request.ProcessInline) { message.Headers.Add("Prefer", "evaluateInline"); } string content = JsonSerializer.Serialize(request, PurviewSerializationUtils.SerializationSettings.GetTypeInfo(typeof(ProcessContentRequest))); message.Content = new StringContent(content, Encoding.UTF8, "application/json"); HttpResponseMessage response; try { response = await this._httpClient.SendAsync(message, cancellationToken).ConfigureAwait(false); } catch (HttpRequestException e) { this._logger.LogError(e, "Http error while processing content."); throw new PurviewRequestException("Http error occurred while processing content.", e); } #if NET5_0_OR_GREATER // Pass the cancellation token if that method is available. string responseContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); #else string responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); #endif if (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.Accepted) { ProcessContentResponse? deserializedResponse; try { JsonTypeInfo typeInfo = (JsonTypeInfo)PurviewSerializationUtils.SerializationSettings.GetTypeInfo(typeof(ProcessContentResponse)); deserializedResponse = JsonSerializer.Deserialize(responseContent, typeInfo); } catch (JsonException jsonException) { const string DeserializeExceptionError = "Failed to deserialize ProcessContent response."; this._logger.LogError(jsonException, DeserializeExceptionError); throw new PurviewRequestException(DeserializeExceptionError, jsonException); } if (deserializedResponse != null) { return deserializedResponse; } const string DeserializeError = "Failed to deserialize ProcessContent response. Response was null."; this._logger.LogError(DeserializeError); throw new PurviewRequestException(DeserializeError); } if (this._logger.IsEnabled(LogLevel.Error)) { this._logger.LogError("Failed to process content. Status code: {StatusCode}", response.StatusCode); } throw CreateExceptionForStatusCode(response.StatusCode, "processContent"); } } /// public async Task GetProtectionScopesAsync(ProtectionScopesRequest request, CancellationToken cancellationToken) { var token = await this._tokenCredential.GetTokenAsync(new TokenRequestContext(this._scopes), cancellationToken).ConfigureAwait(false); string userId = request.UserId; string uri = $"{this._graphUri}/users/{userId}/dataSecurityAndGovernance/protectionScopes/compute"; using (HttpRequestMessage message = new(HttpMethod.Post, new Uri(uri))) { message.Headers.Add("Authorization", $"Bearer {token.Token}"); message.Headers.Add("User-Agent", "agent-framework-dotnet"); var typeinfo = PurviewSerializationUtils.SerializationSettings.GetTypeInfo(typeof(ProtectionScopesRequest)); string content = JsonSerializer.Serialize(request, typeinfo); message.Content = new StringContent(content, Encoding.UTF8, "application/json"); HttpResponseMessage response; try { response = await this._httpClient.SendAsync(message, cancellationToken).ConfigureAwait(false); } catch (HttpRequestException e) { this._logger.LogError(e, "Http error while retrieving protection scopes."); throw new PurviewRequestException("Http error occurred while retrieving protection scopes.", e); } if (response.StatusCode == HttpStatusCode.OK) { #if NET5_0_OR_GREATER // Pass the cancellation token if that method is available. string responseContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); #else string responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); #endif ProtectionScopesResponse? deserializedResponse; try { JsonTypeInfo typeInfo = (JsonTypeInfo)PurviewSerializationUtils.SerializationSettings.GetTypeInfo(typeof(ProtectionScopesResponse)); deserializedResponse = JsonSerializer.Deserialize(responseContent, typeInfo); } catch (JsonException jsonException) { const string DeserializeExceptionError = "Failed to deserialize ProtectionScopes response."; this._logger.LogError(jsonException, DeserializeExceptionError); throw new PurviewRequestException(DeserializeExceptionError, jsonException); } if (deserializedResponse != null) { deserializedResponse.ScopeIdentifier = response.Headers.ETag?.Tag; return deserializedResponse; } const string DeserializeError = "Failed to deserialize ProtectionScopes response."; this._logger.LogError(DeserializeError); throw new PurviewRequestException(DeserializeError); } if (this._logger.IsEnabled(LogLevel.Error)) { this._logger.LogError("Failed to retrieve protection scopes. Status code: {StatusCode}", response.StatusCode); } throw CreateExceptionForStatusCode(response.StatusCode, "protectionScopes/compute"); } } /// public async Task SendContentActivitiesAsync(ContentActivitiesRequest request, CancellationToken cancellationToken) { var token = await this._tokenCredential.GetTokenAsync(new TokenRequestContext(this._scopes), cancellationToken).ConfigureAwait(false); string userId = request.UserId; string uri = $"{this._graphUri}/{userId}/dataSecurityAndGovernance/activities/contentActivities"; using (HttpRequestMessage message = new(HttpMethod.Post, new Uri(uri))) { message.Headers.Add("Authorization", $"Bearer {token.Token}"); message.Headers.Add("User-Agent", "agent-framework-dotnet"); string content = JsonSerializer.Serialize(request, PurviewSerializationUtils.SerializationSettings.GetTypeInfo(typeof(ContentActivitiesRequest))); message.Content = new StringContent(content, Encoding.UTF8, "application/json"); HttpResponseMessage response; try { response = await this._httpClient.SendAsync(message, cancellationToken).ConfigureAwait(false); } catch (HttpRequestException e) { this._logger.LogError(e, "Http error while creating content activities."); throw new PurviewRequestException("Http error occurred while creating content activities.", e); } if (response.StatusCode == HttpStatusCode.Created) { #if NET5_0_OR_GREATER // Pass the cancellation token if that method is available. string responseContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); #else string responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); #endif ContentActivitiesResponse? deserializedResponse; try { JsonTypeInfo typeInfo = (JsonTypeInfo)PurviewSerializationUtils.SerializationSettings.GetTypeInfo(typeof(ContentActivitiesResponse)); deserializedResponse = JsonSerializer.Deserialize(responseContent, typeInfo); } catch (JsonException jsonException) { const string DeserializeExceptionError = "Failed to deserialize ContentActivities response."; this._logger.LogError(jsonException, DeserializeExceptionError); throw new PurviewRequestException(DeserializeExceptionError, jsonException); } if (deserializedResponse != null) { return deserializedResponse; } const string DeserializeError = "Failed to deserialize ContentActivities response."; this._logger.LogError(DeserializeError); throw new PurviewRequestException(DeserializeError); } if (this._logger.IsEnabled(LogLevel.Error)) { this._logger.LogError("Failed to create content activities. Status code: {StatusCode}", response.StatusCode); } throw CreateExceptionForStatusCode(response.StatusCode, "contentActivities"); } } }