diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ab6f5d..b8b343b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 index 9ac6be4..e9852e7 100644 --- a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 +++ b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 @@ -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' } } } \ No newline at end of file diff --git a/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll b/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll index 69a2d2c..3894887 100644 Binary files a/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll and b/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll differ diff --git a/README.md b/README.md index a57f620..f8022f4 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/PSInfisicalAPI.Tests/ProjectScopingTests.cs b/src/PSInfisicalAPI.Tests/ProjectScopingTests.cs index 63cefa8..979e889 100644 --- a/src/PSInfisicalAPI.Tests/ProjectScopingTests.cs +++ b/src/PSInfisicalAPI.Tests/ProjectScopingTests.cs @@ -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 called = GetCalledMethodNames(resolver); + Assert.DoesNotContain("ThrowTerminatingForException", called); + Assert.DoesNotContain("WriteErrorForException", called); + + List ungarded = new List(); + 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() { diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationCmdlet.cs index 15e31a7..55fbf9a 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationCmdlet.cs @@ -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)) diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationEnrollmentCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationEnrollmentCmdlet.cs index be8ba71..370afe0 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationEnrollmentCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationEnrollmentCmdlet.cs @@ -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); diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs index e125de6..b9d15ff 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs @@ -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)) diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateCmdlet.cs index c9b70f7..3deb08c 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateCmdlet.cs @@ -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, diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatePolicyCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatePolicyCmdlet.cs index 6d8bb82..4d3da67 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatePolicyCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatePolicyCmdlet.cs @@ -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)) diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateProfileCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateProfileCmdlet.cs index 3c985a6..422a1fb 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateProfileCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateProfileCmdlet.cs @@ -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)) diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalPkiSubscriberCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalPkiSubscriberCmdlet.cs index 972d449..f2c300a 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalPkiSubscriberCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalPkiSubscriberCmdlet.cs @@ -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)) diff --git a/src/PSInfisicalAPI/Cmdlets/InfisicalCmdletBase.cs b/src/PSInfisicalAPI/Cmdlets/InfisicalCmdletBase.cs index 1f9b3be..46753a1 100644 --- a/src/PSInfisicalAPI/Cmdlets/InfisicalCmdletBase.cs +++ b/src/PSInfisicalAPI/Cmdlets/InfisicalCmdletBase.cs @@ -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. /// /// + /// + /// The project to use, or null 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. + /// 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];