diff --git a/CHANGELOG.md b/CHANGELOG.md
index 349385b..894579b 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.1924
+
+- Build produced from commit 93b0cc1924ec.
+
+## Unreleased (carried forward)
+
## 2026.07.31.0045
- Build produced from commit d47a5af6b3a6.
-## Unreleased (carried forward)
+## Unreleased (carried forward)
## 2026.07.30.2350
@@ -36,6 +42,20 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos
## Unreleased (carried forward)
+### Changed (project scoping)
+
+- **`-ProjectId` is now optional on every PKI cmdlet** (`Get-InfisicalCertificateApplication`, `-ApplicationEnrollment`, `-Authority`, `-Certificate`, `-CertificatePolicy`, `-CertificateProfile`, `Get-InfisicalPkiSubscriber`, `Request-InfisicalCertificate`). The Infisical console never asks which Certificate Manager project to use because its resolver selects the single cert-manager project when an organization has exactly one; the module was stricter than the service it wraps. Omitting `-ProjectId` applies the same rule and reports the resolved project on the verbose stream, and an organization with several produces an error listing them.
+- Resolution is client-side because several PKI endpoints carry the project in the URL path (`/api/v1/projects/{projectId}/pki-subscribers`, `/certificates/search`) and cannot defer to the server's resolver.
+- `Get-InfisicalCertificate -SerialNumber` no longer resolves a project, since addressing a certificate by serial does not need one.
+
+### Added (application scoping)
+
+- `Get-InfisicalCertificateProfile -ApplicationId` and `-CaId` filter a listing the way the console groups profiles. `Get-InfisicalCertificate` already accepted `-ApplicationId`.
+
+### Fixed (documentation)
+
+- The README Quick Start did not run: it passed `-ProjectId` and `-Environment` to `Connect-Infisical`, which has neither, failing with "A parameter cannot be found that matches parameter name 'ProjectId'". Project, environment, and secret path are per-call parameters.
+
### Added (certificate metadata)
- `Request-InfisicalCertificate -Metadata` attaches key/value pairs to the issued or reused certificate, accepting any `IDictionary` (hashtable, `[Ordered]`, generic dictionary).
diff --git a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1
index 7cddb2a..63ab803 100644
--- a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1
+++ b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1
@@ -1,6 +1,6 @@
@{
RootModule = 'PSInfisicalAPI.psm1'
- ModuleVersion = '2026.07.31.0045'
+ ModuleVersion = '2026.07.31.1924'
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 = 'd47a5af6b3a6'
+ CommitHash = '93b0cc1924ec'
}
}
}
\ No newline at end of file
diff --git a/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll b/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll
index 5664731..eb4e942 100644
Binary files a/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll and b/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll differ
diff --git a/Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml b/Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml
index 16d82b8..79cb0b8 100644
--- a/Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml
+++ b/Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml
@@ -1066,6 +1066,7 @@ $RemoveInfisicalTagResult = Remove-InfisicalTag @RemoveInfisicalTagParameters
Notes
+ -ProjectId is optional. The Infisical console never asks which Certificate Manager project to use, because its resolver selects the single cert-manager project when an organization has exactly one; omitting -ProjectId applies the same rule and reports the resolved project on the verbose stream. Pass it explicitly when an organization has more than one, in which case the error lists the candidates. Note that a project contains applications: Get-InfisicalProject -Type cert-manager returns the project, while Get-InfisicalCertificateApplication returns the applications inside it.
ByID retrieval currently always resolves against the internal CA endpoint. CA Ids returned here are the values to pass on -CertificateAuthorityId to Request-InfisicalCertificate. The Type property distinguishes 'internal' from 'acme' when -Kind Any is used. Only CAs whose EnableDirectIssuance property is True can sign a CSR through -CertificateAuthorityId; the others must issue through Request-InfisicalCertificate -CertificateProfileId, which bypasses that check. EnableDirectIssuance is fixed at CA creation and appears in no Infisical update schema, so it cannot be toggled afterwards; a CA migrated from the older requireTemplateForIssuance column reads False permanently.
diff --git a/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml
index 16d82b8..79cb0b8 100644
--- a/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml
+++ b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml
@@ -1066,6 +1066,7 @@ $RemoveInfisicalTagResult = Remove-InfisicalTag @RemoveInfisicalTagParameters
Notes
+ -ProjectId is optional. The Infisical console never asks which Certificate Manager project to use, because its resolver selects the single cert-manager project when an organization has exactly one; omitting -ProjectId applies the same rule and reports the resolved project on the verbose stream. Pass it explicitly when an organization has more than one, in which case the error lists the candidates. Note that a project contains applications: Get-InfisicalProject -Type cert-manager returns the project, while Get-InfisicalCertificateApplication returns the applications inside it.
ByID retrieval currently always resolves against the internal CA endpoint. CA Ids returned here are the values to pass on -CertificateAuthorityId to Request-InfisicalCertificate. The Type property distinguishes 'internal' from 'acme' when -Kind Any is used. Only CAs whose EnableDirectIssuance property is True can sign a CSR through -CertificateAuthorityId; the others must issue through Request-InfisicalCertificate -CertificateProfileId, which bypasses that check. EnableDirectIssuance is fixed at CA creation and appears in no Infisical update schema, so it cannot be toggled afterwards; a CA migrated from the older requireTemplateForIssuance column reads False permanently.
diff --git a/README.md b/README.md
index 2ad11ed..42d034e 100644
--- a/README.md
+++ b/README.md
@@ -134,16 +134,16 @@ $secureSecret = Read-Host -AsSecureString 'Client Secret'
$connection = Connect-Infisical `
-BaseUri 'https://app.infisical.com' `
-OrganizationId '00000000-0000-0000-0000-000000000000' `
- -ProjectId '11111111-1111-1111-1111-111111111111' `
- -Environment 'dev' `
-ClientId 'machine-identity-client-id' `
-ClientSecret $secureSecret `
-PassThru
-Get-InfisicalSecret -SecretPath '/'
+Get-InfisicalSecret -ProjectId '11111111-1111-1111-1111-111111111111' -Environment 'dev' -SecretPath '/'
Disconnect-Infisical
```
+`Connect-Infisical` establishes the session; project, environment, and secret path are supplied per call. On the PKI cmdlets `-ProjectId` is optional — see [Project scoping](#project-scoping).
+
## End-to-end: request and install a chained certificate
Connects, selects a `cert-manager` project, sources SANs from `Get-InfisicalSANList`, requests a certificate through a certificate profile, installs it (and its chain) into the current-user store, and disconnects. Each call uses a splatted `OrderedDictionary` constructed with `OrdinalIgnoreCase` so parameter names round-trip case-insensitively.
@@ -211,6 +211,58 @@ Note `$CertificateProfile` rather than `$Profile`: `$Profile` is an automatic va
`-StoreName`/`-StoreLocation` are omitted deliberately — see [Where certificates get installed](#where-certificates-get-installed).
+### Project scoping
+
+Opening **Certificate Manager** in the Infisical console never asks which project to use. The project is in the URL — `/organizations/{orgId}/projects/cert-manager/{projectId}/applications` — but it is chosen for you, and everything below it is presented as **applications**. That is because Infisical's own resolver takes the single Certificate Manager project when an organization has exactly one:
+
+```ts
+const projects = await projectDAL.find({ orgId: actorOrgId, type: ProjectType.CertificateManager });
+if (projects.length === 1) return projects[0].id;
+```
+
+The PKI cmdlets follow the same rule: **`-ProjectId` is optional**. Omit it and the module resolves the organization's only Certificate Manager project, reporting the choice on the verbose stream:
+
+```powershell
+Get-InfisicalCertificateApplication
+Get-InfisicalCertificateAuthority -Kind Internal
+Get-InfisicalCertificateProfile -IncludeConfigs
+Get-InfisicalPkiSubscriber
+Get-InfisicalCertificate -Status active
+```
+
+```text
+VERBOSE: -ProjectId was not supplied; resolved the organization's only Certificate Manager project
+ 'Microsoft Endpoint Configuration Manager' (2122628e-...).
+```
+
+Pass `-ProjectId` explicitly when an organization has more than one — the resolver cannot guess, and says so with the candidates listed:
+
+```text
+This organization has 2 Certificate Manager projects, so -ProjectId cannot be resolved automatically.
+Pass it explicitly. Available: 'Platform PKI' (aaaa...), 'Lab PKI' (bbbb...).
+```
+
+This is resolved client-side rather than left to the server because several PKI endpoints carry the project in the URL path (`/api/v1/projects/{projectId}/pki-subscribers`, `/certificates/search`) and cannot defer to the server's resolver.
+
+#### Projects contain applications
+
+The two are different levels, which is worth keeping straight when reading output:
+
+| | What it is | Cmdlet |
+| --- | --- | --- |
+| **Project** | The Certificate Manager project itself. One per organization in most setups. | `Get-InfisicalProject -Type cert-manager` |
+| **Application** | A grouping of profiles, members, and certificates inside that project. | `Get-InfisicalCertificateApplication` |
+
+So a project named `mecm` can contain applications named `2pint` and `microsoft-endpoint-configuration-manager`; listing projects returns only `mecm`, because the others are not projects. Every application carries the `ProjectId` it belongs to.
+
+Profiles can be filtered to an application, matching how the console groups them:
+
+```powershell
+$Application = Get-InfisicalCertificateApplication | Where-Object {($_.Name -eq '2pint')}
+Get-InfisicalCertificateProfile -ApplicationId $Application.Id -IncludeConfigs
+Get-InfisicalCertificate -ApplicationId $Application.Id
+```
+
### Example output
```text
diff --git a/src/PSInfisicalAPI.Tests/ProjectScopingTests.cs b/src/PSInfisicalAPI.Tests/ProjectScopingTests.cs
new file mode 100644
index 0000000..67b15f7
--- /dev/null
+++ b/src/PSInfisicalAPI.Tests/ProjectScopingTests.cs
@@ -0,0 +1,203 @@
+using System;
+using System.Collections.Generic;
+using System.Management.Automation;
+using System.Reflection;
+using System.Security.Cryptography.X509Certificates;
+using Xunit;
+
+namespace PSInfisicalAPI.Tests
+{
+ ///
+ /// The Infisical console never asks which Certificate Manager project to use when an organization has one,
+ /// because its resolver selects it implicitly. Requiring -ProjectId made the module stricter than the
+ /// service it wraps, so these pin the parameter as optional across the PKI surface.
+ ///
+ public class ProjectScopingTests
+ {
+ private static readonly Assembly ModuleAssembly = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly;
+
+ private static readonly string[] PkiCmdletTypes = new[]
+ {
+ "PSInfisicalAPI.Cmdlets.GetInfisicalCertificateApplicationCmdlet",
+ "PSInfisicalAPI.Cmdlets.GetInfisicalCertificateApplicationEnrollmentCmdlet",
+ "PSInfisicalAPI.Cmdlets.GetInfisicalCertificateAuthorityCmdlet",
+ "PSInfisicalAPI.Cmdlets.GetInfisicalCertificateCmdlet",
+ "PSInfisicalAPI.Cmdlets.GetInfisicalCertificatePolicyCmdlet",
+ "PSInfisicalAPI.Cmdlets.GetInfisicalCertificateProfileCmdlet",
+ "PSInfisicalAPI.Cmdlets.GetInfisicalPkiSubscriberCmdlet",
+ "PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet"
+ };
+
+ [Fact]
+ public void ProjectId_Is_Optional_On_Every_Pki_Cmdlet()
+ {
+ List offenders = new List();
+
+ foreach (string typeName in PkiCmdletTypes)
+ {
+ Type type = ModuleAssembly.GetType(typeName, true);
+ PropertyInfo projectId = type.GetProperty("ProjectId");
+ Assert.True(projectId != null, string.Concat(typeName, " has no ProjectId property"));
+
+ foreach (CustomAttributeData attribute in projectId.GetCustomAttributesData())
+ {
+ if (attribute.AttributeType != typeof(ParameterAttribute)) { continue; }
+ foreach (CustomAttributeNamedArgument named in attribute.NamedArguments)
+ {
+ if (named.MemberName == "Mandatory" && (bool)named.TypedValue.Value)
+ {
+ offenders.Add(type.Name);
+ }
+ }
+ }
+ }
+
+ Assert.Empty(offenders);
+ }
+
+ [Fact]
+ public void Every_Pki_Cmdlet_Resolves_The_Project_Rather_Than_Assuming_One()
+ {
+ // Optional without resolution would simply send an empty projectId, so confirm each cmdlet calls
+ // the resolver.
+ List missing = new List();
+
+ foreach (string typeName in PkiCmdletTypes)
+ {
+ Type type = ModuleAssembly.GetType(typeName, true);
+ MethodInfo processRecord = type.GetMethod("ProcessRecord", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
+ Assert.NotNull(processRecord);
+
+ if (!GetCalledMethodNames(processRecord).Contains("ResolveCertManagerProjectId"))
+ {
+ missing.Add(type.Name);
+ }
+ }
+
+ Assert.Empty(missing);
+ }
+
+ [Fact]
+ public void Resolver_Is_Available_To_Cmdlets_And_Returns_A_Project_Id()
+ {
+ MethodInfo resolver = typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase)
+ .GetMethod("ResolveCertManagerProjectId", BindingFlags.NonPublic | BindingFlags.Instance);
+
+ Assert.NotNull(resolver);
+ Assert.Equal(typeof(string), resolver.ReturnType);
+
+ ParameterInfo[] parameters = resolver.GetParameters();
+ Assert.Equal(2, parameters.Length);
+ Assert.Equal(typeof(PSInfisicalAPI.Connections.InfisicalConnection), parameters[0].ParameterType);
+ Assert.Equal(typeof(string), parameters[1].ParameterType);
+ }
+
+ [Fact]
+ public void An_Explicit_ProjectId_Short_Circuits_Resolution()
+ {
+ // Supplying the project must never trigger a lookup, so the explicit value has to be returned
+ // before any client is constructed.
+ PSInfisicalAPI.Cmdlets.GetInfisicalCertificateAuthorityCmdlet cmdlet =
+ new PSInfisicalAPI.Cmdlets.GetInfisicalCertificateAuthorityCmdlet();
+
+ MethodInfo resolver = typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase)
+ .GetMethod("ResolveCertManagerProjectId", BindingFlags.NonPublic | BindingFlags.Instance);
+
+ // A null connection would throw the moment a lookup was attempted; returning cleanly proves it did not.
+ string result = (string)resolver.Invoke(cmdlet, new object[] { null, "explicit-project-id" });
+ Assert.Equal("explicit-project-id", result);
+ }
+
+ [Fact]
+ public void Certificate_Retrieval_By_Serial_Does_Not_Resolve_A_Project()
+ {
+ // Addressing a certificate by serial needs no project, so the Single parameter set must not pay
+ // for a lookup.
+ Type type = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.GetInfisicalCertificateCmdlet", true);
+ PropertyInfo serialNumber = type.GetProperty("SerialNumber");
+ Assert.NotNull(serialNumber);
+
+ bool serialIsMandatoryOnSingle = false;
+ foreach (CustomAttributeData attribute in serialNumber.GetCustomAttributesData())
+ {
+ if (attribute.AttributeType != typeof(ParameterAttribute)) { continue; }
+ bool isSingle = false;
+ bool isMandatory = false;
+ foreach (CustomAttributeNamedArgument named in attribute.NamedArguments)
+ {
+ if (named.MemberName == "ParameterSetName" && (string)named.TypedValue.Value == "Single") { isSingle = true; }
+ if (named.MemberName == "Mandatory" && (bool)named.TypedValue.Value) { isMandatory = true; }
+ }
+
+ if (isSingle && isMandatory) { serialIsMandatoryOnSingle = true; }
+ }
+
+ Assert.True(serialIsMandatoryOnSingle);
+ }
+
+ [Fact]
+ public void Profiles_Can_Be_Filtered_To_An_Application_The_Way_The_Console_Groups_Them()
+ {
+ Type type = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.GetInfisicalCertificateProfileCmdlet", true);
+
+ PropertyInfo applicationId = type.GetProperty("ApplicationId");
+ Assert.NotNull(applicationId);
+ Assert.Equal(typeof(string), applicationId.PropertyType);
+
+ PropertyInfo caId = type.GetProperty("CaId");
+ Assert.NotNull(caId);
+ }
+
+ [Fact]
+ public void Profile_Listing_Keeps_Its_Original_Overload_For_Existing_Callers()
+ {
+ Type clientType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalPkiClient", true);
+
+ MethodInfo original = clientType.GetMethod(
+ "ListCertificateProfiles",
+ BindingFlags.Public | BindingFlags.Instance,
+ null,
+ new[] { typeof(PSInfisicalAPI.Connections.InfisicalConnection), typeof(string), typeof(int?), typeof(int?), typeof(bool?) },
+ null);
+ Assert.NotNull(original);
+
+ MethodInfo filtered = clientType.GetMethod(
+ "ListCertificateProfiles",
+ BindingFlags.Public | BindingFlags.Instance,
+ null,
+ new[] { typeof(PSInfisicalAPI.Connections.InfisicalConnection), typeof(string), typeof(int?), typeof(int?), typeof(bool?), typeof(string), typeof(string) },
+ null);
+ Assert.NotNull(filtered);
+ }
+
+ private static List GetCalledMethodNames(MethodInfo method)
+ {
+ List names = new List();
+ MethodBody body = method.GetMethodBody();
+ if (body == null) { return names; }
+
+ byte[] il = body.GetILAsByteArray();
+ if (il == null) { return names; }
+
+ const byte Call = 0x28;
+ const byte CallVirt = 0x6F;
+
+ for (int i = 0; i + 4 < il.Length; i++)
+ {
+ if (il[i] != Call && il[i] != CallVirt) { continue; }
+
+ int token = BitConverter.ToInt32(il, i + 1);
+ try
+ {
+ MethodBase resolved = method.Module.ResolveMethod(token);
+ if (resolved != null) { names.Add(resolved.Name); }
+ }
+ catch (ArgumentException)
+ {
+ }
+ }
+
+ return names;
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationCmdlet.cs
index db11a1b..15e31a7 100644
--- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationCmdlet.cs
+++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationCmdlet.cs
@@ -18,7 +18,7 @@ namespace PSInfisicalAPI.Cmdlets
[Alias("Name")]
public string ApplicationName { get; set; }
- [Parameter(Mandatory = true)] public string ProjectId { get; set; }
+ [Parameter] public string ProjectId { get; set; }
[Parameter(ParameterSetName = "List")] public int? Limit { get; set; }
@@ -29,6 +29,11 @@ namespace PSInfisicalAPI.Cmdlets
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+
+ // The UI never asks which Certificate Manager project to use when an organization has only
+ // 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);
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 c8d3db7..be8ba71 100644
--- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationEnrollmentCmdlet.cs
+++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationEnrollmentCmdlet.cs
@@ -18,13 +18,18 @@ namespace PSInfisicalAPI.Cmdlets
[Alias("CertificateProfileId")]
public string ProfileId { get; set; }
- [Parameter(Mandatory = true)] public string ProjectId { get; set; }
+ [Parameter] public string ProjectId { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+
+ // The UI never asks which Certificate Manager project to use when an organization has only
+ // 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);
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 6b161c0..e125de6 100644
--- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs
+++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs
@@ -14,7 +14,7 @@ namespace PSInfisicalAPI.Cmdlets
[Alias("Id")]
public string CaId { get; set; }
- [Parameter(Mandatory = true)] public string ProjectId { get; set; }
+ [Parameter] public string ProjectId { get; set; }
[Parameter(ParameterSetName = "List")]
[ValidateSet("Internal", "Acme", "Any")]
@@ -25,6 +25,11 @@ namespace PSInfisicalAPI.Cmdlets
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+
+ // The UI never asks which Certificate Manager project to use when an organization has only
+ // 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);
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 4ef7e24..c9b70f7 100644
--- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateCmdlet.cs
+++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateCmdlet.cs
@@ -16,7 +16,7 @@ namespace PSInfisicalAPI.Cmdlets
[Alias("Id", "Identifier")]
public string SerialNumber { get; set; }
- [Parameter(ParameterSetName = "List", Mandatory = true)] public string ProjectId { get; set; }
+ [Parameter(ParameterSetName = "List")] public string ProjectId { get; set; }
[Parameter(ParameterSetName = "List")] public string CommonName { get; set; }
[Parameter(ParameterSetName = "List")] public string FriendlyName { get; set; }
[Parameter(ParameterSetName = "List")] public string Search { get; set; }
@@ -59,6 +59,7 @@ namespace PSInfisicalAPI.Cmdlets
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
+ // Retrieval by serial addresses the certificate directly, so it needs no project.
if (string.Equals(ParameterSetName, "Single", StringComparison.Ordinal))
{
InfisicalCertificate cert = client.RetrieveCertificate(connection, SerialNumber);
@@ -70,6 +71,11 @@ namespace PSInfisicalAPI.Cmdlets
return;
}
+ // The UI never asks which Certificate Manager project to use when an organization has only
+ // 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);
+
InfisicalCertificateSearchQuery query = new InfisicalCertificateSearchQuery
{
ProjectId = ProjectId,
diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatePolicyCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatePolicyCmdlet.cs
index 1046afe..6d8bb82 100644
--- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatePolicyCmdlet.cs
+++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatePolicyCmdlet.cs
@@ -14,7 +14,7 @@ namespace PSInfisicalAPI.Cmdlets
[Alias("Id", "CertificatePolicyId")]
public string PolicyId { get; set; }
- [Parameter(Mandatory = true)] public string ProjectId { get; set; }
+ [Parameter] public string ProjectId { get; set; }
[Parameter(ParameterSetName = "List")] public int? Limit { get; set; }
@@ -25,6 +25,11 @@ namespace PSInfisicalAPI.Cmdlets
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+
+ // The UI never asks which Certificate Manager project to use when an organization has only
+ // 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);
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 6e14bf7..3c985a6 100644
--- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateProfileCmdlet.cs
+++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateProfileCmdlet.cs
@@ -14,7 +14,19 @@ namespace PSInfisicalAPI.Cmdlets
[Alias("Id", "CertificateProfileId")]
public string ProfileId { get; set; }
- [Parameter(Mandatory = true)] public string ProjectId { get; set; }
+ [Parameter] public string ProjectId { get; set; }
+
+ ///
+ /// Narrows the listing to one certificate application, which is how the Infisical console groups
+ /// profiles. See Get-InfisicalCertificateApplication.
+ ///
+ [Parameter(ParameterSetName = "List", ValueFromPipelineByPropertyName = true)]
+ public string ApplicationId { get; set; }
+
+ ///
+ /// Narrows the listing to profiles issued by one certificate authority.
+ ///
+ [Parameter(ParameterSetName = "List")] public string CaId { get; set; }
[Parameter(ParameterSetName = "List")] public int? Limit { get; set; }
@@ -27,6 +39,11 @@ namespace PSInfisicalAPI.Cmdlets
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+
+ // The UI never asks which Certificate Manager project to use when an organization has only
+ // 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);
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal))
@@ -41,8 +58,12 @@ namespace PSInfisicalAPI.Cmdlets
}
bool? includeConfigs = MyInvocation.BoundParameters.ContainsKey("IncludeConfigs") ? (bool?)IncludeConfigs.IsPresent : null;
- InfisicalCertificateProfile[] all = client.ListCertificateProfiles(connection, ProjectId, Limit, Offset, includeConfigs);
- Logger.Information("Get-InfisicalCertificateProfile", string.Concat("Returned ", all.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " certificate profile(s)."));
+ InfisicalCertificateProfile[] all = client.ListCertificateProfiles(connection, ProjectId, Limit, Offset, includeConfigs, ApplicationId, CaId);
+
+ string scope = !string.IsNullOrEmpty(ApplicationId)
+ ? string.Concat(" for application '", ApplicationId, "'")
+ : (!string.IsNullOrEmpty(CaId) ? string.Concat(" for certificate authority '", CaId, "'") : string.Empty);
+ Logger.Information("Get-InfisicalCertificateProfile", string.Concat("Returned ", all.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " certificate profile(s)", scope, "."));
foreach (InfisicalCertificateProfile profile in all)
{
WriteObject(profile);
diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalPkiSubscriberCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalPkiSubscriberCmdlet.cs
index 5a1a37a..972d449 100644
--- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalPkiSubscriberCmdlet.cs
+++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalPkiSubscriberCmdlet.cs
@@ -14,13 +14,18 @@ namespace PSInfisicalAPI.Cmdlets
[Alias("SubscriberName", "Slug")]
public string Name { get; set; }
- [Parameter(Mandatory = true)] public string ProjectId { get; set; }
+ [Parameter] public string ProjectId { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+
+ // The UI never asks which Certificate Manager project to use when an organization has only
+ // 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);
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 49356a8..8785008 100644
--- a/src/PSInfisicalAPI/Cmdlets/InfisicalCmdletBase.cs
+++ b/src/PSInfisicalAPI/Cmdlets/InfisicalCmdletBase.cs
@@ -1,10 +1,13 @@
using System;
+using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Management.Automation;
using System.Runtime.ExceptionServices;
using System.Security.Cryptography.X509Certificates;
using PSInfisicalAPI.Connections;
+using PSInfisicalAPI.Models;
+using PSInfisicalAPI.Projects;
using PSInfisicalAPI.Errors;
using PSInfisicalAPI.Http;
using PSInfisicalAPI.Logging;
@@ -13,9 +16,12 @@ namespace PSInfisicalAPI.Cmdlets
{
public abstract class InfisicalCmdletBase : PSCmdlet
{
+ private const string CertManagerProjectType = "cert-manager";
+
private IInfisicalLogger _logger;
private IInfisicalHttpClient _httpClient;
private bool? _isElevated;
+ private string _resolvedCertManagerProjectId;
protected IInfisicalLogger Logger
{
@@ -49,6 +55,69 @@ namespace PSInfisicalAPI.Cmdlets
return current != null && current.SkipCertificateCheck;
}
+ ///
+ /// Resolves the Certificate Manager project a PKI call should target.
+ ///
+ /// The Infisical UI never asks which project to use: an organization with a single Certificate Manager
+ /// project has it selected implicitly, which is why the project appears only in the URL and everything
+ /// below it is presented as applications. The API behaves the same way - its own resolver takes the
+ /// single cert-manager project when there is exactly one - so requiring -ProjectId on every cmdlet made
+ /// the module stricter than the service it wraps.
+ ///
+ ///
+ /// This mirrors that rule client-side, which is necessary because several PKI endpoints carry the project
+ /// in the URL path and cannot defer to the server's resolver. Resolved once per cmdlet instance.
+ ///
+ ///
+ protected string ResolveCertManagerProjectId(InfisicalConnection connection, string explicitValue)
+ {
+ if (!string.IsNullOrEmpty(explicitValue)) { return explicitValue; }
+ if (_resolvedCertManagerProjectId != null) { return _resolvedCertManagerProjectId; }
+
+ InfisicalProjectClient client = new InfisicalProjectClient(HttpClient, Logger);
+ InfisicalProject[] projects = client.List(connection, CertManagerProjectType, false);
+
+ List certManagerProjects = new List();
+ if (projects != null)
+ {
+ foreach (InfisicalProject project in projects)
+ {
+ if (project == null) { continue; }
+ if (string.Equals(project.Type, CertManagerProjectType, StringComparison.OrdinalIgnoreCase))
+ {
+ certManagerProjects.Add(project);
+ }
+ }
+ }
+
+ 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.");
+ }
+
+ if (certManagerProjects.Count > 1)
+ {
+ List described = new List();
+ foreach (InfisicalProject project in certManagerProjects)
+ {
+ described.Add(string.Concat("'", project.Name ?? project.Slug, "' (", project.Id, ")"));
+ }
+
+ 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;
+ 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, ")."));
+
+ return _resolvedCertManagerProjectId;
+ }
+
///
/// 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
diff --git a/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs
index bd45ba3..d796dc2 100644
--- a/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs
+++ b/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs
@@ -28,7 +28,7 @@ namespace PSInfisicalAPI.Cmdlets
[Alias("ProfileId")]
public string CertificateProfileId { get; set; }
- [Parameter(Mandatory = true)] public string ProjectId { get; set; }
+ [Parameter] public string ProjectId { get; set; }
[Parameter] public IDictionary Subject { get; set; }
[Parameter] public string CommonName { get; set; }
[Parameter] public string Country { get; set; }
@@ -85,6 +85,11 @@ namespace PSInfisicalAPI.Cmdlets
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+
+ // The UI never asks which Certificate Manager project to use when an organization has only
+ // 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);
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
// Resolved once so reuse detection looks in the same stores the install will write to.
diff --git a/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs b/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs
index 4830ab0..864e78a 100644
--- a/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs
+++ b/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs
@@ -530,6 +530,16 @@ namespace PSInfisicalAPI.Pki
}
public InfisicalCertificateProfile[] ListCertificateProfiles(InfisicalConnection connection, string projectId, int? limit, int? offset, bool? includeConfigs)
+ {
+ return ListCertificateProfiles(connection, projectId, limit, offset, includeConfigs, null, null);
+ }
+
+ ///
+ /// Lists certificate profiles, optionally narrowed to one application or issuing CA. Applications are
+ /// how the UI groups profiles, so filtering by application is what makes a script read the way the
+ /// console does.
+ ///
+ public InfisicalCertificateProfile[] ListCertificateProfiles(InfisicalConnection connection, string projectId, int? limit, int? offset, bool? includeConfigs, string applicationId, string caId)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); }
@@ -541,6 +551,8 @@ namespace PSInfisicalAPI.Pki
if (limit.HasValue) { query.Add(new KeyValuePair("limit", limit.Value.ToString(CultureInfo.InvariantCulture))); }
if (offset.HasValue) { query.Add(new KeyValuePair("offset", offset.Value.ToString(CultureInfo.InvariantCulture))); }
if (includeConfigs.HasValue) { query.Add(new KeyValuePair("includeConfigs", includeConfigs.Value ? "true" : "false")); }
+ if (!string.IsNullOrEmpty(applicationId)) { query.Add(new KeyValuePair("applicationId", applicationId)); }
+ if (!string.IsNullOrEmpty(caId)) { query.Add(new KeyValuePair("caId", caId)); }
try
{