diff --git a/src/Auth0.ManagementApi/Clients/ActionsClient.cs b/src/Auth0.ManagementApi/Clients/ActionsClient.cs new file mode 100644 index 000000000..bf5210795 --- /dev/null +++ b/src/Auth0.ManagementApi/Clients/ActionsClient.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Auth0.ManagementApi.Models; +using Auth0.ManagementApi.Models.Actions; +using Auth0.ManagementApi.Paging; +using Newtonsoft.Json; +using Action = Auth0.ManagementApi.Models.Actions.Action; + +namespace Auth0.ManagementApi.Clients +{ + /// + /// Contains methods to access the /actions endpoints. + /// + public class ActionsClient : BaseClient + { + private const string ActionsBasePath = "actions"; + private const string ActionsPath = "actions"; + private const string TriggersPath = "triggers"; + private const string ExecutionsPath = "executions"; + private const string VersionsPath = "versions"; + private const string BindingsPath = "bindings"; + private const string DeployPath = "deploy"; + + private readonly JsonConverter[] _actionsConverters = { new PagedListConverter("actions") }; + private readonly JsonConverter[] _triggersConverters = { new ListConverter("triggers") }; + private readonly JsonConverter[] _versionsConverters = { new PagedListConverter("versions") }; + private readonly JsonConverter[] _triggerBindingsConverters = { new PagedListConverter("bindings") }; + private readonly JsonConverter[] _triggerBindingsListConverters = { new ListConverter("bindings") }; + + public ActionsClient(IManagementConnection connection, Uri baseUri, IDictionary defaultHeaders) : base(connection, baseUri, defaultHeaders) + { + } + + /// + /// Retrieve all actions. + /// + /// Specifies criteria to use when querying actions. + /// Specifies pagination info to use. + /// The cancellation token to cancel operation. + /// An containing the actions. + public Task> GetAllAsync(GetActionsRequest request, PaginationInfo pagination, CancellationToken cancellationToken = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + if (pagination == null) + throw new ArgumentNullException(nameof(pagination)); + + var queryStrings = new Dictionary + { + {"triggerId", request.TriggerId}, + {"actionName", request.ActionName}, + {"deployed", request.Deployed?.ToString()}, + {"page", pagination.PageNo.ToString()}, + {"per_page", pagination.PerPage.ToString()}, + // Uncomment below once "include_totals" is supported. + // {"include_totals", pagination.IncludeTotals.ToString()}, + {"installed", request.Installed?.ToString()}, + }; + + return Connection.GetAsync>(BuildUri($"{ActionsBasePath}/{ActionsPath}", queryStrings), DefaultHeaders, _actionsConverters, cancellationToken); + } + + /// + /// Retrieve an action by its ID. + /// + /// The ID of the action to retrieve. + /// The cancellation token to cancel operation. + /// The retrieved action. + public Task GetAsync(string id, CancellationToken cancellationToken = default) + { + return Connection.GetAsync(BuildUri($"{ActionsBasePath}/{ActionsPath}/{EncodePath(id)}"), DefaultHeaders, cancellationToken: cancellationToken); + } + + /// + /// Create an action. + /// + /// + /// Once an action is created, it must be deployed, and then bound to a trigger before it will be executed as part of a flow. + /// + /// Specifies criteria to use when creating an action. + /// The cancellation token to cancel operation. + /// The new that has been created. + public Task CreateAsync(CreateActionRequest request, CancellationToken cancellationToken = default) + { + return Connection.SendAsync(HttpMethod.Post, BuildUri($"{ActionsBasePath}/{ActionsPath}"), request, DefaultHeaders, cancellationToken: cancellationToken); + } + + /// + /// Update an existing action. + /// + /// + /// If this action is currently bound to a trigger, updating it will not affect any user flows until the action is deployed. + /// + /// The id of the action to update. + /// Specifies criteria to use when updating an action. + /// The cancellation token to cancel operation. + /// The that was updated. + public Task UpdateAsync(string id, UpdateActionRequest request, CancellationToken cancellationToken = default) + { + return Connection.SendAsync(new HttpMethod("PATCH"), BuildUri($"{ActionsBasePath}/{ActionsPath}/{EncodePath(id)}"), request, DefaultHeaders, cancellationToken: cancellationToken); + } + + /// + /// Deletes an action and all of its associated versions. + /// + /// + /// An action must be unbound from all triggers before it can be deleted. + /// + /// The ID of the action to delete. + /// Specifies criteria to use when deleting an action. + /// The cancellation token to cancel operation. + /// A that represents the asynchronous delete operation. + public Task DeleteAsync(string id, DeleteActionRequest request = null, CancellationToken cancellationToken = default) + { + return Connection.SendAsync(HttpMethod.Delete, BuildUri($"{ActionsBasePath}/{ActionsPath}/{EncodePath(id)}"), request, DefaultHeaders, cancellationToken: cancellationToken); + } + + + /// + /// Retrieve the set of triggers currently available within actions. A trigger is an extensibility point to which actions can be bound + /// + /// The cancellation token to cancel operation. + /// A list containing the triggers. + public Task> GetAllTriggersAsync(CancellationToken cancellationToken = default) + { + return Connection.GetAsync>(BuildUri($"{ActionsBasePath}/{TriggersPath}"), DefaultHeaders, _triggersConverters, cancellationToken); + } + + /// + /// Retrieve information about a specific execution of a trigger. + /// + /// + /// Relevant execution IDs will be included in tenant logs generated as part of that authentication flow. + /// Executions will only be stored for 10 days after their creation. + /// + /// The ID of the execution to retrieve. + /// The cancellation token to cancel operation. + /// The retrieved execution. + public Task GetExecutionAsync(string id, CancellationToken cancellationToken = default) + { + return Connection.GetAsync(BuildUri($"{ActionsBasePath}/{ExecutionsPath}/{EncodePath(id)}"), DefaultHeaders, cancellationToken: cancellationToken); + } + + /// + /// Retrieve all versions of an action. + /// + /// + /// An action version is created whenever an action is deployed. An action version is immutable, once created. + /// + /// The ID of the action. + /// Specifies pagination info to use. + /// The cancellation token to cancel operation. + /// The retrieved versions of the specified action. + public Task> GetAllVersionsAsync(string actionId, PaginationInfo pagination, CancellationToken cancellationToken = default) + { + var queryStrings = new Dictionary + { + {"page", pagination.PageNo.ToString()}, + {"per_page", pagination.PerPage.ToString()}, + // {"include_totals", pagination.IncludeTotals.ToString()} + }; + + return Connection.GetAsync>(BuildUri($"{ActionsBasePath}/{ActionsPath}/{EncodePath(actionId)}/{VersionsPath}", queryStrings), DefaultHeaders, _versionsConverters, cancellationToken); + } + + /// + /// Retrieve a specific version of an action. + /// + /// + /// An action version is created whenever an action is deployed. An action version is immutable, once created. + /// + /// The ID of the action. + /// The ID of the action version. + /// The cancellation token to cancel operation. + /// The retrieved version of the specified action. + public Task GetVersionAsync(string actionId, string versionId, CancellationToken cancellationToken = default) + { + return Connection.GetAsync(BuildUri($"{ActionsBasePath}/{ActionsPath}/{EncodePath(actionId)}/{VersionsPath}/{EncodePath(versionId)}"), DefaultHeaders, cancellationToken: cancellationToken); + } + + + /// + /// Retrieve the actions that are bound to a trigger. + /// + /// + /// Once an action is created and deployed, it must be attached (i.e. bound) to a trigger so that it will be executed as part of a flow. + /// The list of actions returned reflects the order in which they will be executed during the appropriate flow. + /// + /// An actions extensibility point. + /// Specifies pagination info to use. + /// The cancellation token to cancel operation. + /// The retrieved trigger bindings. + public Task> GetAllTriggerBindingsAsync(string triggerId, PaginationInfo pagination, CancellationToken cancellationToken = default) + { + var queryStrings = new Dictionary + { + {"page", pagination.PageNo.ToString()}, + {"per_page", pagination.PerPage.ToString()}, + // {"include_totals", pagination.IncludeTotals.ToString()} + }; + + return Connection.GetAsync>(BuildUri($"{ActionsBasePath}/{TriggersPath}/{EncodePath(triggerId)}/{BindingsPath}", queryStrings), DefaultHeaders, _triggerBindingsConverters, cancellationToken); + } + + /// + /// Update the actions that are bound (i.e. attached) to a trigger. + /// Once an action is created and deployed, it must be attached(i.e.bound) to a trigger so that it will be executed as part of a flow. + /// The order in which the actions are provided will determine the order in which they are executed. + /// + /// An actions extensibility point. + /// Specifies criteria to use when updating the trigger bindings. + /// The cancellation token to cancel operation. + /// The trigger bindings. + public Task> UpdateTriggerBindingsAsync(string triggerId, UpdateTriggerBindingsRequest request, CancellationToken cancellationToken = default) + { + return Connection.SendAsync>(new HttpMethod("PATCH"), BuildUri($"{ActionsBasePath}/{TriggersPath}/{EncodePath(triggerId)}/{BindingsPath}"), request, DefaultHeaders, null, _triggerBindingsListConverters, cancellationToken); + } + + /// + /// Deploy an action. + /// + /// + /// Deploying an action will create a new immutable version of the action. If the action is currently bound to a trigger, + /// then the system will begin executing the newly deployed version of the action immediately.Otherwise, the action will only be executed as a part of a flow once it is bound to that flow. + /// + /// The ID of the action to deploy. + /// The cancellation token to cancel operation. + /// The action version that was created. + public Task DeployAsync(string id, CancellationToken cancellationToken = default) + { + return Connection.SendAsync(HttpMethod.Post, BuildUri($"{ActionsBasePath}/{ActionsPath}/{EncodePath(id)}/{DeployPath}"), null, DefaultHeaders, cancellationToken: cancellationToken); + } + + /// + /// Performs the equivalent of a roll-back of an action to an earlier, specified version. + /// + /// + /// Creates a new, deployed action version that is identical to the specified version. + /// If this action is currently bound to a trigger, the system will begin executing the newly-created version immediately. + /// + /// The ID of the action. + /// The ID of the version to deploy. + /// The cancellation token to cancel operation. + /// A that represents the asynchronous delete operation. + public Task RollbackToVersionAsync(string actionId, string versionId, CancellationToken cancellationToken = default) + { + return Connection.SendAsync(HttpMethod.Post, BuildUri($"{ActionsBasePath}/{ActionsPath}/{EncodePath(actionId)}/versions/{versionId}/deploy"), new {}, DefaultHeaders, cancellationToken: cancellationToken); + } + } +} diff --git a/src/Auth0.ManagementApi/HttpClientManagementConnection.cs b/src/Auth0.ManagementApi/HttpClientManagementConnection.cs index bb7a7cf38..97476622e 100644 --- a/src/Auth0.ManagementApi/HttpClientManagementConnection.cs +++ b/src/Auth0.ManagementApi/HttpClientManagementConnection.cs @@ -62,9 +62,9 @@ public async Task GetAsync(Uri uri, IDictionary headers, J } /// - public async Task SendAsync(HttpMethod method, Uri uri, object body, IDictionary headers, IList files = null, CancellationToken cancellationToken = default) + public async Task SendAsync(HttpMethod method, Uri uri, object body, IDictionary headers, IList files = null, JsonConverter[] converters = null, CancellationToken cancellationToken = default) { - return await Retry(async () => await SendAsyncInternal(method, uri, body, headers, files, cancellationToken)).ConfigureAwait(false); + return await Retry(async () => await SendAsyncInternal(method, uri, body, headers, files, converters, cancellationToken)).ConfigureAwait(false); } private async Task GetAsyncInternal(Uri uri, IDictionary headers, JsonConverter[] converters = null, CancellationToken cancellationToken = default) @@ -76,12 +76,12 @@ private async Task GetAsyncInternal(Uri uri, IDictionary h } } - private async Task SendAsyncInternal(HttpMethod method, Uri uri, object body, IDictionary headers, IList files = null, CancellationToken cancellationToken = default) + private async Task SendAsyncInternal(HttpMethod method, Uri uri, object body, IDictionary headers, IList files = null, JsonConverter[] converters = null, CancellationToken cancellationToken = default) { using (var request = new HttpRequestMessage(method, uri) { Content = BuildMessageContent(body, files) }) { ApplyHeaders(request.Headers, headers); - return await SendRequest(request, cancellationToken: cancellationToken).ConfigureAwait(false); + return await SendRequest(request, converters, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Auth0.ManagementApi/IManagementConnection.cs b/src/Auth0.ManagementApi/IManagementConnection.cs index 37e86eeda..f0b12731b 100644 --- a/src/Auth0.ManagementApi/IManagementConnection.cs +++ b/src/Auth0.ManagementApi/IManagementConnection.cs @@ -41,11 +41,12 @@ public interface IManagementConnection /// otherwise containing the JSON representation of the object is expected. /// Dictionary containing additional headers that may override the defaults. /// Optional containing file contents to upload as a post. + /// Optional array of s used to deserialize the resulting . /// The cancellation token to cancel operation. /// representing the async operation containing response body as . /// /// can only be specified if is a Dictionary%lt;string, object%gt;"/>. /// - Task SendAsync(HttpMethod method, Uri uri, object body, IDictionary headers, IList files = null, CancellationToken cancellationToken = default); + Task SendAsync(HttpMethod method, Uri uri, object body, IDictionary headers, IList files = null, JsonConverter[] converters = null, CancellationToken cancellationToken = default); } } diff --git a/src/Auth0.ManagementApi/ListConverter.cs b/src/Auth0.ManagementApi/ListConverter.cs new file mode 100644 index 000000000..a6ea51e47 --- /dev/null +++ b/src/Auth0.ManagementApi/ListConverter.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Auth0.ManagementApi.Paging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Auth0.ManagementApi +{ + internal class ListConverter : JsonConverter + { + private readonly string _collectionFieldName; + + public ListConverter(string collectionFieldName) + { + _collectionFieldName = collectionFieldName; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + public override bool CanConvert(Type objectType) + { + return typeof(IList).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, + JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.StartObject) + { + JObject item = JObject.Load(reader); + + if (item[_collectionFieldName] != null) + { + return item[_collectionFieldName].ToObject>(serializer); + } + } + else + { + JArray array = JArray.Load(reader); + + var collection = array.ToObject>(); + + return new PagedList(collection); + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/ManagementApiClient.cs b/src/Auth0.ManagementApi/ManagementApiClient.cs index 7b70ab5f3..05fa064c9 100644 --- a/src/Auth0.ManagementApi/ManagementApiClient.cs +++ b/src/Auth0.ManagementApi/ManagementApiClient.cs @@ -16,6 +16,11 @@ public class ManagementApiClient : IDisposable protected readonly IManagementConnection connection; IDisposable connectionToDispose; + /// + /// Contains all the methods to call the /actions endpoints. + /// + public ActionsClient Actions { get; } + /// /// Contains all the methods to call the /blacklists/tokens endpoints. /// @@ -151,6 +156,7 @@ public ManagementApiClient(string token, Uri baseUri, IManagementConnection mana var defaultHeaders = CreateDefaultHeaders(token); + Actions = new ActionsClient(managementConnection, baseUri, defaultHeaders); BlacklistedTokens = new BlacklistedTokensClient(managementConnection, baseUri, defaultHeaders); Branding = new BrandingClient(managementConnection, baseUri, defaultHeaders); ClientGrants = new ClientGrantsClient(managementConnection, baseUri, defaultHeaders); diff --git a/src/Auth0.ManagementApi/Models/Actions/Action.cs b/src/Auth0.ManagementApi/Models/Actions/Action.cs new file mode 100644 index 000000000..bbaa8141e --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Actions/Action.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models.Actions +{ + /// + /// Represents an action in Auth0 + /// + public class Action : ActionBase + { + /// + /// The unique ID of the action. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// The build status of this action. + /// + [JsonProperty("status")] + public string Status { get; set; } + + /// + /// The version of the action that is currently deployed. + /// + [JsonProperty("deployed_version")] + public ActionVersion DeployedVersion { get; set; } + + /// + /// True if all of an Action's contents have been deployed. + /// + [JsonProperty("all_changes_deployed")] + public bool AllChangesDeployed { get; set; } + + /// + /// The time when this action was created. + /// + [JsonProperty("created_at")] + public DateTime CreatedAt { get; set; } + + /// + /// The time when this action was updated. + /// + [JsonProperty("updated_at")] + public DateTime UpdatedAt { get; set; } + + /// + /// The list of triggers that this action supports. + /// + [JsonProperty("supported_triggers")] + public IList SupportedTriggers { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Actions/ActionBase.cs b/src/Auth0.ManagementApi/Models/Actions/ActionBase.cs new file mode 100644 index 000000000..6f2fc0c4a --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Actions/ActionBase.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models.Actions +{ + public abstract class ActionBase + { + /// + /// The name of an action. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// The source code of the action. + /// + [JsonProperty("code")] + public string Code { get; set; } + + /// + /// The list of third party npm modules, and their versions, that this action depends on. + /// + [JsonProperty("dependencies")] + public IList Dependencies { get; set; } + + /// + /// The Node runtime. For example: node12, defaults to node12 + /// + [JsonProperty("runtime")] + public string Runtime { get; set; } + + /// + /// The list of secrets that are included in an action or a version of an action. + /// + [JsonProperty("secrets")] + public IList Secrets { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Actions/ActionDependency.cs b/src/Auth0.ManagementApi/Models/Actions/ActionDependency.cs new file mode 100644 index 000000000..66cace207 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Actions/ActionDependency.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models.Actions +{ + /// + /// Represent an npm dependency for an action or an action's version. + /// + public class ActionDependency + { + /// + /// The name of the npm module, e.g. 'lodash' + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// The version of the npm module, e.g. '4.17.1' + /// + [JsonProperty("version")] + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Actions/ActionError.cs b/src/Auth0.ManagementApi/Models/Actions/ActionError.cs new file mode 100644 index 000000000..db4db0889 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Actions/ActionError.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models.Actions +{ + public class ActionError + { + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("msg")] + public string Message { get; set; } + + [JsonProperty("url")] + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Actions/ActionExecution.cs b/src/Auth0.ManagementApi/Models/Actions/ActionExecution.cs new file mode 100644 index 000000000..a0a47f390 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Actions/ActionExecution.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models.Actions +{ + public class ActionExecution + { + /// + /// Identifies a specific execution. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// The actions extensibility point. + /// + [JsonProperty("trigger_id")] + public string TriggerId { get; set; } + + /// + /// The overall status of an execution. + /// + [JsonProperty("status")] + public string Status { get; set; } + + /// + /// Captures the results of a single action being executed. + /// + [JsonProperty("results")] + public IList Results { get; set; } + + /// + /// The time that the execution was started. + /// + [JsonProperty("created_at")] + public DateTime CreatedAt { get; set; } + + /// + /// The time that the execution finished executing. + /// + [JsonProperty("updated_at")] + public DateTime UpdatedAt { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Actions/ActionExecutionResult.cs b/src/Auth0.ManagementApi/Models/Actions/ActionExecutionResult.cs new file mode 100644 index 000000000..95041112b --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Actions/ActionExecutionResult.cs @@ -0,0 +1,29 @@ +using System; +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models.Actions +{ + /// + /// Captures the results of a single action being executed. + /// + public class ActionExecutionResult + { + /// + /// The name of the action that was executed. + /// + [JsonProperty("action_name")] + public string ActionName { get; set; } + + /// + /// The time when the action was started. + /// + [JsonProperty("started_at")] + public DateTime StartedAt { get; set; } + + /// + /// The time when the action finished executing. + /// + [JsonProperty("ended_at")] + public DateTime EndedAt { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Actions/ActionSecret.cs b/src/Auth0.ManagementApi/Models/Actions/ActionSecret.cs new file mode 100644 index 000000000..2662567fb --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Actions/ActionSecret.cs @@ -0,0 +1,26 @@ +using System; +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models.Actions +{ + public class ActionSecret + { + /// + /// The name of the particular secret, e.g. API_KEY. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// The time when the secret was last updated. + /// + [JsonProperty("updated_at")] + public DateTime UpdatedAt { get; private set; } + + /// + /// The value of the particular secret, e.g. secret123. A secret's value can only be set upon creation. A secret's value will never be returned by the API. + /// + [JsonProperty("value")] + public string Value { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Actions/ActionSupportedTrigger.cs b/src/Auth0.ManagementApi/Models/Actions/ActionSupportedTrigger.cs new file mode 100644 index 000000000..a96370448 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Actions/ActionSupportedTrigger.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models.Actions +{ + public class ActionSupportedTrigger + { + /// + /// The actions extensibility point + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// The version of a trigger. v1, v2, etc. + /// + [JsonProperty("version")] + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Actions/ActionVersion.cs b/src/Auth0.ManagementApi/Models/Actions/ActionVersion.cs new file mode 100644 index 000000000..012ba8189 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Actions/ActionVersion.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models.Actions +{ + /// + /// Represents a version for an action in Auth0 + /// + public class ActionVersion + { + /// + /// The unique id of an action version. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// The source code of this specific version of the action. + /// + [JsonProperty("code")] + public string Code { get; set; } + + /// + /// The Node runtime. For example: `node12` + /// + [JsonProperty("runtime")] + public string Runtime { get; set; } + + /// + /// The index of this version in list of versions for the action. + /// + [JsonProperty("number")] + public int Number { get; set; } + + /// + /// Indicates if this specific version is the currently one deployed. + /// + [JsonProperty("deployed")] + public bool? Deployed { get; set; } + + /// + /// The list of third party npm modules, and their versions, that this specific version depends on. + /// + [JsonProperty("dependencies")] + public IList Dependencies { get; set; } + + /// + /// The build status of this specific version. + /// + [JsonProperty("status")] + public string Status { get; set; } + + /// + /// The time when this version was created. + /// + [JsonProperty("created_at")] + public DateTime CreatedAt { get; set; } + + /// + /// The time when a version was updated. Versions are never updated externally. Only Auth0 will update an action version as it is being built. + /// + [JsonProperty("updated_at")] + public DateTime UpdatedAt { get; set; } + + /// + /// The action to which this version belongs. + /// + [JsonProperty("action")] + public Action Action { get; set; } + + /// + /// The list of secrets that are included in the version. + /// + [JsonProperty("secrets")] + public IList Secrets { get; set; } + + /// + /// Any errors that occurred while the version was being built. + /// + [JsonProperty("errors")] + public IList Errors { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Actions/CreateActionRequest.cs b/src/Auth0.ManagementApi/Models/Actions/CreateActionRequest.cs new file mode 100644 index 000000000..04baa1d22 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Actions/CreateActionRequest.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models.Actions +{ + /// + /// Request configuration for creating an action. + /// + public class CreateActionRequest : ActionBase + { + /// + /// The list of triggers that this action supports. At this time, an action can only target a single trigger at a time. + /// + [JsonProperty("supported_triggers")] + public IList SupportedTriggers { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Actions/DeleteActionRequest.cs b/src/Auth0.ManagementApi/Models/Actions/DeleteActionRequest.cs new file mode 100644 index 000000000..40c762e0e --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Actions/DeleteActionRequest.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models.Actions +{ + /// + /// Request configuration for deleting an action. + /// + public class DeleteActionRequest + { + /// + /// Force action deletion detaching bindings + /// + [JsonProperty("force")] + public bool? Force { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Actions/GetActionsRequest.cs b/src/Auth0.ManagementApi/Models/Actions/GetActionsRequest.cs new file mode 100644 index 000000000..8ff59ad22 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Actions/GetActionsRequest.cs @@ -0,0 +1,28 @@ +namespace Auth0.ManagementApi.Models.Actions +{ + /// + /// Request configuration for retrieving all actions. + /// + public class GetActionsRequest + { + /// + /// An actions extensibility point. + /// + public string TriggerId { get; set; } + + /// + /// The name of the action to retrieve. + /// + public string ActionName { get; set; } + + /// + /// Optional filter to only retrieve actions that are deployed. + /// + public bool? Deployed { get; set; } + + /// + /// Optional. When true, return only installed actions. When false, return only custom actions. Returns all actions by default. + /// + public bool? Installed { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Actions/Trigger.cs b/src/Auth0.ManagementApi/Models/Actions/Trigger.cs new file mode 100644 index 000000000..2b68c4c81 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Actions/Trigger.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models.Actions +{ + /// + /// Represents a Trigger in Auth0 + /// + public class Trigger + { + /// + /// The actions extensibility point. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// Runtimes supported by this trigger. + /// + [JsonProperty("runtimes")] + public IList Runtimes { get; set; } + + /// + /// Runtime that will be used when none is specified when creating an action. + /// + [JsonProperty("default_runtime")] + public string DefaultRuntime { get; set; } + + /// + /// The version of a trigger. v1, v2, etc. + /// + [JsonProperty("version")] + public string Version { get; set; } + + /// + /// The trigger's status. + /// + [JsonProperty("status")] + public string Status { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Actions/TriggerBinding.cs b/src/Auth0.ManagementApi/Models/Actions/TriggerBinding.cs new file mode 100644 index 000000000..8c6db023e --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Actions/TriggerBinding.cs @@ -0,0 +1,47 @@ +using System; +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models.Actions +{ + /// + /// Represents a Trigger Binding in Auth0 + /// + public class TriggerBinding + { + /// + /// The unique ID of this binding. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// The actions extensibility point. + /// + [JsonProperty("trigger_id")] + public string TriggerId { get; set; } + + /// + /// The connected action. + /// + [JsonProperty("action")] + public Action Action { get; set; } + + /// + /// The time when the binding was created. + /// + [JsonProperty("created_at")] + public DateTime CreatedAt { get; set; } + + /// + /// The time when the binding was updated. + /// + [JsonProperty("updated_at")] + public DateTime UpdatedAt { get; set; } + + /// + /// The name of the binding. + /// + [JsonProperty("display_name")] + public string DisplayName { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Actions/UpdateActionRequest.cs b/src/Auth0.ManagementApi/Models/Actions/UpdateActionRequest.cs new file mode 100644 index 000000000..dd83599a3 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Actions/UpdateActionRequest.cs @@ -0,0 +1,10 @@ +namespace Auth0.ManagementApi.Models.Actions +{ + /// + /// Request configuration for updating an action. + /// + public class UpdateActionRequest: ActionBase + { + + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Actions/UpdateTriggerBindingEntry.cs b/src/Auth0.ManagementApi/Models/Actions/UpdateTriggerBindingEntry.cs new file mode 100644 index 000000000..fca875801 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Actions/UpdateTriggerBindingEntry.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models.Actions +{ + public class UpdateTriggerBindingEntry + { + public class BindingRef + { + /// + /// How the action is being referred to: `action_id` or `action_name`. + /// + [JsonProperty("type")] + public string Type { get; set; } + + /// + /// The id or name of an action that is being bound to a trigger. + /// + [JsonProperty("value")] + public string Value { get; set; } + } + + /// + /// A reference to an action. An action can be referred to by ID or by Name. + /// + [JsonProperty("ref")] + public BindingRef Ref { get; set; } + + /// + /// The name of the binding. + /// + [JsonProperty("display_name")] + public string DisplayName { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Actions/UpdateTriggerBindingsRequest.cs b/src/Auth0.ManagementApi/Models/Actions/UpdateTriggerBindingsRequest.cs new file mode 100644 index 000000000..8efc71038 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Actions/UpdateTriggerBindingsRequest.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models.Actions +{ + /// + /// Request configuration to update the actions that are bound (i.e. attached) to a trigger. + /// + public class UpdateTriggerBindingsRequest + { + /// + /// The actions that will be bound to this trigger. The order in which they are included will be the order in which they are executed. + /// + [JsonProperty("bindings")] + public IList Bindings { get; set; } + } +} \ No newline at end of file diff --git a/tests/Auth0.ManagementApi.IntegrationTests/ActionsTests.cs b/tests/Auth0.ManagementApi.IntegrationTests/ActionsTests.cs new file mode 100644 index 000000000..cb1a16335 --- /dev/null +++ b/tests/Auth0.ManagementApi.IntegrationTests/ActionsTests.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Auth0.ManagementApi.Models.Actions; +using Auth0.ManagementApi.Paging; +using Auth0.Tests.Shared; +using FluentAssertions; +using Xunit; + +namespace Auth0.ManagementApi.IntegrationTests +{ + public class ActionsTests : TestBase, IAsyncLifetime + { + private ManagementApiClient _apiClient; + public async Task InitializeAsync() + { + string token = await GenerateManagementApiToken(); + + _apiClient = new ManagementApiClient(token, GetVariable("AUTH0_MANAGEMENT_API_URL"), new HttpClientManagementConnection(options: new HttpClientManagementConnectionOptions { NumberOfHttpRetries = 9 })); + } + + public Task DisposeAsync() + { + _apiClient.Dispose(); + return Task.CompletedTask; + } + + [Fact] + public async Task Test_actions_crud_sequence() + { + var actionsBeforeCreate = await _apiClient.Actions.GetAllAsync(new GetActionsRequest(), new PaginationInfo()); + + var createdAction = await _apiClient.Actions.CreateAsync(new CreateActionRequest + { + Name = $"Int-Test-Action-{Guid.NewGuid()}", + Code = "module.exports = () => {}", + Runtime = "node12", + Secrets = new List { new ActionSecret { Name = "My_Secret", Value = "Test123" } }, + SupportedTriggers = new List { new ActionSupportedTrigger { Id = "post-login", Version = "v2"} } + }); + + var actionsAfterCreate = await _apiClient.Actions.GetAllAsync(new GetActionsRequest(), new PaginationInfo()); + + actionsAfterCreate.Count.Should().Be(actionsBeforeCreate.Count + 1); + createdAction.Should().BeEquivalentTo(actionsAfterCreate.Last(), options => options.Excluding(o => o.Status)); + + var updatedAction = await _apiClient.Actions.UpdateAsync(createdAction.Id, new UpdateActionRequest + { + Code = "module.exports = () => { console.log(true); }" + }); + + updatedAction.Should().BeEquivalentTo(createdAction, options => options.Excluding(o => o.Code).Excluding(o => o.UpdatedAt)); + updatedAction.Code.Should().Be("module.exports = () => { console.log(true); }"); + + var actionAfterUpdate = await _apiClient.Actions.GetAsync(updatedAction.Id); + + updatedAction.Should().BeEquivalentTo(actionAfterUpdate, options => options.Excluding(o => o.Status)); + actionAfterUpdate.Code.Should().Be("module.exports = () => { console.log(true); }"); + + await _apiClient.Actions.DeleteAsync(actionAfterUpdate.Id); + + var actionsAfterDelete = await _apiClient.Actions.GetAllAsync(new GetActionsRequest(), new PaginationInfo()); + actionsAfterDelete.Count.Should().Be(actionsBeforeCreate.Count); + } + + [Fact] + public async Task Test_get_triggers() + { + var triggers = await _apiClient.Actions.GetAllTriggersAsync(); + + triggers.Should().NotBeEmpty(); + } + + [Fact] + public async Task Test_get_and_update_trigger_bindings() + { + var triggerBindingsBeforeCreate = await _apiClient.Actions.GetAllTriggerBindingsAsync("post-login", new PaginationInfo()); + + var createdAction = await _apiClient.Actions.CreateAsync(new CreateActionRequest + { + Name = $"Int-Test-Action-{Guid.NewGuid()}", + Code = "module.exports = () => {}", + Runtime = "node12", + Secrets = new List { new ActionSecret { Name = "My_Secret", Value = "Test123" } }, + SupportedTriggers = new List { new ActionSupportedTrigger { Id = "post-login", Version = "v2" } } + }); + + await _apiClient.Actions.DeployAsync(createdAction.Id); + + await _apiClient.Actions.UpdateTriggerBindingsAsync("post-login", new UpdateTriggerBindingsRequest + { + Bindings = new List + { + new UpdateTriggerBindingEntry + { + Ref = new UpdateTriggerBindingEntry.BindingRef + { + Type = "action_id", + Value = createdAction.Id + }, + DisplayName = "My Action" + } + } + }); + + var triggerBindingsAfterCreate = await _apiClient.Actions.GetAllTriggerBindingsAsync("post-login", new PaginationInfo()); + + triggerBindingsAfterCreate.Count.Should().Be(triggerBindingsBeforeCreate.Count + 1); + + await _apiClient.Actions.UpdateTriggerBindingsAsync("post-login", new UpdateTriggerBindingsRequest + { + Bindings = new List() + }); + + await _apiClient.Actions.DeleteAsync(createdAction.Id); + } + + [Fact] + public async Task Test_action_version_crud_sequence() + { + // 1. Create a new Action + var createdAction = await _apiClient.Actions.CreateAsync(new CreateActionRequest + { + Name = $"Int-Test-Action-{Guid.NewGuid()}", + Code = "module.exports = () => {}", + Runtime = "node12", + Secrets = new List { new ActionSecret { Name = "My_Secret", Value = "Test123" } }, + SupportedTriggers = new List { new ActionSupportedTrigger { Id = "post-login", Version = "v2" } } + }); + + // 2. Get all the versions after the action was created + var versionsAfterCreate = await _apiClient.Actions.GetAllVersionsAsync(createdAction.Id, new PaginationInfo()); + + versionsAfterCreate.Count.Should().Be(0); + + // 3. Deploy the current version + await _apiClient.Actions.DeployAsync(createdAction.Id); + + // 4. Get all the versions after the action was deployed + var versionsAfterDeploy = await _apiClient.Actions.GetAllVersionsAsync(createdAction.Id, new PaginationInfo()); + + versionsAfterDeploy.Count.Should().Be(1); + + // 5. Update the action + await _apiClient.Actions.UpdateAsync(createdAction.Id, new UpdateActionRequest + { + Code = "module.exports = () => { console.log(true); }" + }); + + // 6. Deploy the latest version + var deployedVersion = await _apiClient.Actions.DeployAsync(createdAction.Id); + + // 7. Get all the versions after the action was updated + var versionsAfterSecondDeploy = await _apiClient.Actions.GetAllVersionsAsync(createdAction.Id, new PaginationInfo()); + + versionsAfterSecondDeploy.Count.Should().Be(2); + versionsAfterSecondDeploy.Single(v => v.Id == deployedVersion.Id).Deployed.Should().BeTrue(); + versionsAfterSecondDeploy.Single(v => v.Id != deployedVersion.Id).Deployed.Should().BeFalse(); + + var action = await _apiClient.Actions.GetAsync(createdAction.Id); + action.DeployedVersion.Id.Should().Be(deployedVersion.Id); + + // 9. Rollback + var rollbackedVersion = await _apiClient.Actions.RollbackToVersionAsync(createdAction.Id, versionsAfterDeploy.Single().Id); + + // 10. Get all the versions after the action was rollbacked + var versionsAfterRollback = await _apiClient.Actions.GetAllVersionsAsync(createdAction.Id, new PaginationInfo()); + var versionAfterRollback = await _apiClient.Actions.GetVersionAsync(createdAction.Id, rollbackedVersion.Id); + + versionsAfterRollback.Count.Should().Be(3); + versionsAfterRollback.Single(v => v.Id == versionAfterRollback.Id).Should().BeEquivalentTo(versionAfterRollback); + versionsAfterRollback.Single(v => v.Id == versionAfterRollback.Id).Deployed.Should().BeTrue(); + versionsAfterRollback.Where(v => v.Id != versionAfterRollback.Id).ToList().ForEach(v => v.Deployed.Should().BeFalse()); + + // 10. Delete Action + await _apiClient.Actions.DeleteAsync(createdAction.Id); + } + } +} diff --git a/tests/Auth0.ManagementApi.IntegrationTests/ManagementApiClientTests.cs b/tests/Auth0.ManagementApi.IntegrationTests/ManagementApiClientTests.cs index e0a086480..40bfa1018 100644 --- a/tests/Auth0.ManagementApi.IntegrationTests/ManagementApiClientTests.cs +++ b/tests/Auth0.ManagementApi.IntegrationTests/ManagementApiClientTests.cs @@ -57,7 +57,7 @@ public Task GetAsync(Uri uri, IDictionary headers = null, return Task.FromResult(default(T)); } - public Task SendAsync(HttpMethod method, Uri uri, object body, IDictionary headers = null, IList files = null, CancellationToken cancellationToken = default) + public Task SendAsync(HttpMethod method, Uri uri, object body, IDictionary headers = null, IList files = null, JsonConverter[] converters = null, CancellationToken cancellationToken = default) { return Task.FromResult(default(T)); } @@ -104,7 +104,7 @@ public Task GetAsync(Uri uri, IDictionary headers = null, return Task.FromResult(default(T)); } - public Task SendAsync(HttpMethod method, Uri uri, object body, IDictionary headers = null, IList files = null, CancellationToken cancellationToken = default) + public Task SendAsync(HttpMethod method, Uri uri, object body, IDictionary headers = null, IList files = null, JsonConverter[] converters = null, CancellationToken cancellationToken = default) { LastHeaders = headers; return Task.FromResult(default(T));