-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
19 changed files
with
432 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,4 +16,4 @@ jobs: | |
uses: SneaksAndData/github-actions/[email protected] | ||
with: | ||
major_v: 0 | ||
minor_v: 0 | ||
minor_v: 1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
using SnD.ApiClient.Boxer.Exceptions; | ||
using SnD.ApiClient.Boxer.Models; | ||
|
||
namespace SnD.ApiClient.Boxer.Base; | ||
|
||
public interface IBoxerClaimsClient | ||
{ | ||
/// <summary> | ||
/// Create a jwt-user registration in Boxer for a given user id and provider | ||
/// If user already exists, the method not do anything and return true | ||
/// </summary> | ||
/// <param name="userId"></param> | ||
/// <param name="provider"></param> | ||
/// <param name="cancellationToken"></param> | ||
/// <returns>true if success or if user already exists</returns> | ||
/// <exception cref="HttpRequestException">Throws if the request to Boxer fails</exception> | ||
public Task<bool> CreateUserAsync(string userId, string provider, CancellationToken cancellationToken); | ||
|
||
/// <summary> | ||
/// Deletes a user from Boxer by user id and provider (jwt-user registration) and all its claims. | ||
/// </summary> | ||
/// <param name="userId"></param> | ||
/// <param name="provider"></param> | ||
/// <param name="cancellationToken"></param> | ||
/// <returns></returns> | ||
/// <exception cref="UserNotFoundException">Throws if the user is not found under that identity provider</exception> | ||
/// <exception cref="HttpRequestException">Throws if the request to Boxer fails</exception> | ||
public Task<bool> DisassociateUserAsync(string userId, string provider, CancellationToken cancellationToken); | ||
|
||
/// <summary> | ||
/// Get claims by user id and provider | ||
/// </summary> | ||
/// <param name="userId">User principal name (UPN) in Boxer</param> | ||
/// <param name="provider">Identity provider (IDP) in Boxer</param> | ||
/// <param name="cancellationToken">Cancellation token</param> | ||
/// <returns>Enumerator of object <see cref="BoxerJwtClaim"/> for the user</returns> | ||
/// <exception cref="UserNotFoundException">Throws if the user is not found under that identity provider</exception> | ||
/// <exception cref="HttpRequestException">Throws if the request to Boxer fails</exception> | ||
public Task<IEnumerable<BoxerJwtClaim>> GetUserClaimsAsync(string userId, string provider, CancellationToken cancellationToken); | ||
|
||
/// <summary> | ||
/// Set (Update/Edit) claims for user id and provider | ||
/// </summary> | ||
/// <param name="userId">User principal name (UPN) in Boxer</param> | ||
/// <param name="provider">Identity provider (IDP) in Boxer</param> | ||
/// <param name="claims">Claims to set in the form of an enumerator of type <see cref="BoxerJwtClaim"/></param> | ||
/// <param name="cancellationToken">Cancellation token</param> | ||
/// <returns>True if the operation was successful, false otherwise</returns> | ||
/// <exception cref="UserNotFoundException">Throws if the user is not found under that identity provider</exception> | ||
/// <exception cref="HttpRequestException">Throws if the request to Boxer fails</exception> | ||
public Task<bool> PatchUserClaimsAsync(string userId, string provider, IEnumerable<BoxerJwtClaim> claims, CancellationToken cancellationToken); | ||
|
||
/// <summary> | ||
/// Delete claims for user id and provider | ||
/// Note: The method will match claims by user, identity provider and path. It will not match by api methods. | ||
/// </summary> | ||
/// <param name="userId">User principal name (UPN) in Boxer</param> | ||
/// <param name="provider">Identity provider (IDP) in Boxer</param> | ||
/// <param name="claims">Claims to delete in the form of an enumerator of type <see cref="BoxerJwtClaim"/></param> | ||
/// <param name="cancellationToken">Cancellation token</param> | ||
/// <returns>true on success</returns> | ||
/// <exception cref="UserNotFoundException">Throws if the user is not found under that identity provider</exception> | ||
/// <exception cref="HttpRequestException">Throws if the request to Boxer fails</exception> | ||
public Task<bool> DeleteUserClaimsAsync(string userId, string provider, IEnumerable<BoxerJwtClaim> claims, CancellationToken cancellationToken); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
using System.Net; | ||
using System.Text; | ||
using System.Text.Json; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
using SnD.ApiClient.Base; | ||
using SnD.ApiClient.Boxer.Base; | ||
using SnD.ApiClient.Boxer.Exceptions; | ||
using SnD.ApiClient.Boxer.Extensions; | ||
using SnD.ApiClient.Boxer.Models; | ||
using SnD.ApiClient.Config; | ||
|
||
namespace SnD.ApiClient.Boxer; | ||
|
||
public class BoxerClaimsClient : SndApiClient, IBoxerClaimsClient | ||
{ | ||
private readonly Uri claimsUri; | ||
|
||
public BoxerClaimsClient | ||
(IOptions<BoxerClaimsClientOptions> boxerClientOptions, HttpClient httpClient, | ||
IJwtTokenExchangeProvider boxerConnector, ILogger<BoxerClaimsClient> logger) : base(httpClient, boxerConnector, | ||
logger) | ||
{ | ||
claimsUri = new Uri(boxerClientOptions.Value.BaseUri | ||
?? throw new ArgumentNullException(nameof(BoxerClaimsClientOptions.BaseUri))); | ||
} | ||
|
||
public async Task<bool> CreateUserAsync(string userId, string provider, CancellationToken cancellationToken) | ||
{ | ||
cancellationToken.ThrowIfCancellationRequested(); | ||
var requestUri = new Uri(claimsUri, new Uri($"claim/{provider}/{userId}", UriKind.Relative)); | ||
var request = new HttpRequestMessage(HttpMethod.Post, requestUri); | ||
request.Content = new StringContent("{}", Encoding.UTF8, "application/json"); | ||
var response = await SendAuthenticatedRequestAsync(request, cancellationToken); | ||
return response.EnsureSuccessStatusCode().IsSuccessStatusCode; | ||
} | ||
|
||
public async Task<bool> DisassociateUserAsync(string userId, string provider, CancellationToken cancellationToken) | ||
{ | ||
cancellationToken.ThrowIfCancellationRequested(); | ||
var requestUri = new Uri(claimsUri, new Uri($"claim/{provider}/{userId}", UriKind.Relative)); | ||
var request = new HttpRequestMessage(HttpMethod.Delete, requestUri); | ||
request.Content = new StringContent("{}", Encoding.UTF8, "application/json"); | ||
var response = await SendAuthenticatedRequestAsync(request, cancellationToken); | ||
return response.StatusCode switch | ||
{ | ||
HttpStatusCode.NotFound => throw new UserNotFoundException(), | ||
_ => response.EnsureSuccessStatusCode().IsSuccessStatusCode | ||
}; | ||
} | ||
|
||
public async Task<IEnumerable<BoxerJwtClaim>> GetUserClaimsAsync(string userId, string provider, | ||
CancellationToken cancellationToken) | ||
{ | ||
cancellationToken.ThrowIfCancellationRequested(); | ||
var requestUri = new Uri(claimsUri, new Uri($"claim/{provider}/{userId}", UriKind.Relative)); | ||
var request = new HttpRequestMessage(HttpMethod.Get, requestUri); | ||
var response = await SendAuthenticatedRequestAsync(request, cancellationToken); | ||
return await response.ExtractClaimsAsync(); | ||
} | ||
|
||
public async Task<bool> PatchUserClaimsAsync(string userId, string provider, IEnumerable<BoxerJwtClaim> claims, | ||
CancellationToken cancellationToken) | ||
{ | ||
cancellationToken.ThrowIfCancellationRequested(); | ||
var requestUri = new Uri(claimsUri, new Uri($"claim/{provider}/{userId}", UriKind.Relative)); | ||
var requestBody = BoxerClaimsApiPatchBody.CreateInsertOperation(claims); | ||
var request = new HttpRequestMessage(HttpMethod.Patch, requestUri); | ||
request.Content = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json"); | ||
var response = await SendAuthenticatedRequestAsync(request, cancellationToken); | ||
return response.StatusCode switch | ||
{ | ||
HttpStatusCode.NotFound => throw new UserNotFoundException(), | ||
_ => response.EnsureSuccessStatusCode().IsSuccessStatusCode | ||
}; | ||
} | ||
|
||
public async Task<bool> DeleteUserClaimsAsync(string userId, string provider, IEnumerable<BoxerJwtClaim> claims, | ||
CancellationToken cancellationToken) | ||
{ | ||
cancellationToken.ThrowIfCancellationRequested(); | ||
var requestUri = new Uri(claimsUri, new Uri($"claim/{provider}/{userId}", UriKind.Relative)); | ||
var requestBody = BoxerClaimsApiPatchBody.CreateDeleteOperation(claims); | ||
var request = new HttpRequestMessage(HttpMethod.Patch, requestUri); | ||
request.Content = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json"); | ||
var response = await SendAuthenticatedRequestAsync(request, cancellationToken); | ||
return response.StatusCode switch | ||
{ | ||
HttpStatusCode.NotFound => throw new UserNotFoundException(), | ||
_ => response.EnsureSuccessStatusCode().IsSuccessStatusCode | ||
}; | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
src/SnD.ApiClient/Boxer/Exceptions/UserNotFoundException.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
using System.Diagnostics.CodeAnalysis; | ||
|
||
namespace SnD.ApiClient.Boxer.Exceptions; | ||
|
||
/// <summary> | ||
/// Exception thrown when a user is not found in Boxer, e.g., when trying to add claims to a user that does not exist | ||
/// </summary> | ||
[ExcludeFromCodeCoverage] | ||
public class UserNotFoundException : Exception | ||
{ | ||
/// <summary> | ||
/// Create a new instance of <see cref="UserNotFoundException"/> | ||
/// </summary> | ||
public UserNotFoundException() : base("Requested user not found in Boxer") | ||
{ | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
src/SnD.ApiClient/Boxer/Extensions/BoxerJwtClaimExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
using System.Net; | ||
using System.Text.Json; | ||
using SnD.ApiClient.Boxer.Exceptions; | ||
using SnD.ApiClient.Boxer.Models; | ||
|
||
namespace SnD.ApiClient.Boxer.Extensions; | ||
|
||
public static class BoxerJwtClaimExtensions | ||
{ | ||
public static async Task<IEnumerable<BoxerJwtClaim>> ExtractClaimsAsync(this HttpResponseMessage response) | ||
{ | ||
return response.StatusCode switch | ||
{ | ||
HttpStatusCode.NotFound => throw new UserNotFoundException(), | ||
_ => JsonSerializer.Deserialize<GetUserClaimsResponse>(await response.EnsureSuccessStatusCode().Content | ||
.ReadAsStringAsync()).Claims.Select(c => new BoxerJwtClaim(c.First().Key, c.First().Value)) | ||
}; | ||
} | ||
} |
32 changes: 32 additions & 0 deletions
32
src/SnD.ApiClient/Boxer/Extensions/StringHttpMethodExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
using System.Collections.Immutable; | ||
using System.Text.RegularExpressions; | ||
|
||
namespace SnD.ApiClient.Boxer.Extensions; | ||
|
||
public static class StringHttpMethodExtensions | ||
{ | ||
/// <summary> | ||
/// Convert a string to a collection of HttpMethod that matches (by regex) the string | ||
/// </summary> | ||
/// <param name="httpMethods"></param> | ||
/// <returns></returns> | ||
public static IEnumerable<HttpMethod> ToBoxerHttpMethods(this string httpMethods) | ||
{ | ||
if (string.IsNullOrWhiteSpace(httpMethods)) | ||
return Enumerable.Empty<HttpMethod>(); | ||
var regex = new Regex(httpMethods); | ||
return new[] { HttpMethod.Get, HttpMethod.Post, HttpMethod.Put, HttpMethod.Delete, HttpMethod.Patch } | ||
.Where(m => regex.IsMatch(m.Method)); | ||
} | ||
|
||
/// <summary> | ||
/// Convert a collection of HttpMethod to a regex string that matches the collection | ||
/// </summary> | ||
/// <param name="httpMethods"></param> | ||
/// <returns></returns> | ||
public static string ToRegexString(this IEnumerable<HttpMethod> httpMethods) | ||
{ | ||
var methods = httpMethods.Select(m=>m.Method).ToImmutableSortedSet(); | ||
return methods.IsEmpty ? string.Empty : $"^({string.Join("|", methods)})$"; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Text.Json.Serialization; | ||
|
||
|
||
namespace SnD.ApiClient.Boxer.Models; | ||
|
||
[ExcludeFromCodeCoverage] | ||
internal class BoxerClaimsApiPatchBody | ||
{ | ||
private BoxerClaimsApiPatchBody() | ||
{ | ||
} | ||
|
||
public static BoxerClaimsApiPatchBody CreateInsertOperation(IEnumerable<BoxerJwtClaim> claims) => | ||
new() | ||
{ | ||
Operation = "Insert", | ||
Claims = claims.ToDictionary(c => c.Type, c => c.Value) | ||
}; | ||
|
||
public static BoxerClaimsApiPatchBody CreateDeleteOperation(IEnumerable<BoxerJwtClaim> claims) => | ||
new() | ||
{ | ||
Operation = "Delete", | ||
Claims = claims.ToDictionary(c => c.Type, c => c.Value) | ||
}; | ||
|
||
[JsonPropertyName("operation")] | ||
public string Operation { get; private set; } | ||
|
||
[JsonPropertyName("claims")] | ||
public Dictionary<string, string> Claims { get; private set; } = new(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Security.Claims; | ||
using System.Text.Json; | ||
using SnD.ApiClient.Boxer.Extensions; | ||
|
||
namespace SnD.ApiClient.Boxer.Models; | ||
|
||
[ExcludeFromCodeCoverage] | ||
public class BoxerJwtClaim : Claim | ||
{ | ||
internal BoxerJwtClaim(string type, string value) : base(type, value) | ||
{ | ||
ApiMethods = value.ToBoxerHttpMethods().ToHashSet(); | ||
Path = type; | ||
} | ||
|
||
/// <summary> | ||
/// Static method to convert a Boxer API Claims response to a collection of BoxerJwtClaim objects | ||
/// </summary> | ||
/// <param name="response"></param> | ||
/// <returns></returns> | ||
public static IEnumerable<BoxerJwtClaim> FromBoxerClaimsApiResponse(string response) | ||
{ | ||
return JsonSerializer.Deserialize<GetUserClaimsResponse>(response) | ||
.Claims.Select(c => new BoxerJwtClaim(c.First().Key, c.First().Value)); | ||
} | ||
|
||
/// <summary> | ||
/// Static constructor to create a BoxerJwtClaim from a path and a collection of ApiMethodElement | ||
/// </summary> | ||
/// <param name="path"></param> | ||
/// <param name="apiMethods"></param> | ||
/// <returns></returns> | ||
public static BoxerJwtClaim Create(string path, HashSet<HttpMethod> apiMethods) | ||
{ | ||
return new BoxerJwtClaim(path, apiMethods.ToRegexString()); | ||
} | ||
|
||
/// <summary> | ||
/// API path | ||
/// </summary> | ||
public readonly string Path; | ||
|
||
/// <summary> | ||
/// API methods | ||
/// </summary> | ||
public readonly HashSet<HttpMethod> ApiMethods; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
using System.Text.Json.Serialization; | ||
|
||
namespace SnD.ApiClient.Boxer.Models; | ||
|
||
internal class GetUserClaimsResponse | ||
{ | ||
[JsonPropertyName("identityProvider")] | ||
public string IdentityProvider { get; set; } | ||
|
||
[JsonPropertyName("userId")] | ||
public string UserId { get; set; } | ||
|
||
[JsonPropertyName("claims")] | ||
public List<Dictionary<string, string>> Claims { get; set; } | ||
|
||
[JsonPropertyName("billingId")] | ||
public object BillingId { get; set; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
namespace SnD.ApiClient.Config; | ||
|
||
public class BoxerClaimsClientOptions | ||
{ | ||
/// <summary> | ||
/// Base URI of the Boxer Claims API instance | ||
/// </summary> | ||
public string BaseUri { get; set; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.