Treat a missing cert-manager project as an empty result rather than an error

An organization that has not set up Certificate Manager has nothing to list, so
resolution now returns nothing instead of throwing, and the PKI Get-* cmdlets
return quietly. -Verbose explains what happened and what to do:

  -ProjectId was not supplied and this organization has no Certificate Manager
  project, so there is nothing to resolve to. Create one in Infisical
  (Certificate Management), or pass -ProjectId to target a specific project.

Without the guard the null would have travelled to the client layer and
surfaced as "ProjectId is required" from somewhere unrelated to the cause, so
each Get-* cmdlet returns as soon as resolution comes back empty.

Request-InfisicalCertificate deliberately proceeds, because profile issuance
does not need a project: only the reuse search does, and that already degrades
to a local match with a warning when the search cannot run.

Resolution is now attempted once per cmdlet instance rather than once per
non-null result, so an organization with no project does not re-query on every
call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 16:06:33 -04:00
parent 276958e3a8
commit a1e1d22e72
13 changed files with 85 additions and 6 deletions
+8 -1
View File
@@ -6,11 +6,17 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos
## Unreleased
## 2026.07.31.2006
- Build produced from commit 276958e3a83a.
## Unreleased (carried forward)
## 2026.07.31.1958
- Build produced from commit 633f40c1fa54.
## Unreleased (carried forward)
## Unreleased (carried forward)
## 2026.07.31.1924
@@ -50,6 +56,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos
### Changed (project scoping, follow-up)
- An organization with **no** Certificate Manager project is no longer an error. Resolution returns nothing and the PKI `Get-*` cmdlets emit no output, with `-Verbose` explaining that none was found and suggesting one be created. Nothing to list is an empty result, not a failure.
- Several Certificate Manager projects in one organization is **no longer an error**. Infisical designates one as the organization's active project and serves certificate applications only from it, so resolution now picks that one; when none is designated the first is used and the verbose line says so.
- `InfisicalOrganization.DefaultCertManagerProjectId` exposes the organization's active Certificate Manager project.
- `Get-InfisicalProject` now calls `/api/v1/projects`, keeping the previously used `/api/v1/workspace` as a fallback candidate — that route mounts Infisical's deprecated project router.
+2 -2
View File
@@ -1,6 +1,6 @@
@{
RootModule = 'PSInfisicalAPI.psm1'
ModuleVersion = '2026.07.31.1958'
ModuleVersion = '2026.07.31.2006'
GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51'
Author = 'Grace Solutions'
CompanyName = 'Grace Solutions'
@@ -74,7 +74,7 @@
LicenseUri = 'https://www.gnu.org/licenses/agpl-3.0.html'
ProjectUri = 'https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI'
ReleaseNotes = 'See CHANGELOG.md in the project repository for release history.'
CommitHash = '633f40c1fa54'
CommitHash = '276958e3a83a'
}
}
}
Binary file not shown.
+8
View File
@@ -216,6 +216,14 @@ VERBOSE: -ProjectId was not supplied; resolved the organization's only Certifica
'Microsoft Endpoint Configuration Manager' (2122628e-...).
```
An organization with **no** Certificate Manager project is not an error either. There is nothing to list, so the PKI `Get-*` cmdlets return nothing and `-Verbose` explains why:
```text
VERBOSE: -ProjectId was not supplied and this organization has no Certificate Manager project, so there is
nothing to resolve to. Create one in Infisical (Certificate Management), or pass -ProjectId to
target a specific project.
```
Several Certificate Manager projects in one organization is not an error. Infisical designates one as the organization's **active** project, and that is what resolution picks:
```text
@@ -109,6 +109,36 @@ namespace PSInfisicalAPI.Tests
Assert.Equal(typeof(PSInfisicalAPI.Models.InfisicalProject), finder.ReturnType);
}
[Fact]
public void An_Organization_Without_Any_Cert_Manager_Project_Is_Not_An_Error()
{
// Nothing to list is an empty result, not a failure, so resolution returns null and each Get-*
// cmdlet returns quietly rather than surfacing "ProjectId is required" from deep in the client.
MethodInfo resolver = typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase)
.GetMethod("ResolveCertManagerProjectId", BindingFlags.NonPublic | BindingFlags.Instance);
List<string> called = GetCalledMethodNames(resolver);
Assert.DoesNotContain("ThrowTerminatingForException", called);
Assert.DoesNotContain("WriteErrorForException", called);
List<string> ungarded = new List<string>();
foreach (string typeName in PkiCmdletTypes)
{
// Request-InfisicalCertificate can still issue through a profile without a project, so it is
// deliberately allowed to proceed.
if (typeName.EndsWith("RequestInfisicalCertificateCmdlet", StringComparison.Ordinal)) { continue; }
Type type = ModuleAssembly.GetType(typeName, true);
MethodInfo processRecord = type.GetMethod("ProcessRecord", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
if (!GetCalledMethodNames(processRecord).Contains("IsNullOrEmpty"))
{
ungarded.Add(type.Name);
}
}
Assert.Empty(ungarded);
}
[Fact]
public void The_Organizations_Active_Cert_Manager_Project_Is_Modelled()
{
@@ -34,6 +34,9 @@ namespace PSInfisicalAPI.Cmdlets
// one; -ProjectId is optional here for the same reason. Assigned back so every call below
// sees the resolved value without threading a second variable through.
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
// No Certificate Manager project means nothing to list; that is an empty result, not a failure.
if (string.IsNullOrEmpty(ProjectId)) { return; }
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal))
@@ -30,6 +30,9 @@ namespace PSInfisicalAPI.Cmdlets
// one; -ProjectId is optional here for the same reason. Assigned back so every call below
// sees the resolved value without threading a second variable through.
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
// No Certificate Manager project means nothing to list; that is an empty result, not a failure.
if (string.IsNullOrEmpty(ProjectId)) { return; }
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
InfisicalCertificateApplicationEnrollment enrollment = client.GetCertificateApplicationEnrollment(connection, ApplicationId, ProfileId, ProjectId);
@@ -30,6 +30,9 @@ namespace PSInfisicalAPI.Cmdlets
// one; -ProjectId is optional here for the same reason. Assigned back so every call below
// sees the resolved value without threading a second variable through.
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
// No Certificate Manager project means nothing to list; that is an empty result, not a failure.
if (string.IsNullOrEmpty(ProjectId)) { return; }
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal))
@@ -76,6 +76,9 @@ namespace PSInfisicalAPI.Cmdlets
// sees the resolved value without threading a second variable through.
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
// No Certificate Manager project means nothing to list; that is an empty result, not a failure.
if (string.IsNullOrEmpty(ProjectId)) { return; }
InfisicalCertificateSearchQuery query = new InfisicalCertificateSearchQuery
{
ProjectId = ProjectId,
@@ -30,6 +30,9 @@ namespace PSInfisicalAPI.Cmdlets
// one; -ProjectId is optional here for the same reason. Assigned back so every call below
// sees the resolved value without threading a second variable through.
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
// No Certificate Manager project means nothing to list; that is an empty result, not a failure.
if (string.IsNullOrEmpty(ProjectId)) { return; }
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal))
@@ -44,6 +44,9 @@ namespace PSInfisicalAPI.Cmdlets
// one; -ProjectId is optional here for the same reason. Assigned back so every call below
// sees the resolved value without threading a second variable through.
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
// No Certificate Manager project means nothing to list; that is an empty result, not a failure.
if (string.IsNullOrEmpty(ProjectId)) { return; }
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal))
@@ -26,6 +26,9 @@ namespace PSInfisicalAPI.Cmdlets
// one; -ProjectId is optional here for the same reason. Assigned back so every call below
// sees the resolved value without threading a second variable through.
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
// No Certificate Manager project means nothing to list; that is an empty result, not a failure.
if (string.IsNullOrEmpty(ProjectId)) { return; }
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
if (string.Equals(ParameterSetName, "ByName", StringComparison.Ordinal))
@@ -23,6 +23,7 @@ namespace PSInfisicalAPI.Cmdlets
private IInfisicalHttpClient _httpClient;
private bool? _isElevated;
private string _resolvedCertManagerProjectId;
private bool _certManagerProjectResolved;
protected IInfisicalLogger Logger
{
@@ -70,10 +71,17 @@ namespace PSInfisicalAPI.Cmdlets
/// in the URL path and cannot defer to the server's resolver. Resolved once per cmdlet instance.
/// </para>
/// </summary>
/// <returns>
/// The project to use, or <c>null</c> when the organization has no Certificate Manager project. Callers
/// that cannot proceed without one should return quietly rather than failing: an organization that has
/// not set up Certificate Manager has nothing to list, which is an empty result and not an error.
/// </returns>
protected string ResolveCertManagerProjectId(InfisicalConnection connection, string explicitValue)
{
if (!string.IsNullOrEmpty(explicitValue)) { return explicitValue; }
if (_resolvedCertManagerProjectId != null) { return _resolvedCertManagerProjectId; }
if (_certManagerProjectResolved) { return _resolvedCertManagerProjectId; }
_certManagerProjectResolved = true;
InfisicalProjectClient client = new InfisicalProjectClient(HttpClient, Logger);
InfisicalProject[] projects = client.List(connection, CertManagerProjectType, false);
@@ -93,8 +101,13 @@ namespace PSInfisicalAPI.Cmdlets
if (certManagerProjects.Count == 0)
{
throw new InfisicalConfigurationException(
"This organization has no Certificate Manager project, so there is nothing to resolve -ProjectId to. Create one in Infisical, or pass -ProjectId explicitly.");
Logger.Verbose(GetType().Name, string.Concat(
"-ProjectId was not supplied and this organization has no Certificate Manager project, so there is ",
"nothing to resolve to. Create one in Infisical (Certificate Management), or pass -ProjectId to ",
"target a specific project."));
_resolvedCertManagerProjectId = null;
return null;
}
InfisicalProject chosen = certManagerProjects[0];