diff --git a/src/Auth0.ManagementApi/Clients/ConnectionsClient.cs b/src/Auth0.ManagementApi/Clients/ConnectionsClient.cs index 07a16bb2b..092cd5db6 100644 --- a/src/Auth0.ManagementApi/Clients/ConnectionsClient.cs +++ b/src/Auth0.ManagementApi/Clients/ConnectionsClient.cs @@ -14,7 +14,8 @@ namespace Auth0.ManagementApi.Clients /// public class ConnectionsClient : BaseClient, IConnectionsClient { - readonly JsonConverter[] converters = new JsonConverter[] { new PagedListConverter("connections") }; + private readonly JsonConverter[] _converters = { new PagedListConverter("connections") }; + private readonly JsonConverter[] _defaultMappingsConverter = { new ListConverter("mapping") }; /// /// Initializes a new instance of the . @@ -118,7 +119,7 @@ public Task> GetAllAsync(GetConnectionsRequest request, P } } - return Connection.GetAsync>(BuildUri("connections", queryStrings), DefaultHeaders, converters, cancellationToken); + return Connection.GetAsync>(BuildUri("connections", queryStrings), DefaultHeaders, _converters, cancellationToken); } /// @@ -143,5 +144,95 @@ public Task CheckStatusAsync(string id, CancellationToken cancellationToken = de { return Connection.GetAsync(BuildUri($"connections/{EncodePath(id)}/status"), DefaultHeaders, cancellationToken: cancellationToken); } + + /// + /// Creates an . + /// + /// The id of the connection to create an + /// containing information required for creating an + /// The cancellation token to cancel operation. + /// A . + public Task CreateScimConfigurationAsync(string id, ScimConfigurationCreateRequest request, CancellationToken cancellationToken = default) + { + return Connection.SendAsync(HttpMethod.Post, BuildUri($"connections/{EncodePath(id)}/scim-configuration"), request, DefaultHeaders, cancellationToken: cancellationToken); + } + + /// + /// Retrieves an . + /// + /// The id of the connection to retrieve its + /// The cancellation token to cancel operation. + /// A . + public Task GetScimConfigurationAsync(string id, CancellationToken cancellationToken = default) + { + return Connection.GetAsync(BuildUri($"connections/{EncodePath(id)}/scim-configuration"), DefaultHeaders, cancellationToken: cancellationToken); + } + + /// + /// Updates an n. + /// + /// The id of the connection to update its SCIM configuration + /// containing information required for updating an + /// The cancellation token to cancel operation. + /// A . + public Task UpdateScimConfigurationAsync(string id, ScimConfigurationUpdateRequest request, CancellationToken cancellationToken = default) + { + return Connection.SendAsync(new HttpMethod("PATCH"), BuildUri($"connections/{EncodePath(id)}/scim-configuration"), request, DefaultHeaders, cancellationToken: cancellationToken); + } + + /// + /// Deletes an . + /// + /// The id of the connection to delete its SCIM configuration + /// The cancellation token to cancel operation. + public Task DeleteScimConfigurationAsync(string id, CancellationToken cancellationToken = default) + { + return Connection.SendAsync(HttpMethod.Delete, BuildUri($"connections/{EncodePath(id)}/scim-configuration"), null, DefaultHeaders, cancellationToken: cancellationToken); + } + + /// + /// Retrieves the default . + /// + /// The id of the connection to retrieve its default + /// The cancellation token to cancel operation. + /// An IList of . + public Task> GetDefaultScimMappingAsync(string id, CancellationToken cancellationToken = default) + { + return Connection.GetAsync>(BuildUri($"connections/{EncodePath(id)}/scim-configuration/default-mapping"), DefaultHeaders, _defaultMappingsConverter, cancellationToken: cancellationToken); + } + + /// + /// Creates an . + /// + /// The id of the connection to create an + /// containing information required for creating an + /// The cancellation token to cancel operation. + /// An . + public Task CreateScimTokenAsync(string id, ScimTokenCreateRequest request, CancellationToken cancellationToken = default) + { + return Connection.SendAsync(HttpMethod.Post, BuildUri($"connections/{EncodePath(id)}/scim-configuration/tokens"), request, DefaultHeaders, cancellationToken: cancellationToken); + } + + /// + /// Retrieves all for the given connection. + /// + /// The id of the connection to retrieve its + /// The cancellation token to cancel operation. + /// An . + public Task> GetScimTokenAsync(string id, CancellationToken cancellationToken = default) + { + return Connection.GetAsync>(BuildUri($"connections/{EncodePath(id)}/scim-configuration/tokens"), DefaultHeaders, cancellationToken: cancellationToken); + } + + /// + /// Deletes an SCIM token. + /// + /// The ID of the connection that owns the to be deleted + /// The ID of the to delete + /// The cancellation token to cancel operation. + public Task DeleteScimTokenAsync(string id, string tokenId, CancellationToken cancellationToken = default) + { + return Connection.SendAsync(HttpMethod.Delete, BuildUri($"connections/{EncodePath(id)}/scim-configuration/tokens/{EncodePath(tokenId)}"), null, DefaultHeaders, cancellationToken: cancellationToken); + } } } diff --git a/src/Auth0.ManagementApi/Clients/IConnectionsClient.cs b/src/Auth0.ManagementApi/Clients/IConnectionsClient.cs index 70699bc4c..815c32c34 100644 --- a/src/Auth0.ManagementApi/Clients/IConnectionsClient.cs +++ b/src/Auth0.ManagementApi/Clients/IConnectionsClient.cs @@ -1,7 +1,10 @@ + + namespace Auth0.ManagementApi.Clients { using System.Threading; using System.Threading.Tasks; + using System.Collections.Generic; using Models; using Paging; @@ -70,5 +73,71 @@ public interface IConnectionsClient /// The cancellation token to cancel operation. /// A that represents the asynchronous check operation. Will throw if the status check fails. Task CheckStatusAsync(string id, CancellationToken cancellationToken = default); + + /// + /// Creates an . + /// + /// The id of the connection to create an + /// containing information required for creating an + /// The cancellation token to cancel operation. + /// A . + Task CreateScimConfigurationAsync(string id, ScimConfigurationCreateRequest request, CancellationToken cancellationToken = default); + + /// + /// Retrieves an . + /// + /// The id of the connection to retrieve its + /// The cancellation token to cancel operation. + /// A . + Task GetScimConfigurationAsync(string id, CancellationToken cancellationToken = default); + + /// + /// Updates an . + /// + /// The id of the connection to update + /// containing information required for updating an + /// The cancellation token to cancel operation. + /// A . + Task UpdateScimConfigurationAsync(string id, ScimConfigurationUpdateRequest request, CancellationToken cancellationToken = default); + + /// + /// Deletes an . + /// + /// The id of the connection to delete + /// The cancellation token to cancel operation. + Task DeleteScimConfigurationAsync(string id, CancellationToken cancellationToken = default); + + /// + /// Retrieves the default . + /// + /// The id of the connection to retrieve its default + /// The cancellation token to cancel operation. + /// An IList of . + Task> GetDefaultScimMappingAsync(string id, CancellationToken cancellationToken = default); + + /// + /// Creates an . + /// + /// The id of the connection to create an + /// containing information required for creating an + /// The cancellation token to cancel operation. + /// An . + Task CreateScimTokenAsync(string id, ScimTokenCreateRequest request, CancellationToken cancellationToken = default); + + /// + /// Retrieves all for the given connection. + /// + /// The id of the connection to retrieve its + /// The cancellation token to cancel operation. + /// An . + Task> GetScimTokenAsync(string id, CancellationToken cancellationToken = default); + + /// + /// Deletes an SCIM token. + /// + /// The ID of the connection that owns the to be deleted + /// The ID of the to delete + /// The cancellation token to cancel operation. + Task DeleteScimTokenAsync(string id, string tokenId, CancellationToken cancellationToken = default); } } diff --git a/src/Auth0.ManagementApi/Models/Scim/ScimConfiguration.cs b/src/Auth0.ManagementApi/Models/Scim/ScimConfiguration.cs new file mode 100644 index 000000000..21ab40900 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Scim/ScimConfiguration.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Auth0.ManagementApi.Models +{ + /// + /// Represents an SCIM Configuration + /// + public class ScimConfiguration + { + /// + /// The connection's identifier + /// + [JsonProperty("connection_id")] + public string ConnectionId { get; set; } + + /// + /// The connection's identifier + /// + [JsonProperty("connection_name")] + public string ConnectionName { get; set; } + + /// + /// The connection's strategy + /// + [JsonProperty("strategy")] + public string Strategy { get; set; } + + /// + /// The tenant's name + /// + [JsonProperty("tenant_name")] + public string TenantName { get; set; } + + /// + /// User ID attribute for generating unique user ids + /// + [JsonProperty("user_id_attribute")] + public string UserIdAttribute { get; set; } + + /// + /// The mapping between auth0 and SCIM + /// + [JsonProperty("mapping")] + public List Mapping { get; set; } + + /// + /// The Date Time SCIM Configuration was created + /// + [JsonProperty("created_at")] + public string CreatedAt { get; set; } + + /// + /// The Date Time SCIM Configuration was last updated + /// + [JsonProperty("updated_on")] + public string UpdatedOn { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Scim/ScimConfigurationCreateRequest.cs b/src/Auth0.ManagementApi/Models/Scim/ScimConfigurationCreateRequest.cs new file mode 100644 index 000000000..8e25c6a3a --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Scim/ScimConfigurationCreateRequest.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models +{ + public class ScimConfigurationCreateRequest + { + /// + /// User ID attribute for generating unique user ids + /// + [JsonProperty("user_id_attribute")] + public string UserIdAttribute { get; set; } + + /// + /// The mapping between auth0 and SCIM + /// + [JsonProperty("mapping")] + public List Mapping { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Scim/ScimConfigurationUpdateRequest.cs b/src/Auth0.ManagementApi/Models/Scim/ScimConfigurationUpdateRequest.cs new file mode 100644 index 000000000..1a0397e76 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Scim/ScimConfigurationUpdateRequest.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models +{ + public class ScimConfigurationUpdateRequest + { + /// + /// User ID attribute for generating unique user ids + /// + [JsonProperty("user_id_attribute")] + public string UserIdAttribute { get; set; } + + /// + /// The mapping between auth0 and SCIM + /// + [JsonProperty("mapping")] + public List Mapping { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Scim/ScimMapping.cs b/src/Auth0.ManagementApi/Models/Scim/ScimMapping.cs new file mode 100644 index 000000000..aa44d268a --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Scim/ScimMapping.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models +{ + /// + /// Represents the mapping between SCIM and Auth0 + /// + public class ScimMapping + { + /// + /// The field location in the auth0 schema + /// + [JsonProperty("auth0")] + public string Auth0 { get; set; } + + /// + /// The field location in the SCIM schema + /// + [JsonProperty("scim")] + public string Scim { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Scim/ScimToken.cs b/src/Auth0.ManagementApi/Models/Scim/ScimToken.cs new file mode 100644 index 000000000..424c0bb45 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Scim/ScimToken.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models +{ + public class ScimToken : ScimTokenBase + { + /// + /// The token's last used at timestamp + /// + [JsonProperty("last_used_at")] + public string LastUsedAt { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Scim/ScimTokenBase.cs b/src/Auth0.ManagementApi/Models/Scim/ScimTokenBase.cs new file mode 100644 index 000000000..87f20ab87 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Scim/ScimTokenBase.cs @@ -0,0 +1,35 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Auth0.ManagementApi.Models +{ + /// + /// Represents an SCIM token for an SCIM client. + /// + public class ScimTokenBase + { + /// + /// The token's identifier + /// + [JsonProperty("token_id")] + public string TokenId { get; set; } + + /// + /// The scopes of the scim token + /// + [JsonProperty("scopes")] + public string[] Scopes { get; set; } + + /// + /// The token's created at timestamp + /// + [JsonProperty("created_at")] + public string CreatedAt { get; set; } + + /// + /// The token's valid until at timestamp + /// + [JsonProperty("valid_until")] + public string ValidUntil { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Scim/ScimTokenCreateRequest.cs b/src/Auth0.ManagementApi/Models/Scim/ScimTokenCreateRequest.cs new file mode 100644 index 000000000..fdb8f3a1f --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Scim/ScimTokenCreateRequest.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Auth0.ManagementApi.Models +{ + /// + /// Represents the client request to Create + /// + public class ScimTokenCreateRequest + { + /// + /// The scopes of the scim token + /// + [JsonProperty("scopes")] + public string[] Scopes { get; set; } + + /// + /// Lifetime of the token in seconds. Must be greater than 900 + /// + [JsonProperty("token_lifetime")] + public int TokenLifetime { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Scim/ScimTokenCreateResponse.cs b/src/Auth0.ManagementApi/Models/Scim/ScimTokenCreateResponse.cs new file mode 100644 index 000000000..f235b247c --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Scim/ScimTokenCreateResponse.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models +{ + public class ScimTokenCreateResponse : ScimTokenBase + { + /// + /// The SCIM client's token + /// + [JsonProperty("token")] + public string Token { get; set; } + } +} \ No newline at end of file diff --git a/tests/Auth0.ManagementApi.IntegrationTests/ConnectionTests.cs b/tests/Auth0.ManagementApi.IntegrationTests/ConnectionTests.cs index 2d2708296..0e44b9c3a 100644 --- a/tests/Auth0.ManagementApi.IntegrationTests/ConnectionTests.cs +++ b/tests/Auth0.ManagementApi.IntegrationTests/ConnectionTests.cs @@ -9,6 +9,8 @@ using Auth0.Tests.Shared; using Auth0.ManagementApi.Paging; using System.Collections.Generic; +using Auth0.AuthenticationApi.Models; +using Auth0.Tests.Shared; namespace Auth0.ManagementApi.IntegrationTests { @@ -215,5 +217,236 @@ public async Task Test_multiple_strategies() // Assert Assert.NotNull(connections); } + + [Fact] + public async Task Test_scim_configuration_crud_sequence() + { + var expectedScimConfiguration = new ScimConfiguration() + { + Strategy = "samlp", + ConnectionId = "con_wP6Ya7Fbp98JQXuY", + ConnectionName = "fake-saml", + TenantName = "brucke", + UserIdAttribute = "string", + Mapping = new List() + { + new() + { + Auth0 = "string", + Scim = "string" + } + } + }; + + var token = await GenerateBruckeManagementApiToken(); + var apiClient = new ManagementApiClient(token, TestBaseUtils.GetVariable("BRUCKE_MANAGEMENT_API_URL"), + new HttpClientManagementConnection(options: new HttpClientManagementConnectionOptions + { NumberOfHttpRetries = 9 })); + try + { + // Create an SCIM configuration + await apiClient.Connections.CreateScimConfigurationAsync( + expectedScimConfiguration.ConnectionId, new ScimConfigurationCreateRequest() + { + UserIdAttribute = expectedScimConfiguration.UserIdAttribute, + Mapping = expectedScimConfiguration.Mapping + }); + + // Get SCIM configuration and verify + var scimConfiguration = + await apiClient.Connections.GetScimConfigurationAsync(expectedScimConfiguration.ConnectionId); + + AssertScimConfiguration(expectedScimConfiguration, scimConfiguration); + + // Update SCIM Configuration and Validate + var updateRequest = new ScimConfigurationUpdateRequest() + { + UserIdAttribute = "string", + Mapping = new List() + { + new() + { + Auth0 = "string", + Scim = "string" + }, + new() + { + Auth0 = "int", + Scim = "int" + } + } + + }; + expectedScimConfiguration.UserIdAttribute = updateRequest.UserIdAttribute; + scimConfiguration = + await apiClient.Connections.UpdateScimConfigurationAsync( + expectedScimConfiguration.ConnectionId, updateRequest); + AssertScimConfiguration(expectedScimConfiguration, scimConfiguration); + } + finally + { + // Delete SCIM Configuration and Validate + await apiClient.Connections.DeleteScimConfigurationAsync(expectedScimConfiguration.ConnectionId); + } + } + + [Fact] + public async Task Test_get_default_scim_configuration() + { + var expectedScimConfiguration = new ScimConfiguration() + { + Strategy = "samlp", + ConnectionId = "con_wP6Ya7Fbp98JQXuY", + ConnectionName = "fake-saml", + TenantName = "brucke", + UserIdAttribute = "string", + Mapping = new List() + { + new() + { + Auth0 = "random", + Scim = "random" + } + } + }; + + var token = await GenerateBruckeManagementApiToken(); + var apiClient = new ManagementApiClient(token, TestBaseUtils.GetVariable("BRUCKE_MANAGEMENT_API_URL"), + new HttpClientManagementConnection(options: new HttpClientManagementConnectionOptions + { NumberOfHttpRetries = 9 })); + try + { + // Create an SCIM configuration + await apiClient.Connections.CreateScimConfigurationAsync( + expectedScimConfiguration.ConnectionId, new ScimConfigurationCreateRequest() + { + UserIdAttribute = expectedScimConfiguration.UserIdAttribute, + Mapping = expectedScimConfiguration.Mapping + }); + + var defaultScimMapping = await apiClient.Connections.GetDefaultScimMappingAsync(expectedScimConfiguration.ConnectionId); + Assert.NotNull(defaultScimMapping); + } + finally + { + // Clean-up + await apiClient.Connections.DeleteScimConfigurationAsync(expectedScimConfiguration.ConnectionId); + } + } + + [Fact] + public async Task Test_scim_token_crud_sequence() + { + var expectedScimConfiguration = new ScimConfiguration() + { + Strategy = "samlp", + ConnectionId = "con_wP6Ya7Fbp98JQXuY", + ConnectionName = "fake-saml", + TenantName = "brucke", + UserIdAttribute = "string", + Mapping = new List() + { + new() + { + Auth0 = "random", + Scim = "random" + } + } + }; + + var token = await GenerateBruckeManagementApiToken(); + var apiClient = new ManagementApiClient(token, TestBaseUtils.GetVariable("BRUCKE_MANAGEMENT_API_URL"), + new HttpClientManagementConnection(options: new HttpClientManagementConnectionOptions + { NumberOfHttpRetries = 9 })); + try + { + // Create an SCIM configuration + await apiClient.Connections.CreateScimConfigurationAsync( + expectedScimConfiguration.ConnectionId, new ScimConfigurationCreateRequest() + { + UserIdAttribute = expectedScimConfiguration.UserIdAttribute, + Mapping = expectedScimConfiguration.Mapping + }); + + var createTokenRequest = new ScimTokenCreateRequest() + { + Scopes = new string[] { "openid", "offline_access" }, + TokenLifetime = 1000 + }; + + // Create two SCIM tokens and Validate + var scimTokenOne = await CreateScimTokenAndValidate(createTokenRequest); + var scimTokenTwo = await CreateScimTokenAndValidate(createTokenRequest); + + // Retrieve the token and validate + var retrievedScimTokens = + await apiClient.Connections.GetScimTokenAsync(expectedScimConfiguration.ConnectionId); + Assert.Equal(scimTokenOne.Scopes, retrievedScimTokens[0].Scopes); + Assert.Equal(scimTokenOne.TokenId, retrievedScimTokens[0].TokenId); + Assert.Equal(scimTokenOne.CreatedAt, retrievedScimTokens[0].CreatedAt); + Assert.Equal(scimTokenOne.ValidUntil, retrievedScimTokens[0].ValidUntil); + + Assert.Equal(scimTokenTwo.Scopes, retrievedScimTokens[1].Scopes); + Assert.Equal(scimTokenTwo.TokenId, retrievedScimTokens[1].TokenId); + Assert.Equal(scimTokenTwo.CreatedAt, retrievedScimTokens[1].CreatedAt); + Assert.Equal(scimTokenTwo.ValidUntil, retrievedScimTokens[1].ValidUntil); + + // Delete SCIM Token and validate + await apiClient.Connections.DeleteScimTokenAsync(expectedScimConfiguration.ConnectionId, scimTokenOne.TokenId); + await apiClient.Connections.DeleteScimTokenAsync(expectedScimConfiguration.ConnectionId, scimTokenTwo.TokenId); + var retrievedScimTokensAfterDelete = + await apiClient.Connections.GetScimTokenAsync(expectedScimConfiguration.ConnectionId); + Assert.Empty(retrievedScimTokensAfterDelete); + } + finally + { + // Clean-up + await apiClient.Connections.DeleteScimConfigurationAsync(expectedScimConfiguration.ConnectionId); + } + + async Task CreateScimTokenAndValidate(ScimTokenCreateRequest createTokenRequest) + { + var scimToken = + await apiClient.Connections.CreateScimTokenAsync(expectedScimConfiguration.ConnectionId, createTokenRequest); + Assert.NotNull(scimToken); + Assert.NotNull(scimToken.Scopes); + Assert.NotNull(scimToken.TokenId); + Assert.NotNull(scimToken.Token); + Assert.NotNull(scimToken.CreatedAt); + Assert.NotNull(scimToken.ValidUntil); + scimToken.Scopes.Should().HaveCount(2); + Assert.Equal(createTokenRequest.Scopes, scimToken.Scopes); + return scimToken; + } + } + + + private async Task GenerateBruckeManagementApiToken() + { + using var authenticationApiClient = + new TestAuthenticationApiClient(TestBaseUtils.GetVariable("BRUCKE_AUTHENTICATION_API_URL")); + // Get the access token + var token = await authenticationApiClient.GetTokenAsync(new ClientCredentialsTokenRequest + { + ClientId = TestBaseUtils.GetVariable("BRUCKE_MANAGEMENT_API_CLIENT_ID"), + ClientSecret = TestBaseUtils.GetVariable("BRUCKE_MANAGEMENT_API_CLIENT_SECRET"), + Audience = TestBaseUtils.GetVariable("BRUCKE_MANAGEMENT_API_AUDIENCE") + }); + + return token.AccessToken; + } + + private void AssertScimConfiguration(ScimConfiguration expectedScimConfiguration, + ScimConfiguration actualScimConfiguration) + { + actualScimConfiguration.Should().NotBeNull(); + Assert.Equal(expectedScimConfiguration.ConnectionId, actualScimConfiguration.ConnectionId); + Assert.Equal(expectedScimConfiguration.ConnectionName, actualScimConfiguration.ConnectionName); + Assert.Equal(expectedScimConfiguration.TenantName, actualScimConfiguration.TenantName); + Assert.Equal(expectedScimConfiguration.UserIdAttribute, actualScimConfiguration.UserIdAttribute); + Assert.Equal(expectedScimConfiguration.Strategy, actualScimConfiguration.Strategy); + Assert.NotNull(actualScimConfiguration.UpdatedOn); + Assert.NotNull(actualScimConfiguration.CreatedAt); + } } }