diff --git a/src/PSInfisicalAPI.Tests/EndpointRegistryTests.cs b/src/PSInfisicalAPI.Tests/EndpointRegistryTests.cs index ccd6d61..1edada8 100644 --- a/src/PSInfisicalAPI.Tests/EndpointRegistryTests.cs +++ b/src/PSInfisicalAPI.Tests/EndpointRegistryTests.cs @@ -45,5 +45,39 @@ namespace PSInfisicalAPI.Tests { Assert.Throws(() => InfisicalEndpointRegistry.Get("NotARealEndpoint")); } + + [Theory] + [InlineData(InfisicalEndpointNames.CreateSecret, "POST", "/api/v3/secrets/raw/{secretName}")] + [InlineData(InfisicalEndpointNames.UpdateSecret, "PATCH", "/api/v3/secrets/raw/{secretName}")] + [InlineData(InfisicalEndpointNames.DeleteSecret, "DELETE", "/api/v3/secrets/raw/{secretName}")] + [InlineData(InfisicalEndpointNames.ListProjects, "GET", "/api/v1/workspace")] + [InlineData(InfisicalEndpointNames.RetrieveProject, "GET", "/api/v1/workspace/{projectId}")] + [InlineData(InfisicalEndpointNames.CreateProject, "POST", "/api/v2/workspace")] + [InlineData(InfisicalEndpointNames.UpdateProject, "PATCH", "/api/v1/workspace/{projectId}")] + [InlineData(InfisicalEndpointNames.DeleteProject, "DELETE", "/api/v1/workspace/{projectId}")] + [InlineData(InfisicalEndpointNames.CreateEnvironment, "POST", "/api/v1/workspace/{projectId}/environments")] + [InlineData(InfisicalEndpointNames.UpdateEnvironment, "PATCH", "/api/v1/workspace/{projectId}/environments/{environmentId}")] + [InlineData(InfisicalEndpointNames.DeleteEnvironment, "DELETE", "/api/v1/workspace/{projectId}/environments/{environmentId}")] + [InlineData(InfisicalEndpointNames.ListFolders, "GET", "/api/v1/folders")] + [InlineData(InfisicalEndpointNames.CreateFolder, "POST", "/api/v1/folders")] + [InlineData(InfisicalEndpointNames.UpdateFolder, "PATCH", "/api/v1/folders/{folderId}")] + [InlineData(InfisicalEndpointNames.DeleteFolder, "DELETE", "/api/v1/folders/{folderId}")] + [InlineData(InfisicalEndpointNames.ListTags, "GET", "/api/v1/workspace/{projectId}/tags")] + [InlineData(InfisicalEndpointNames.CreateTag, "POST", "/api/v1/workspace/{projectId}/tags")] + [InlineData(InfisicalEndpointNames.UpdateTag, "PATCH", "/api/v1/workspace/{projectId}/tags/{tagId}")] + [InlineData(InfisicalEndpointNames.DeleteTag, "DELETE", "/api/v1/workspace/{projectId}/tags/{tagId}")] + [InlineData(InfisicalEndpointNames.JwtAuthLogin, "POST", "/api/v1/auth/jwt-auth/login")] + [InlineData(InfisicalEndpointNames.OidcAuthLogin, "POST", "/api/v1/auth/oidc-auth/login")] + [InlineData(InfisicalEndpointNames.LdapAuthLogin, "POST", "/api/v1/auth/ldap-auth/login")] + [InlineData(InfisicalEndpointNames.KubernetesAuthLogin, "POST", "/api/v1/auth/kubernetes-auth/login")] + [InlineData(InfisicalEndpointNames.AwsAuthLogin, "POST", "/api/v1/auth/aws-auth/login")] + [InlineData(InfisicalEndpointNames.AzureAuthLogin, "POST", "/api/v1/auth/azure-auth/login")] + [InlineData(InfisicalEndpointNames.GcpIamAuthLogin, "POST", "/api/v1/auth/gcp-auth/login")] + public void Registered_Endpoints_Have_Expected_Shape(string name, string method, string template) + { + InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(name); + Assert.Equal(method, definition.Method); + Assert.Equal(template, definition.Template); + } } } diff --git a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs index 1bea19d..2d331ba 100644 --- a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs +++ b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs @@ -3,7 +3,43 @@ namespace PSInfisicalAPI.Endpoints public static class InfisicalEndpointNames { public const string UniversalAuthLogin = "UniversalAuthLogin"; + public const string TokenAuthLogin = "TokenAuthLogin"; + public const string JwtAuthLogin = "JwtAuthLogin"; + public const string OidcAuthLogin = "OidcAuthLogin"; + public const string LdapAuthLogin = "LdapAuthLogin"; + public const string KubernetesAuthLogin = "KubernetesAuthLogin"; + public const string AwsAuthLogin = "AwsAuthLogin"; + public const string AzureAuthLogin = "AzureAuthLogin"; + public const string GcpIamAuthLogin = "GcpIamAuthLogin"; + public const string ListSecrets = "ListSecrets"; public const string RetrieveSecret = "RetrieveSecret"; + public const string CreateSecret = "CreateSecret"; + public const string UpdateSecret = "UpdateSecret"; + public const string DeleteSecret = "DeleteSecret"; + + public const string ListProjects = "ListProjects"; + public const string RetrieveProject = "RetrieveProject"; + public const string CreateProject = "CreateProject"; + public const string UpdateProject = "UpdateProject"; + public const string DeleteProject = "DeleteProject"; + + public const string ListEnvironments = "ListEnvironments"; + public const string RetrieveEnvironment = "RetrieveEnvironment"; + public const string CreateEnvironment = "CreateEnvironment"; + public const string UpdateEnvironment = "UpdateEnvironment"; + public const string DeleteEnvironment = "DeleteEnvironment"; + + public const string ListFolders = "ListFolders"; + public const string RetrieveFolder = "RetrieveFolder"; + public const string CreateFolder = "CreateFolder"; + public const string UpdateFolder = "UpdateFolder"; + public const string DeleteFolder = "DeleteFolder"; + + public const string ListTags = "ListTags"; + public const string RetrieveTag = "RetrieveTag"; + public const string CreateTag = "CreateTag"; + public const string UpdateTag = "UpdateTag"; + public const string DeleteTag = "DeleteTag"; } } diff --git a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs index 997d14f..1ccf72a 100644 --- a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs +++ b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs @@ -5,83 +5,435 @@ namespace PSInfisicalAPI.Endpoints { public static class InfisicalEndpointRegistry { - private static readonly Dictionary> Candidates = - new Dictionary> + private static readonly Dictionary> Candidates; + + static InfisicalEndpointRegistry() + { + Candidates = new Dictionary>(); + RegisterAuthentication(Candidates); + RegisterSecrets(Candidates); + RegisterProjects(Candidates); + RegisterEnvironments(Candidates); + RegisterFolders(Candidates); + RegisterTags(Candidates); + } + + private static void Add(Dictionary> map, InfisicalEndpointDefinition definition) + { + List list; + if (!map.TryGetValue(definition.Name, out list)) { - { - InfisicalEndpointNames.UniversalAuthLogin, - new List - { - new InfisicalEndpointDefinition - { - Name = InfisicalEndpointNames.UniversalAuthLogin, - Resource = "Authentication", - Version = "v1", - Method = "POST", - Template = "/api/v1/auth/universal-auth/login", - RequiresAuthorization = false, - ContainsSecretMaterialInRequest = true, - ContainsSecretMaterialInResponse = true - } - } - }, - { - InfisicalEndpointNames.ListSecrets, - new List - { - new InfisicalEndpointDefinition - { - Name = InfisicalEndpointNames.ListSecrets, - Resource = "Secrets", - Version = "v4", - Method = "GET", - Template = "/api/v4/secrets", - RequiresAuthorization = true, - ContainsSecretMaterialInRequest = false, - ContainsSecretMaterialInResponse = true - }, - new InfisicalEndpointDefinition - { - Name = InfisicalEndpointNames.ListSecrets, - Resource = "Secrets", - Version = "v3", - Method = "GET", - Template = "/api/v3/secrets/raw", - RequiresAuthorization = true, - ContainsSecretMaterialInRequest = false, - ContainsSecretMaterialInResponse = true - } - } - }, - { - InfisicalEndpointNames.RetrieveSecret, - new List - { - new InfisicalEndpointDefinition - { - Name = InfisicalEndpointNames.RetrieveSecret, - Resource = "Secrets", - Version = "v4", - Method = "GET", - Template = "/api/v4/secrets/{secretName}", - RequiresAuthorization = true, - ContainsSecretMaterialInRequest = false, - ContainsSecretMaterialInResponse = true - }, - new InfisicalEndpointDefinition - { - Name = InfisicalEndpointNames.RetrieveSecret, - Resource = "Secrets", - Version = "v3", - Method = "GET", - Template = "/api/v3/secrets/raw/{secretName}", - RequiresAuthorization = true, - ContainsSecretMaterialInRequest = false, - ContainsSecretMaterialInResponse = true - } - } - } - }; + list = new List(); + map[definition.Name] = list; + } + + list.Add(definition); + } + + private static void RegisterAuthentication(Dictionary> map) + { + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.UniversalAuthLogin, + Resource = "Authentication", + Version = "v1", + Method = "POST", + Template = "/api/v1/auth/universal-auth/login", + RequiresAuthorization = false, + ContainsSecretMaterialInRequest = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.TokenAuthLogin, + Resource = "Authentication", + Version = "v1", + Method = "POST", + Template = "/api/v1/auth/token-auth/login", + RequiresAuthorization = false, + ContainsSecretMaterialInRequest = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.JwtAuthLogin, + Resource = "Authentication", + Version = "v1", + Method = "POST", + Template = "/api/v1/auth/jwt-auth/login", + RequiresAuthorization = false, + ContainsSecretMaterialInRequest = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.OidcAuthLogin, + Resource = "Authentication", + Version = "v1", + Method = "POST", + Template = "/api/v1/auth/oidc-auth/login", + RequiresAuthorization = false, + ContainsSecretMaterialInRequest = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.LdapAuthLogin, + Resource = "Authentication", + Version = "v1", + Method = "POST", + Template = "/api/v1/auth/ldap-auth/login", + RequiresAuthorization = false, + ContainsSecretMaterialInRequest = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.KubernetesAuthLogin, + Resource = "Authentication", + Version = "v1", + Method = "POST", + Template = "/api/v1/auth/kubernetes-auth/login", + RequiresAuthorization = false, + ContainsSecretMaterialInRequest = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.AwsAuthLogin, + Resource = "Authentication", + Version = "v1", + Method = "POST", + Template = "/api/v1/auth/aws-auth/login", + RequiresAuthorization = false, + ContainsSecretMaterialInRequest = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.AzureAuthLogin, + Resource = "Authentication", + Version = "v1", + Method = "POST", + Template = "/api/v1/auth/azure-auth/login", + RequiresAuthorization = false, + ContainsSecretMaterialInRequest = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.GcpIamAuthLogin, + Resource = "Authentication", + Version = "v1", + Method = "POST", + Template = "/api/v1/auth/gcp-auth/login", + RequiresAuthorization = false, + ContainsSecretMaterialInRequest = true, + ContainsSecretMaterialInResponse = true + }); + } + + private static void RegisterSecrets(Dictionary> map) + { + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.ListSecrets, + Resource = "Secrets", + Version = "v4", + Method = "GET", + Template = "/api/v4/secrets", + RequiresAuthorization = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.ListSecrets, + Resource = "Secrets", + Version = "v3", + Method = "GET", + Template = "/api/v3/secrets/raw", + RequiresAuthorization = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.RetrieveSecret, + Resource = "Secrets", + Version = "v4", + Method = "GET", + Template = "/api/v4/secrets/{secretName}", + RequiresAuthorization = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.RetrieveSecret, + Resource = "Secrets", + Version = "v3", + Method = "GET", + Template = "/api/v3/secrets/raw/{secretName}", + RequiresAuthorization = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.CreateSecret, + Resource = "Secrets", + Version = "v3", + Method = "POST", + Template = "/api/v3/secrets/raw/{secretName}", + RequiresAuthorization = true, + ContainsSecretMaterialInRequest = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.UpdateSecret, + Resource = "Secrets", + Version = "v3", + Method = "PATCH", + Template = "/api/v3/secrets/raw/{secretName}", + RequiresAuthorization = true, + ContainsSecretMaterialInRequest = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.DeleteSecret, + Resource = "Secrets", + Version = "v3", + Method = "DELETE", + Template = "/api/v3/secrets/raw/{secretName}", + RequiresAuthorization = true, + ContainsSecretMaterialInResponse = true + }); + } + + private static void RegisterProjects(Dictionary> map) + { + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.ListProjects, + Resource = "Projects", + Version = "v1", + Method = "GET", + Template = "/api/v1/workspace", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.RetrieveProject, + Resource = "Projects", + Version = "v1", + Method = "GET", + Template = "/api/v1/workspace/{projectId}", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.CreateProject, + Resource = "Projects", + Version = "v2", + Method = "POST", + Template = "/api/v2/workspace", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.UpdateProject, + Resource = "Projects", + Version = "v1", + Method = "PATCH", + Template = "/api/v1/workspace/{projectId}", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.DeleteProject, + Resource = "Projects", + Version = "v1", + Method = "DELETE", + Template = "/api/v1/workspace/{projectId}", + RequiresAuthorization = true + }); + } + + private static void RegisterEnvironments(Dictionary> map) + { + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.ListEnvironments, + Resource = "Environments", + Version = "v1", + Method = "GET", + Template = "/api/v1/workspace/{projectId}", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.RetrieveEnvironment, + Resource = "Environments", + Version = "v1", + Method = "GET", + Template = "/api/v1/workspace/{projectId}/environments/{environmentId}", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.CreateEnvironment, + Resource = "Environments", + Version = "v1", + Method = "POST", + Template = "/api/v1/workspace/{projectId}/environments", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.UpdateEnvironment, + Resource = "Environments", + Version = "v1", + Method = "PATCH", + Template = "/api/v1/workspace/{projectId}/environments/{environmentId}", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.DeleteEnvironment, + Resource = "Environments", + Version = "v1", + Method = "DELETE", + Template = "/api/v1/workspace/{projectId}/environments/{environmentId}", + RequiresAuthorization = true + }); + } + + private static void RegisterFolders(Dictionary> map) + { + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.ListFolders, + Resource = "Folders", + Version = "v1", + Method = "GET", + Template = "/api/v1/folders", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.RetrieveFolder, + Resource = "Folders", + Version = "v1", + Method = "GET", + Template = "/api/v1/folders/{folderId}", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.CreateFolder, + Resource = "Folders", + Version = "v1", + Method = "POST", + Template = "/api/v1/folders", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.UpdateFolder, + Resource = "Folders", + Version = "v1", + Method = "PATCH", + Template = "/api/v1/folders/{folderId}", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.DeleteFolder, + Resource = "Folders", + Version = "v1", + Method = "DELETE", + Template = "/api/v1/folders/{folderId}", + RequiresAuthorization = true + }); + } + + private static void RegisterTags(Dictionary> map) + { + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.ListTags, + Resource = "Tags", + Version = "v1", + Method = "GET", + Template = "/api/v1/workspace/{projectId}/tags", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.RetrieveTag, + Resource = "Tags", + Version = "v1", + Method = "GET", + Template = "/api/v1/workspace/{projectId}/tags/{tagId}", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.CreateTag, + Resource = "Tags", + Version = "v1", + Method = "POST", + Template = "/api/v1/workspace/{projectId}/tags", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.UpdateTag, + Resource = "Tags", + Version = "v1", + Method = "PATCH", + Template = "/api/v1/workspace/{projectId}/tags/{tagId}", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.DeleteTag, + Resource = "Tags", + Version = "v1", + Method = "DELETE", + Template = "/api/v1/workspace/{projectId}/tags/{tagId}", + RequiresAuthorization = true + }); + } public static InfisicalEndpointDefinition Get(string name) { diff --git a/src/PSInfisicalAPI/Http/InfisicalApiInvoker.cs b/src/PSInfisicalAPI/Http/InfisicalApiInvoker.cs new file mode 100644 index 0000000..d6e2872 --- /dev/null +++ b/src/PSInfisicalAPI/Http/InfisicalApiInvoker.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Endpoints; +using PSInfisicalAPI.Errors; +using PSInfisicalAPI.Security; + +namespace PSInfisicalAPI.Http +{ + internal sealed class InfisicalApiInvoker + { + private readonly IInfisicalHttpClient _httpClient; + + public InfisicalApiInvoker(IInfisicalHttpClient httpClient) + { + _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + } + + public InfisicalHttpResponse Invoke( + InfisicalConnection connection, + string endpointName, + string operationName, + IDictionary pathParameters, + IEnumerable> queryParameters, + string body) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(endpointName)) { throw new ArgumentNullException(nameof(endpointName)); } + + InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(endpointName); + Uri uri = InfisicalUriBuilder.Build(connection.BaseUri, definition, pathParameters, queryParameters); + + InfisicalHttpResponse response = ExecuteAuthorized(connection, definition, operationName, uri, body); + + if (response.StatusCode >= 200 && response.StatusCode < 300) + { + return response; + } + + InfisicalApiException exception = BuildApiException(response, definition); + response.Clear(); + throw exception; + } + + private InfisicalHttpResponse ExecuteAuthorized( + InfisicalConnection connection, + InfisicalEndpointDefinition definition, + string operationName, + Uri uri, + string body) + { + Dictionary headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + headers["Accept"] = "application/json"; + + if (!string.IsNullOrEmpty(body)) + { + headers["Content-Type"] = "application/json"; + } + + if (definition.RequiresAuthorization) + { + if (connection.AccessToken == null) + { + throw new InfisicalAuthenticationException("Connection does not contain an access token."); + } + + SecureStringUtility.UsePlainText(connection.AccessToken, plainToken => + { + headers["Authorization"] = string.Concat("Bearer ", plainToken ?? string.Empty); + }); + } + + InfisicalHttpRequest request = new InfisicalHttpRequest + { + OperationName = operationName, + EndpointName = definition.Name, + Method = definition.Method, + Uri = uri, + Headers = headers, + Body = body, + ContainsSecretMaterialInRequest = definition.ContainsSecretMaterialInRequest, + ContainsSecretMaterialInResponse = definition.ContainsSecretMaterialInResponse + }; + + return _httpClient.Send(request); + } + + private static InfisicalApiException BuildApiException(InfisicalHttpResponse response, InfisicalEndpointDefinition definition) + { + InfisicalApiException exception = new InfisicalApiException(string.Concat( + "Infisical API returned ", + response.StatusCode.ToString(CultureInfo.InvariantCulture), + " (", response.ReasonPhrase ?? string.Empty, ").")); + exception.StatusCode = response.StatusCode; + exception.ReasonPhrase = response.ReasonPhrase; + exception.EndpointName = definition.Name; + exception.RequestMethod = definition.Method; + exception.SanitizedBody = response.Body; + return exception; + } + } +}