Resolve to the organization's active cert-manager project instead of erroring
Several Certificate Manager projects in one organization is a normal
configuration, not an ambiguity to reject. Infisical designates one as the
organization's active project and serves certificate applications only from it:
if (req.internalCertManagerProjectId !== activeProjectId) {
throw new BadRequestError({ message: "Applications are only available on
this organization's active Certificate Manager project." });
}
So an application-centric workflow is single-project by design, and resolving to
the active project is what makes it work. Resolution now picks that project when
several exist, falling back to the first - saying so on the verbose stream -
when the organization designates none. Only an organization with no Certificate
Manager project at all still errors, because there is genuinely nothing to
resolve to.
Adds InfisicalOrganization.DefaultCertManagerProjectId, which is what the
organization record calls its active project, so the choice is read rather than
guessed.
Moves Get-InfisicalProject onto /api/v1/projects. /api/v1/workspace mounts
Infisical's deprecated project router; it is retained as a fallback candidate so
older servers keep working, and the endpoint shape test now expects the current
route.
The end-to-end README example drops to the four calls that actually do the work:
find the application, pick its profile, gather SANs, request. The project lookup
is gone because -ProjectId resolves itself, and the CA lookup is gone because the
profile already binds its issuing CA and -InstallChain installs the chain
regardless.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,7 +50,9 @@ namespace PSInfisicalAPI.Tests
|
||||
[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")]
|
||||
// /api/v1/workspace mounts Infisical's deprecated project router; /api/v1/projects is the current one
|
||||
// and is preferred, with the deprecated route retained as a fallback candidate.
|
||||
[InlineData(InfisicalEndpointNames.ListProjects, "GET", "/api/v1/projects")]
|
||||
[InlineData(InfisicalEndpointNames.RetrieveProject, "GET", "/api/v1/workspace/{projectId}")]
|
||||
[InlineData(InfisicalEndpointNames.CreateProject, "POST", "/api/v2/workspace")]
|
||||
[InlineData(InfisicalEndpointNames.UpdateProject, "PATCH", "/api/v1/workspace/{projectId}")]
|
||||
|
||||
@@ -92,6 +92,45 @@ namespace PSInfisicalAPI.Tests
|
||||
Assert.Equal(typeof(string), parameters[1].ParameterType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolution_Does_Not_Error_When_An_Organization_Has_Several_Projects()
|
||||
{
|
||||
// Certificate applications are served only from the organization's active project, so several
|
||||
// Certificate Manager projects is a normal configuration rather than an ambiguity to reject.
|
||||
MethodInfo resolver = typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase)
|
||||
.GetMethod("ResolveCertManagerProjectId", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
List<string> called = GetCalledMethodNames(resolver);
|
||||
Assert.Contains("FindActiveCertManagerProject", called);
|
||||
|
||||
MethodInfo finder = typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase)
|
||||
.GetMethod("FindActiveCertManagerProject", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
Assert.NotNull(finder);
|
||||
Assert.Equal(typeof(PSInfisicalAPI.Models.InfisicalProject), finder.ReturnType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_Organizations_Active_Cert_Manager_Project_Is_Modelled()
|
||||
{
|
||||
PropertyInfo property = typeof(PSInfisicalAPI.Models.InfisicalOrganization)
|
||||
.GetProperty("DefaultCertManagerProjectId");
|
||||
Assert.NotNull(property);
|
||||
Assert.Equal(typeof(string), property.PropertyType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Project_Listing_Prefers_The_Current_Route_Over_The_Deprecated_One()
|
||||
{
|
||||
// /api/v1/workspace mounts Infisical's deprecated project router; /api/v1/projects is current.
|
||||
IReadOnlyList<PSInfisicalAPI.Endpoints.InfisicalEndpointDefinition> candidates =
|
||||
PSInfisicalAPI.Endpoints.InfisicalEndpointRegistry.GetCandidates(
|
||||
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.ListProjects);
|
||||
|
||||
Assert.True(candidates.Count >= 2, "both the current and deprecated routes should be registered");
|
||||
Assert.Equal("/api/v1/projects", candidates[0].Template);
|
||||
Assert.Contains(candidates, c => c.Template == "/api/v1/workspace");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void An_Explicit_ProjectId_Short_Circuits_Resolution()
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Runtime.ExceptionServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using PSInfisicalAPI.Connections;
|
||||
using PSInfisicalAPI.Models;
|
||||
using PSInfisicalAPI.Organizations;
|
||||
using PSInfisicalAPI.Projects;
|
||||
using PSInfisicalAPI.Errors;
|
||||
using PSInfisicalAPI.Http;
|
||||
@@ -96,28 +97,77 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
"This organization has no Certificate Manager project, so there is nothing to resolve -ProjectId to. Create one in Infisical, or pass -ProjectId explicitly.");
|
||||
}
|
||||
|
||||
InfisicalProject chosen = certManagerProjects[0];
|
||||
string reason = "the organization's only Certificate Manager project";
|
||||
|
||||
if (certManagerProjects.Count > 1)
|
||||
{
|
||||
List<string> described = new List<string>();
|
||||
foreach (InfisicalProject project in certManagerProjects)
|
||||
// More than one is not an error. Infisical designates one of them as the organization's active
|
||||
// Certificate Manager project, and certificate applications are only served from that one, so
|
||||
// resolving to it is what makes an application-centric script work.
|
||||
InfisicalProject active = FindActiveCertManagerProject(connection, certManagerProjects);
|
||||
if (active != null)
|
||||
{
|
||||
described.Add(string.Concat("'", project.Name ?? project.Slug, "' (", project.Id, ")"));
|
||||
chosen = active;
|
||||
reason = "the organization's active Certificate Manager project";
|
||||
}
|
||||
else
|
||||
{
|
||||
reason = string.Concat(
|
||||
"the first of ", certManagerProjects.Count.ToString(CultureInfo.InvariantCulture),
|
||||
" Certificate Manager projects (no active project is set on the organization; pass -ProjectId to choose another)");
|
||||
}
|
||||
|
||||
throw new InfisicalConfigurationException(string.Concat(
|
||||
"This organization has ", certManagerProjects.Count.ToString(CultureInfo.InvariantCulture),
|
||||
" Certificate Manager projects, so -ProjectId cannot be resolved automatically. Pass it explicitly. Available: ",
|
||||
string.Join(", ", described.ToArray()), "."));
|
||||
}
|
||||
|
||||
_resolvedCertManagerProjectId = certManagerProjects[0].Id;
|
||||
_resolvedCertManagerProjectId = chosen.Id;
|
||||
Logger.Verbose(GetType().Name, string.Concat(
|
||||
"-ProjectId was not supplied; resolved the organization's only Certificate Manager project '",
|
||||
certManagerProjects[0].Name ?? certManagerProjects[0].Slug, "' (", _resolvedCertManagerProjectId, ")."));
|
||||
"-ProjectId was not supplied; resolved ", reason, ": '",
|
||||
chosen.Name ?? chosen.Slug, "' (", _resolvedCertManagerProjectId, ")."));
|
||||
|
||||
return _resolvedCertManagerProjectId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the organization's active Certificate Manager project among the candidates. Certificate
|
||||
/// applications are served only from this project, so when several exist it is the one a PKI call
|
||||
/// should target. Returns null when the organization designates none, leaving the caller to fall back.
|
||||
/// </summary>
|
||||
private InfisicalProject FindActiveCertManagerProject(InfisicalConnection connection, List<InfisicalProject> candidates)
|
||||
{
|
||||
try
|
||||
{
|
||||
InfisicalOrganizationClient organizationClient = new InfisicalOrganizationClient(HttpClient, Logger);
|
||||
InfisicalOrganization[] organizations = organizationClient.List(connection);
|
||||
if (organizations == null) { return null; }
|
||||
|
||||
string organizationId = connection != null ? connection.OrganizationId : null;
|
||||
foreach (InfisicalOrganization organization in organizations)
|
||||
{
|
||||
if (organization == null || string.IsNullOrEmpty(organization.DefaultCertManagerProjectId)) { continue; }
|
||||
if (!string.IsNullOrEmpty(organizationId)
|
||||
&& !string.Equals(organization.Id, organizationId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (InfisicalProject candidate in candidates)
|
||||
{
|
||||
if (string.Equals(candidate.Id, organization.DefaultCertManagerProjectId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
if (IsPipelineControlException(exception)) { throw; }
|
||||
Logger.Verbose(GetType().Name, string.Concat("Could not read the organization's active Certificate Manager project (continuing): ", exception.Message));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reports whether the host process is running elevated. Evaluated through the PowerShell engine rather
|
||||
/// than WindowsIdentity directly, because the module targets netstandard2.0 and does not carry a
|
||||
|
||||
@@ -288,6 +288,18 @@ namespace PSInfisicalAPI.Endpoints
|
||||
|
||||
private static void RegisterProjects(Dictionary<string, List<InfisicalEndpointDefinition>> map)
|
||||
{
|
||||
// /api/v1/projects is the current route; /api/v1/workspace mounts Infisical's deprecated project
|
||||
// router and is kept only as a fallback for older servers.
|
||||
Add(map, new InfisicalEndpointDefinition
|
||||
{
|
||||
Name = InfisicalEndpointNames.ListProjects,
|
||||
Resource = "Projects",
|
||||
Version = "v1",
|
||||
Method = "GET",
|
||||
Template = "/api/v1/projects",
|
||||
RequiresAuthorization = true
|
||||
});
|
||||
|
||||
Add(map, new InfisicalEndpointDefinition
|
||||
{
|
||||
Name = InfisicalEndpointNames.ListProjects,
|
||||
|
||||
@@ -9,6 +9,12 @@ namespace PSInfisicalAPI.Models
|
||||
public string Slug { get; set; }
|
||||
public string CustomerId { get; set; }
|
||||
public bool AuthEnforced { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The organization's active Certificate Manager project. Certificate applications are only available on
|
||||
/// this project, so it is what a PKI call resolves to when an organization has more than one.
|
||||
/// </summary>
|
||||
public string DefaultCertManagerProjectId { get; set; }
|
||||
public bool ScimEnabled { get; set; }
|
||||
public DateTimeOffset? CreatedAtUtc { get; set; }
|
||||
public DateTimeOffset? UpdatedAtUtc { get; set; }
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace PSInfisicalAPI.Organizations
|
||||
[JsonProperty("slug")] public string Slug { get; set; }
|
||||
[JsonProperty("customerId")] public string CustomerId { get; set; }
|
||||
[JsonProperty("authEnforced")] public bool AuthEnforced { get; set; }
|
||||
[JsonProperty("defaultCertManagerProjectId", NullValueHandling = NullValueHandling.Ignore)] public string DefaultCertManagerProjectId { get; set; }
|
||||
[JsonProperty("scimEnabled")] public bool ScimEnabled { get; set; }
|
||||
[JsonProperty("createdAt")] public string CreatedAt { get; set; }
|
||||
[JsonProperty("updatedAt")] public string UpdatedAt { get; set; }
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace PSInfisicalAPI.Organizations
|
||||
Slug = dto.Slug,
|
||||
CustomerId = dto.CustomerId,
|
||||
AuthEnforced = dto.AuthEnforced,
|
||||
DefaultCertManagerProjectId = dto.DefaultCertManagerProjectId,
|
||||
ScimEnabled = dto.ScimEnabled,
|
||||
CreatedAtUtc = ParseTimestamp(dto.CreatedAt),
|
||||
UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt)
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace PSInfisicalAPI.Projects
|
||||
try
|
||||
{
|
||||
_logger.Information(Component, "Attempting to list Infisical projects. Please Wait...");
|
||||
InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.ListProjects, "ListProjects", null, queryParameters, null);
|
||||
InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.ListProjects, "ListProjects", null, queryParameters, null);
|
||||
InfisicalProjectListResponseDto dto = _serializer.Deserialize<InfisicalProjectListResponseDto>(response.Body);
|
||||
response.Clear();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user