refactor!(scoping): mandate explicit -ProjectId/-Environment; add -Type/-IncludeRoles to Get-InfisicalProject

BREAKING CHANGES
- Connect-Infisical no longer accepts -ProjectId, -Environment, or -SecretPath.
- InfisicalConnection no longer carries ProjectId, Environment, or DefaultSecretPath.
- Every cmdlet that previously inherited those fields now requires -ProjectId
  and/or -Environment as Mandatory=true. -SecretPath / -Path remain optional
  and default to "/" at the client layer.
- INFISICAL_PROJECT_ID, INFISICAL_ENVIRONMENT, INFISICAL_SECRET_PATH env-var
  scanning removed from Connect-Infisical.
- Resolve{ProjectId,Environment,SecretPath} helpers removed from
  InfisicalCmdletBase. ResolveOrganizationId retained.

ADDED
- Get-InfisicalProject -Type <enum> filters the list by product surface
  (secret-manager, cert-manager, kms, ssh, secret-scanning, pam, ai) with
  IntelliSense via ValidateSet.
- Get-InfisicalProject -IncludeRoles switch maps to includeRoles=true/false
  query parameter (always sent).

RATIONALE
- Implicit connection scoping caused 400 Bad Request when the active
  connection's ProjectId belonged to a different product surface than the
  cmdlet's target (e.g. secret-manager project id passed to /cert-manager/*).
- Explicit parameters make scope unambiguous and make scripts portable
  across projects.
- The new -Type filter on Get-InfisicalProject lets callers discover the
  correct project id for each subsequent CRUD invocation without needing
  connection-level inheritance.

INTERNAL
- All client classes (Secrets / Folders / Environments / Tags / Projects /
  Pki) now receive scoping as explicit arguments rather than reading the
  InfisicalConnection object.
- Client-layer SecretPath / Path defaulting to "/" is preserved via
  FirstNonEmpty(...).
- Help XML updated to remove all "session-pinned" / "active connection"
  phrasing; OrderedDictionary splatting examples now include the mandatory
  parameters.
- 216/216 unit tests passing.
This commit is contained in:
GraceSolutions
2026-06-04 21:16:52 -04:00
parent 7ae5d4a59d
commit cffda99591
42 changed files with 379 additions and 617 deletions
@@ -5,7 +5,6 @@ using System.Reflection;
using PSInfisicalAPI.Cmdlets;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Logging;
using PSInfisicalAPI.Models;
using Xunit;
namespace PSInfisicalAPI.Tests
@@ -26,21 +25,6 @@ namespace PSInfisicalAPI.Tests
[Cmdlet(VerbsCommon.Get, "TestCmdlet")]
private sealed class TestCmdlet : InfisicalCmdletBase
{
public string CallResolveProjectId(InfisicalConnection connection, string explicitValue)
{
return ResolveProjectId(connection, explicitValue);
}
public string CallResolveEnvironment(InfisicalConnection connection, string explicitValue)
{
return ResolveEnvironment(connection, explicitValue);
}
public string CallResolveSecretPath(InfisicalConnection connection, string explicitValue)
{
return ResolveSecretPath(connection, explicitValue);
}
public string CallResolveApiVersion(InfisicalConnection connection, string explicitValue)
{
return ResolveApiVersion(connection, explicitValue);
@@ -65,60 +49,11 @@ namespace PSInfisicalAPI.Tests
return new InfisicalConnection
{
BaseUri = new Uri("https://app.example.com"),
ProjectId = "proj-conn",
Environment = "prod-conn",
DefaultSecretPath = "/db",
OrganizationId = "org-conn",
PinnedApiVersion = "v3"
};
}
[Fact]
public void Explicit_Value_Overrides_Connection_And_Does_Not_Log()
{
RecordingLogger logger = new RecordingLogger();
TestCmdlet cmdlet = CreateCmdletWith(logger);
string resolved = cmdlet.CallResolveProjectId(ConnectionWithDefaults(), "explicit-proj");
Assert.Equal("explicit-proj", resolved);
Assert.Empty(logger.VerboseEntries);
}
[Fact]
public void Missing_Value_Inherits_From_Connection_And_Logs()
{
RecordingLogger logger = new RecordingLogger();
TestCmdlet cmdlet = CreateCmdletWith(logger);
string resolved = cmdlet.CallResolveProjectId(ConnectionWithDefaults(), null);
Assert.Equal("proj-conn", resolved);
Assert.Single(logger.VerboseEntries);
Assert.Contains("Inherited ProjectId", logger.VerboseEntries[0]);
Assert.Contains("proj-conn", logger.VerboseEntries[0]);
}
[Fact]
public void ResolveSecretPath_Defaults_To_Root_When_Connection_Has_No_Default()
{
RecordingLogger logger = new RecordingLogger();
TestCmdlet cmdlet = CreateCmdletWith(logger);
InfisicalConnection bareConnection = new InfisicalConnection { BaseUri = new Uri("https://app.example.com") };
string resolved = cmdlet.CallResolveSecretPath(bareConnection, null);
Assert.Equal("/", resolved);
}
[Fact]
public void ResolveSecretPath_Inherits_From_Connection_When_Set()
{
RecordingLogger logger = new RecordingLogger();
TestCmdlet cmdlet = CreateCmdletWith(logger);
string resolved = cmdlet.CallResolveSecretPath(ConnectionWithDefaults(), null);
Assert.Equal("/db", resolved);
Assert.Contains(logger.VerboseEntries, v => v.Contains("SecretPath") && v.Contains("/db"));
}
[Fact]
public void ResolveApiVersion_Prefers_PinnedApiVersion_From_Connection()
{
@@ -130,14 +65,24 @@ namespace PSInfisicalAPI.Tests
}
[Fact]
public void ResolveEnvironment_And_ResolveOrganizationId_Inherit()
public void ResolveOrganizationId_Inherits_From_Connection_And_Logs()
{
RecordingLogger logger = new RecordingLogger();
TestCmdlet cmdlet = CreateCmdletWith(logger);
Assert.Equal("prod-conn", cmdlet.CallResolveEnvironment(ConnectionWithDefaults(), null));
Assert.Equal("org-conn", cmdlet.CallResolveOrganizationId(ConnectionWithDefaults(), null));
Assert.Equal(2, logger.VerboseEntries.Count);
Assert.Single(logger.VerboseEntries);
Assert.Contains("OrganizationId", logger.VerboseEntries[0]);
}
[Fact]
public void ResolveOrganizationId_Explicit_Value_Wins_And_Does_Not_Log()
{
RecordingLogger logger = new RecordingLogger();
TestCmdlet cmdlet = CreateCmdletWith(logger);
Assert.Equal("explicit-org", cmdlet.CallResolveOrganizationId(ConnectionWithDefaults(), "explicit-org"));
Assert.Empty(logger.VerboseEntries);
}
}
}