M1: endpoint registry + shared API invoker for full CRUD expansion

This commit is contained in:
GraceSolutions
2026-06-03 17:17:53 -04:00
parent 269f0ea438
commit 612ecf2c7d
4 changed files with 601 additions and 76 deletions
@@ -45,5 +45,39 @@ namespace PSInfisicalAPI.Tests
{
Assert.Throws<InfisicalConfigurationException>(() => 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);
}
}
}
@@ -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";
}
}
@@ -5,83 +5,435 @@ namespace PSInfisicalAPI.Endpoints
{
public static class InfisicalEndpointRegistry
{
private static readonly Dictionary<string, List<InfisicalEndpointDefinition>> Candidates =
new Dictionary<string, List<InfisicalEndpointDefinition>>
private static readonly Dictionary<string, List<InfisicalEndpointDefinition>> Candidates;
static InfisicalEndpointRegistry()
{
Candidates = new Dictionary<string, List<InfisicalEndpointDefinition>>();
RegisterAuthentication(Candidates);
RegisterSecrets(Candidates);
RegisterProjects(Candidates);
RegisterEnvironments(Candidates);
RegisterFolders(Candidates);
RegisterTags(Candidates);
}
private static void Add(Dictionary<string, List<InfisicalEndpointDefinition>> map, InfisicalEndpointDefinition definition)
{
List<InfisicalEndpointDefinition> list;
if (!map.TryGetValue(definition.Name, out list))
{
{
InfisicalEndpointNames.UniversalAuthLogin,
new List<InfisicalEndpointDefinition>
{
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<InfisicalEndpointDefinition>
{
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<InfisicalEndpointDefinition>
{
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<InfisicalEndpointDefinition>();
map[definition.Name] = list;
}
list.Add(definition);
}
private static void RegisterAuthentication(Dictionary<string, List<InfisicalEndpointDefinition>> 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<string, List<InfisicalEndpointDefinition>> 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<string, List<InfisicalEndpointDefinition>> 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<string, List<InfisicalEndpointDefinition>> 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<string, List<InfisicalEndpointDefinition>> 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<string, List<InfisicalEndpointDefinition>> 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)
{
@@ -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<string, string> pathParameters,
IEnumerable<KeyValuePair<string, string>> 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<string, string> headers = new Dictionary<string, string>(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;
}
}
}